JS: Accumulate from 0系列之变量&操作符

作者 Kylewh 日期 2017-03-07
JS
JS: Accumulate from 0系列之变量&操作符

本文内容讲述JS里最基本的知识:变量 和 操作符

变量的声明

变量的声明有三种方法: var, let, const

  • var 声明一个变量,将其初始化成一个值,不支持块级作用域,存在变量提升。注意:使用var操作符定义的变量将成为定义该变量的作用域中的局部变量,也就是说,如果在函数中使用var定义一个变量,在函数执行完毕生命周期结束时这个局部变量也会被销毁。
var a;
console.log(a); // undefined;
a = 1;
console.log(a); // 1
//不支持块级作用域
var b = 10;
{
var b = 100;
}
console.log(b); //100
//变量提升
console.log(x); //undefined
var x = 100;
console.log(x); //100

特殊的一点: 在函数内部声明变量如果不使用var操作符的话这个变量会被创建成一个全局变量

function test(){
message = 'hi'; //global variable
}
test();
console.log(message); //hi;
  • let 声明一个块级作用域变量, 可以将其初始化成一个值。在同一作用域中不允许重复声明,存在暂存死区(不支持变量提升)。
var a = 100;
{
let a = 10;
}
console.log(a); //100
console.log(b); //"ReferenceError: b is not defined
let b = 9;
//let b = 99; //报错
//块级作用域妙用 循环
for(let i =0; i<10; i++) {
console.log(i);
}
//0,1,2,3,4,5,6,7,8,9 不需要使用闭包了
  • const 声明的标识符绑定的值不可以再次改变,其他行为和let一样
const a = 10;
a = 100; //"TypeError: Assignment to constant variable.
const b = {
'one': 1,
'two': 2
};
b.one = 2;
//此时绑定的是对object的引用,通过这个引用我们可以改变里面key对应的值。
console.dir(b);
/*
[object Object] {
one: 2,
two: 2
}
*/
//改变引用则报错
b = {}; //"TypeError: Assignment to constant variable.
//利用闭包模拟静态变量
var Conf = (function () {
//私有变量
var conf = {
STOCK: 100, //静态变量都大写
PRICE: 50
};
//返回取值器对象
return {
get: function (name) {
return conf[name] ? conf[name] : null;
}
}
})();
console.log(Conf.get('STOCK')); //100
//只能获取,不能修改。

严格模式:use strict下,不声明进行使用会报错:

(function(){
'use strict';
var x = y = 0; //"ReferenceError: y is not defined
})();

数据类型

ECMAScript中有6种简单数据类型,Undefined, Null, Boolean, Number, String, Symbol. 一种复杂数据类型Object(Function).

typeof操作符

用来检测给定变量的数据类型:

console.log (
typeof a, //undefined;
typeof false, //boolean
typeof 'kylewh', //string
typeof 666, //number
typeof {}, //object
typeof (function(){}), // function
typeof null //object 历史遗留问题 object的源头
);

PITFALL ALERT

  • 对于未定义和未初始化的变量,typeof对其的检测结果都是undefined,这说明了显式初始化变量的重要性,以使我们可以分辨一个变量是否已经被声明。
console.log( typeof a );//undefined;
var b;
console.log( typeof b ); //undefined;
  • 因为基本包装类型的存在,依赖typeof进行类型检测并不可靠:(PS:基本包装类型从属于引用类型,在之后将介绍,期间还会有一个小trick,留意哦)
var a = new Number(9);
console.log( typeof a ); //object;
  • 可靠的检测方法
var Type = {};
for (var i = 0,type; type=['String', 'Array', 'Number'][i++]; ) {
(function(type){
Type['is' + type] = function (obj) {
return Object.prototype.toString.call(obj) === '[Object ' + type + ']';
}
})(type)
}
console.log ( Type.isArray([])); //true

Undefined类型

Undefined类型只有一个值: undefined,其他内容上面已经带着说了一下。

Null类型

它是第二个只有一个值的类型,它可以被看做javascript一切对象的起源,后面讲原型的时候会讲到。null值表示一个空对象指针,这也是为什么检测null用typeof会返回obejct的原因。 如果生命了以后对象是为了以后保存一个对象,那么可以显式的将其初始化为null而不是其他值。

console.log( null == undefined ); //这里==操作符其实对操作数做了转换

undefined其实派生自null.

Boolean类型

两个值: ture and false.如果想要将其他类型的值转化为boolean类型,可以使用转型函数Boolean(); 在if语句中,括号里的值也会被调用Boolean()进行转型,转型结果见下面falsy value.

var mes = 'hi there';
console.log(Boolean(mes)); //true

另外我们也可以使用!!让一个值转化为boolean类型:

var b = {};
console.log(!!b, typeof !!b); //true, boolean

falsy value

所谓falsy value就是这些值在进行转型时会被转化成false的值:

  • false`
  • “”(空字符串)
  • 0-0 , NaN
  • null
  • undefined

而除此以外,非空字符串,非0数字值,包括Infinity,任何对象,都能被转化为true.

Number类型

整数范围:-2^53 - 2^53, Number类型具有不同进制数,ECMA-262定义了不同的数值字面量格式:

  • 八进制第一位必须为0,然后是八进制数字序列(0-7),如果字面值中的数值超出了范围,前导的0将会被忽略,后边的数值将被当做十进制数值解析。
  • 十六进制前两位必须是0x, 后跟任何十六进制数字(0-9,A-F), A-F字母可以大写也可以小写。
var intNum = 55; //整数
var octalNum1 = 070; //八进制56
var octalNum2 = 079; // 9超出0-7范围,解析失败,忽略0,当成十进制解析 结果为79
var hexNum1 = 0xA; //十六进制的10
//严格模式下不允许八进制和十六进制的字面量写法 'use strict'
```
**PITFALL ALIERT**
```javascript
var hexNum2 = 0xZ; //十六进制的10
//SyntaxError: Invalid or unexpected token 记住十六进制不像八进制那样可以进行宽容解析。

进行算术计算时,所有以八进制和十六进制表示的数值最终都将被转化成十进制,也就是说计算使用十进制。

let result = hexNum1 + octalNum1 + intNum;
//10+56+55 = 121

浮点值

  • 内存占用为整数的两倍,所以JS会抓住时机将其转化为整数,比如小数点后没有任何数字时,它就会被保存为整数: var a = 1.;, 如果浮点值本身表示的就是整数,也会被转化成整数: var b = 11.0;
  • JS的浮点数计算使用基于IEEE754数值的浮点计算: 0.1 + 0.2 !== 0.3
  • 可以使用科学计数法: var floatNum = 3.12e7,等于31250000.
  • Number.MIN_VALUE : 5e-324
  • Number.MAX_VALUE : 1.7976931348623157e+308
  • 如果一个数值超越了数值范围,会被转化成Infinity(带正负号),可以用isFinite()判断一个值是否有穷。
  • NaN,任何涉及NaN的值都会返回NaN, 不和任何值相等,包括自己,所以有一个isNaN()函数用来判断它,接受一个值,使用Number()转型函数将其转换成数字,然后判断其是不是NaN.
  • 如果传入对象,会先调用valueOf()方法,如果不能转化为数值,就调用toString()方法,再返回测试值

数值转换

Number(), parseInt(), parseFloat(), Number可以把任何数据类型转型成数值,后面两个方法专注于将字符串转化成数值。

  • 如果是Boolean值,true和false分别别转化成1和0
  • 如果是数字,简单的传入和返回
  • 如果是null,返回0
  • 如果是undefined,返回NaN
  • 如果是对象,调用对象的valueOf方法,然后按前面的规则转化,如果结果是NaN,则调用toString方法,再按照前面的方法转化。
//利用一元加操作符可以将非数值转换成数值,(利用Number());
var date = new Date();
console.log( +date ); //1488956237515
var o = {
valueOf: function () {
return -1;
}
}
console.log( +o ); // -1;

parseInt(): 它会忽略传入字符串前面的空格,直到找到第一个非空格字符串,如果第一个字符不是数字字符,或者符号,返回NaN。如果第一个字符是数字,则继续解析,直到解析完所有字符或者遇到非数字字符则停下来。

var num1 = parseInt(' a111'); //NaN
var num2 = parseInt(' 0111a '); //111

在ECMAScipt 5 JavaScript引擎中,已经不支持对八进制的解析,所以可以传入第二个参数进行制定制式解析。

var num = parseInt('0xAF', 16); //175
//传入基数后,可以不加前缀0x
var num2 = parseInt('AF', 16); //175
var num3 = parseInt('AF'); //NaN 未传入基数

parseFloat(): 始终会忽略前导的0,只解析十进制数值,而且如果转化后的小数满足转化成整数的原则的话,会自动转化为整数: ‘1.’=> 1, ‘11.0’=>11

暂时更到这里,明天继续。

String类型

表示

单引号or双引号,前后统一。 单引号使用较多,见如下情况:

var tpl = '<div class="container">' + content + '</div>';
//如果用了双引号,那么在class处的双引号就会把字符串分割。

转义

/n:换行, \' or \" 单引号,双引号。 \unnnn以十六进制代码nnnn代表一个Unicode字符.

任何字符串的长度都可以通过访问length属性取得,要注意的是如果字符串包含双字节字符,那么length可能不会精确的返回字符串中的字符数目。

var testStr = '猜猜我的长度是多少hahaha?';
console.log(testStr.length); //16

不可变

字符串一旦创建,它们的值就不能改变,我们所见到的变量保存字符串的重新赋值,其实是已经销毁了以前的字符串.

var str = 'Old one';
str += 'I am a new one';
// 创建一个新的字符串能够容纳以前的字符串+现在增加的字符串,填充内容。
// 销毁以前的字符串

转换

第一种方式: toString()

var age = 25;
var ageStr = age.toString(); //'25'
var _age = 10;
var binaryStr = _age.toString(2); // '1010' 可以传入进制数使数字在被转换时转换成相应的进制
//如果值是undefined,返回undefined。
//如果是null,返回null。
//如果值具有toString方法,调用并返回相应的结果。
var value;
value.toString(); //undefined;
var value2 = null;
value2.toString(); //null;
var date = new Date();
console.log( date.toString() ); //"Wed Mar 08 2017 14:32:31 GMT+0800 (CST)"
var foo = {
toString() {
return 'foo';
}
};
console.log( foo + ' bar' ); // foo bar

第二种方式: 使用加号(+)操作符,将某个值跟另一个字符串加在一起,就会将这个值转换为字符串。

var age = 25;
var str = age + '';
console.log(str, typeof str); //'25', 'string'

包装类型

Boolean, Number, String也属于特殊的引用类型,它们同时也具有各自基本类型的特殊行为,在每读取一个基本类型值的时候,后台就会建立一个对应的基本包装类型的对象(作为引用类型的由来),可以让我们调用一些方法来操作这些数据。

看个例子:

let s1 = 'i am a string';
let s2 = s1.substring(7);

我们知道s1是一个字符串,那它理应属于原始类型数据,那我们又何从调用一个方法呢?其实在第二行代码后台访问s1的时候,会处于一种读取模式,即从内存中读取这个字符串的值,而在读取模式中,后台也会自动完成以下三个步骤:

  1. 创建String类型的一个实例。
  2. 在实例上调用指定的方法。
  3. 销毁这个实例。

可以看作是隐式执行了如下代码:

let s1 = new String('i am a string');
let s2 = s1.substring(7);
s1 = null;

还记得我们之前说过,javascript的一切都是对象,这句话是没错的,就是因为存在对于基本类型的包装对象,只是我们很难明显的察觉到它。

我们也可以显式的调用String, Number, Boolean来创建一个基本包装类型的对象,但是这样的做法是不推荐的,因为这样让我们很难分清我们到底是在对于原始数据类型进行操作,还是对引用类型进行操作。所以不是绝对必要的情况下,还是使用字面量的形式创建。

我们也可以使用Object创建基本包装类型对象,而Object会自动识别传入值的类型而返回相应的基本包装类型的实例,见如下代码:

let str_obj = new Object('hunger');
console.log ( str_obj instanceof String

要注意的是,使用new调用基本类型的构造函数,与直接调用同名的转型函数是不一样的:

let num = Number('92');
console.log ( typeof num ); //number; num保存着原始类型
let num2 = new Number(92); //可以传入字符串
console.log( typeof num2 ); // object; num2保存着Number的实例
console.log( Object.prototype.toString.call(num) == Object.prototype.toString.call(num2) ); // true
//可见在进行类型判断时,使用Object.prototype.toString.call()更为稳妥。
//实际上Object、Array、Date和RegExp,这些引用类型都是 JavaScript 的内置对象。typeof运算符在判断这些引用类型时全都返回"object"。

那么,对于基础类型和复杂类型,它们的区别是什么呢?
基础类型保存的是一个值,而复杂类型保存的是内存地址,通过访问内存地址,我们可以接触到里面的基础类型所保存的值。在传递时,基础类型直接传递值,而复杂类型直接传递内存地址,也就是我们所说的:引用。当我们使用引用传递时,通过变量访问这些地址,从而进行一些修改,无论这些引用地址被传递给多少个变量,通过其中任何一个进行修改,都会使堆内存中保存的对象发生相应变化。

let obj = { "a": 1, "b" : 2 }; // push a address to stack, and was placed in heap.
let a = 1; //push to stack, above the obj.
let b = 2; // push to stack, above the a.

JavaScript中的内存也分为栈内存和堆内存。一般来说,栈内存中存放的是存储对象的地址,而堆内存中存放的是存储对象的具体内容。对于原始类型的值而言,其地址和具体内容都存在与栈内存中;而基于引用类型的值,其地址存在栈内存,其具体内容存在堆内存中。堆内存与栈内存是有区别的,栈内存运行效率比堆内存高,空间相对推内存来说较小,反之则是堆内存的特点。所以将构造简单的原始类型值放在栈内存中,将构造复杂的引用类型值放在堆中而不影响栈的效率。

Object类型

它是一组数据和功能的集合,可以通过执行new操作符后跟要创建的对象类型的名称来创建。

var o = new Object();

它有一些属性和方法,我们会在后面的引用类型专题详细说:

  • constructor: 保存着用于创建当前对象的函数
  • hasOwnProperty(propertyName): 用于检查某个属性在当前对象的实例中而不是在实例的原型中,传入的参数必须是字符串形式。
  • isPrototypeOf(object): 用于检查传入的对象是否是当前对象的原型。
  • propertyIsEnumerable(propertyName): 用于检查给定的属性是否能够在for-in语句中被枚举出来。
  • toLocaleString(): 返回对象的字符串表示,这个字符串跟执行环境的地区对应。
  • toString()
  • valueOf: 返回对象的字符串,数值或布尔表示,通常与toString的返回值相同,之前我们提到过如果使用Number进行转型时也会先调用valueOf方法。
let obj = {
valueOf: function () {
return '666';
}
};
let objNum = Number(obj);
console.log(objNum); //666

操作符

一元操作符

//自加
var age = 25;
console.log( 'Next year I will be ' + (++age) + ' years old'); // "Next year I will be 26years old"
var _age = 25;
console.log( 'This year I am ' + (_age++) + ' years old, and next year I will be ' + _age ); //"This year I am 25 years old, and next year I will be 26"
//同理自减,操作符在前后决定了操作是在计算之前或之后进行。

布尔操作符

逻辑非

  • 如果操作数是一个对象,返回false.
  • 如果操作数是一个空字符串,返回true.
  • 如果操作数是一个非空字符串,返回false.
  • 如果操作数是数值0,返回true.
  • 如果操作数是任意非0数值,返回false.
  • 如果操作数是null,返回true.
  • 如果操作数是NaN,返回true.
  • 如果操作数是undefined,返回true.
//前面提过:同时使用两个逻辑非操作符,就会模拟Boolean()转型函数的行为,将其转化成一个boolean类型的值
alert(!!"blue"); //true
[] == false; //

逻辑与

//Returns expr1 if it can be converted to false; otherwise, returns expr2.
//注意: 逻辑与并不是对前后两个值求boolean结合值
//第一个操作数如果是对象,返回第二个操作数
console.log( {} && undefined ); //不是返回false,返回undefined
//有一个操作数为null,就返回null
console.log ( true && null ); //null
//有一个操作数为NaN,就返回NaN
console.log( parseInt('haha123') && null ); //NaN
//短路特性:如果第一个操作数能够决定结果,那么就不会再对第二个操作数求值
console.log( NaN && null ); //NaN
console.log( null && NaN ); //null
//未定义的值不能进行操作
var unknown;
console.log( unknown && undefinedVariable ); //error
//利用短路特性
var unknown = false;
console.log( unknown && undefinedVariable ); //false

逻辑或

//Returns expr1 if it can be converted to true; otherwise, returns expr2.
//如果第一个操作数是对象,则返回第一个操作数
console.log( [] || false ); //[]
console.log( new Object() && function(){} );

||经常用来设置默认值:

function Student (name, age, skill) {
this.name = name;
this.age = age || alert('请必须提供年龄');
this.skill = skill || '暂未提供技能';
}
var me = new Student('kyle', '25'); //必须提供年龄
console.log (me.skill); //暂未提供技能
//这个技巧在之后我们还会频繁使用

加性操作符

PITFALL ALERT

var num1 = 10;
var num2 = 5;
var message = "The sum of num1 and num2 is " + num1 + num2;
console.log(message); //The sum of num1 and num2 is 105
//变成了字符串相加, 我们必须用括号将这个运算括起来

减性操作符

//对一个字符串数字进行-0,可以将其转换为数字
console.log('10'-0, typeof ('10'-0)); // 10 "number"

关系操作符

// 如果两个操作符都是数值,则进行数值比较
// 如果两个操作符都是字符串,则比较两个字符串对应的字符编码值
console.log ( 'a' < 'b' ); //true
console.log ( 'a' > 'z' ); //false
// 如果一个操作数是数值,则将另一个操作数转换为一个数值,再进行数值比较
console.log( '23' < 3 ); //false
console.log( '23' < '3'); //true

相等操作符

非严格相等:

  • 如果有一个操作数是布尔值,在比较相等性之前先将其转化为数值: false转换为0,而true转换成1.
  • 如果一个操作数是字符,另一个操作数是数值,在比较相等性之前先将字符串转换为数值
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf(),转化成基本类型值以后再去比较。 (转化成原始值)
  • 如果二者都是对象,则比较它们是不是同一个对象,如果两个操作数都指向同一个对象,则相等操作返回ture.
console.log([null !== undefined, null == undefined]); //true, true
console.log(['1' == 1, [] == 0, '' == 0, 0 == false, 1 == true]); //[true, true, true, true, true]
console.log([NaN != NaN]); //[true]
//如果一个操作数是对象,另一个不是,则调用对象的valueOf(),转化成基本类型值以后再去比较。

严格相等

//常用于作参数检测,要求类型和值都相等
//如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值
console.log( '10' == 10); //true
console.log( '10' === 10); //false
//在switch语句中的case判定使用的便是严格相等'==='
var i = '10';
switch(i) {
case 10:
console.log('是一个数字');
break;
case '10':
console.log('是一个字符串');
break;
default:
console.log('没有匹配到结果');
} //"是一个字符串"
//结合上面的内容看一道题
console.log([[]==false, []==![]]);
// 答案是[ true,true ]
// 当相等操作符的一边有布尔值的时候,将布尔值转换成数字
// [] == false => [] == 0;
// 当操作数有一个是对象,另一个不是,则将对象转化为原始值,先调用valueOf,再调用toString
// [] == 0 => '' == 0;
// 如果一个操作数是字符串,另外一个是数值,则在比较之前先把字符串转化为数值
// '' == 0 => 0 == 0 => true
// 逻辑非: 如果操作数是对象,返回false
// [] == false 同上。