《你不知道的JavaScript -中卷》读书笔记

你不知道的JavaScript 中

一、类型

类型是值的内部特征,定义了值的行为,以便于与其他值进行区分。例如数字42字符串42,开发者对这两个值的操作方式必然会不同。

1. JavaScript语言中一共有七种内置类型,除object以外,其他6种都是基本类型

|类型|释义|typeof类型检测| |:-:|:-:|:-:| |null|空值|'object'| |undefined|未定义|'undefined'| |boolean|布尔值|'boolean'| |string|字符串|'string'| |number|数字|'number'| |object|对象|'object'| |symbol|符号,ES6新增|'symbol'|

除了null外,其他6种类型使用typeof检测的结果都与其类型相同。 可以使用一下方法来检测某个值是否为null类型:

var a = null;  
(!a && typeof(a) === 'object')  // => true

函数和数组都是对象,更准确的说是object的子类型

2. 变量、值和类型

JavaScript中的类型是针对值而言的,变量本身是没有类型的。变量可以根据需要被赋予各种不同类型的值。

var a;  
a // => undefined  
b // => ReferenceError: b is not defined

typeof a // => undefined  
typeof b // => undefined,typeof有特殊的安全防范机制,不会报错  

变量声明、赋值、定义:

  • 在作用域中声明但是没有赋值的变量 => undefined
  • 未在作用域中声明的变量 => ReferenceError,not defined (undeclared)

二、值

1. 数组

数组可以通过数字进行索引,也可以包含字符串键值和属性

var a = [];  
a[0] = 1;  
a['foo'] = 2;  
a['12'] = 3;

a.length // 13  
a.foo // 2  
a['foo'] // 2 

delete a['foo'];  
a.length // 13  
  • 通过字符串键值和属性的形式加如数组的元素,并不计算在数组的长度内
  • 若字符串键值能够被强制转化为数字的话,此键值会被当做数字索引来处理
  • 数组长度由所有数字索引中最大值来决定,并不是其真正所包含的元素个数
  • 使用delete可以将单元从数组中删除,但是删除后并不会对数组的length属性产生影响
  • a[1], a[2]... 等这些未赋值的空白单元的值均为undefined

建议使用对象来存放键值/属性值,用数组来存放数字索引值

2. 字符串

字符串和数组很相似,都是类数组且享有很多共同的方法,但也有一定的区别: js中字符串是不可变的,而数组是可变化的。针对字符串的一系列操作不会改变其原始值,而数组是在其原始值上进行操作的

var a = [0, 1, 2, 3];  
var b = '0123';

a.push(4);  
console.log(a); // [0, 1, 2, 3, 4],改变了数组a

b.concat('4');  
console.log(b); // '0123',字符串b保持不变  

有些数字有的方法字符串并不支持,比如反转reverse函数,可以通过split先将字符串转为字符数组,然后使用数组的方法对其进行操作,最后用join将字符数组转化为字符串。

3. 数字

js只有一种数值类型:number,包括整数和带小数的十进制数。而js中的整数就是没有小数的十进制数,所以1.0等同于1。

js使用的是双精度格式(64位2进制)

默认情况下,数字都是以十进制显示,小数部分最后面的0会被省略

3.1 精度

二进制浮点数有一个问题:

0.1 + 0.2 === 0.3 // false

造成这个问题的原因是:二进制浮点数中的0.1和0.2并不是十分精确,他们相加的结果是一个比较接近0.3的一个数字0.30000000000000004,所以判断结果为false。

js在处理整数时,大部分数字类型是绝对安全的,但是在处理小时时需要特别注意!

上述问题的解决方法:设置一个误差范围值即机器精度,通常是2^-52,这个值定义在Number.EPSILON中(ES6开始)。

if(!Number.EPSILON) {  
    Number.EPSILON = Math.pow(2, -52);
}

function checkEqualEnough(n1, n2) {  
    return Math.abs(n1 - n2) < Number.EPSILON;
}

var a = 0.1 + 0.2;  
var b = 0.3;  
checkEqualEnough(a, b); // true  

3.2 整数的安全范围与检测

  • 最大整数:2^53 - 1,Number.MAXSAFEINTEGER

  • 最小整数:1 - 2^53,Number.MINSAFEINTEGER

  • 是否整数:Math.isInteger(num)

  • 是否安全整数:Math.isSafeInteger(num)

3.3 特殊数值

    1. undefined:只有一个值,即 undefined

undefined是标识符,可以被当作变量来使用和赋值。如果要将代码中的值设为undefined,可以使用void。如:

var a = 'example';  
console.log(void a);  
    1. null:只有一个值,即 null

null是一个特殊关键字,不是标识符,不可以当作变量使用个赋值

    1. NaN:Not a Number,无效数值,执行数学运算失败后的返回结果。NaN是唯一一个非自反的值,即NaN != NaN。
    1. Infinity(Number.POSITIVE_INFINITY):无穷数
    1. Object.is(a, b):判断两个值是否绝对相等。能够使用 = 时尽量不要用Object.is来比较,因为前者效率更高,后者主要用于处理特殊的相等比较。

4. 值和引用

js引用指向的是值。若一个值有10个引用,这10个引用指向的都是同一个值,他们相互之间没有引用或指向关系。js对值和引用的赋值或传递完全是由值的类型来决定的,我们无法自行决定使用值复制还是引用复制。

var a = 2; // 2是一个标量基本类型值,a持有该值的一个副本  
var b = a; // b是a的值的一个副本,b的变化不会影响a  
b++;  
a; // 2  
b; // 3

var c = [1, 2, 3]; // c指向复合值[1, 2, 3]的一个引用  
var d = c; // d指向[1, 2, 3]的另一个引用,指向并不是持有,c和d更改的是同一个值  
d.push(4);  
c; // [1, 2, 3, 4]  
d; // [1, 2, 3, 4]  
  • 简单值:即标量基本类型值,总是通过值复制的方式来赋值或传递,包括null、undefined、字符串、数字、bool和symbol。标量基本类型值不可更改。

  • 复合值:对象,包括数组、封装对象和函数,通过引用复制的方式来赋值或传递。一个引用无法更改为另一个引用的指向。

function foo(x) {  
    x.push(4);
    x; // [1, 2, 3, 4]

    x = [4, 5, 6];
    x.push(7);
    x; // [4, 5, 6, 7]
}
var a = [1, 2, 3];  
foo(a);  
a; // [1, 2, 3, 4]

三、原生函数(内建函数)

|内建函数|含义| |:--|:--:| |String()|| |Number()|| |Boolean()|| |Array()|| |Object()|| |Function()|| |RegExp()|正则表达式| |Date()|时间| |Error()|异常| |Symbol()|ES6引用|

内建函数可以当做构造函数使用,但是通过构造函数创建出来的是封装了基本类型值的封装对象

1. 内部属性[[Class]]

所有typeof返回值为"object"的对象都包含一个内部属性[[Class]],这个属性可以通过Object.prototype.toString.call(...)来获取。

  • 多数情况下,对象的内部[[Class]]属性和创建改对象的内建原生构造函数相对应。
var arr = [1, 2, 3, 4];  
Object.prototype.toString.call(arr); // '[object Array]'  
  • 基本类型值,除去null和undefined,其他基本类型值被各自的封装对象自动包装。

|基本类型值|[[Class]]| |:---:|:---:| |null|'object Null'| |undefined|'object Undefined'| |'abc'|'object String'| |123|'object Number'| |true|'object Boolean'|

2. 封装对象包装与拆封

  • 使用Object(...)方法可以自行封装基本类型值,但是不推荐直接使用封装对象。

  • valueOf()函数可以获取封装对象中的基本类型值,在需要用到封装对象总的基本类型值的地方会发生隐式拆封。

var a = new String('abc');  
var b = a + '';  
typeof a; // 'object'  
typeof b; // 'string'  

3. 原生函数

用常量的形式来创建array、object、function和regexp等和用其对应的构造函数的效果是一样的,使用常量方式创建的值都是通过封装对象来包装。

3.1 Array()

  • Array(1, 2, 3)new Array(1, 2, 3)的效果是一样的。
  • 当Array构造函数只带有一个数字参数时,改参数会被作为数组的预设长度,而不是数组中的元素。
  • 至少包含一个空单元的数组称为稀疏数组。【将数组的length属性设置为超过其实际单元数的值、delete数组中的一个单元】
  • 不要创建和使用空单元数组

3.2 Object()、Function()、RegExp()

  • 尽量不要使用Object()、Function()、RegExp()
  • RegExp()可以用来动态定义正则表达式
var name = 'name';  
var namePattern = new RegExp('\\b(?:'+name+')\\b', 'ig');  

3.3 Date()、 Error()没有对应的常量形式作为替代

3.4 Symbol()-符号,是一种简单标量基本类型,不可以带new关键字

3.5 原生原型

原生构造函数有自己的.prototype对象,这些对象包含其对应子类型所特有的行为特征。

  • String.prototype => 字符串
  • Function.prototype => 函数
  • Array.prototype => 数组
  • RegExp.prototype => 正则表达式

四、强制类型转换

1. 值类型转换

  • 类型转换:将值从一种类型转换为另一种类型。
  • 强制类型转换:隐式的类型转换称为强制类型转换。强制类型转换总是会返回标量基本类型值,比如字符串、数字和布尔值,而不会返回对象和函数。

类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时,在JavaScript中通常将他们统称为强制类型转换

var a = 12;  
var b = a + ''; // 隐式强制类型转换  
var c = String(a); // 显示强制类型转换  

2. 抽象值操作

2.1 ToString 非字符串=>字符串

  • 基本类型值
null => 'null'  
undefined => 'undefined'  
true => 'true'  
42 => '42' // number  
1.07e21 => '1.07e21' // 极大和极小的数字使用指数形式  
  • 非自定义的普通对象返回内部属性的[[Class]]值
var a = {};  
a.toString(); // "[object Object]"

var a = [1, 2, 3];  
a.toString(); // "1,2,3",如果对象有自己的toString()方法,字符串化时会调用该方法并返回值。数字的toString()方法经过了重新定义,将所有单元字符串化以后再用","连接起来。  

JSON字符串化

JSON.stringify(...)函数将JSON对象序列化为字符串,这个过程中用到了ToString,而JSON字符串化并不是严格意义上的强制类型转换。

(1) 不安全的JSON值: undefined、function、symbol和包含循环引用的对象。

JSON.Stringify(...)在遇到不安全值时会自动将其忽略:

在数组中则会返回null,以确保单元位置不变;

对包含循环引用的对象执行JSON.stringify(...)会出错;

如果对象中定义了toJSON()方法,JSON字符串化时会首先调用该方法,然后用它的返回值进行序列化;

如果需要对含有非法JSON值的对象进行字符串化或对象中的某些值无法被序列化时,就需要定义toJSON方法来返回安全的JSON值。这个过程中,toJSON返回的是任意类型且合法适当的值,然后再由JSON.stringify(...)对其进行字符串化。

(2) 对于大多数简单值来说,JSON字符串化和toString()的效果基本相同,只不过通过JSON序列化出来的总是字符串。

"abc".toString(); // "abc"
JSON.stringify("abc"); // ""abc"",含有双引号的字符串  

(3) 可选参数 replacer、space

  • replacer: 数组或函数,用来制定对象序列化过程中该被处理和排除的属性

如果是数组,则必须是一个字符串数组,其中包含序列化操作要处理的对象的属性名称,除此之外其他的属性会被忽略

如果是函数,首先会对该对象本身调用一次,然后对对象中的每个属性各调用一次。每次传入两个参数,分别是key和value

``` var a = { b: 42, c: '42', d: [1, 2, 3]
};

JSON.stringify(a, ['b', 'c']); // "{"b": 42, "c": "42"}"

JSON.stringify(a, function(k, v) { if( k ! 'c') { return v; } }); // "{"b": 42, "d": [1, 2, 3]}"

```

  • space: 指定输出的缩进格式。正整数时是每一级缩进的字符数,字符串时最前面的字符被用于缩进。

2.2 ToNumber 非数字=>数字

true => 1;  
false => 0;  
undefined => NaN;  
null => 0;  

ToNumber对字符串的处理遵循数字常量的基本法则,处理失败时会返回NaN。

ToNumber对于以0开头的十六进制数字并不按照十六进制进行处理,而是按照十进制。

对象/数组会首先被转化为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则强制转换为数字。

值转换为基本类型值的过程:

``` 抽象操作ToPrimitive首先检查该值是否有valueOf()方法

如果有则返回基本类型值,并使用该值进行强制类型转换

如果没有则使用toString()方法的返回值来进行强制类型转换

如果valueOf()和toString()均不返回基本类型值,则会产生TypeError错误。 ```

var a = {  
    valueOf: function() {
        return "42";
    }
};

var b = {  
    toString: function() {
        return "42";
    }
};

var c = [4, 2];  
c.toString = function() {  
    return this.join(""); // "42"
};

Number(a); // 42  
Number(b); // 42  
Number(c); //42  
Number(""); // 0  
Number([]); // 0  
Number(["abc"]); // NaN  

2.3 ToBoolean

js中的0和1与false和true是不一样的,虽然可以相互通过强制类型转换使其相等,但并不是同一回事

  • js中的值可以分为两类:
  1. 被强制类型转为false的值

  2. 被强制类型转化为true的值

  • 假值列表:【js规范具体定义了一些(可列举)可以被强制类型转换为false的值】
  • undefined

  • null

  • false

  • +0, -0, NaN

  • "" //空字符串

可以理解为价值列表以外的值都是真值【规范没有明确定义】

  • 假值对象:封装了假值的对象,但是真值

3 显示强制类型转换

3.1 字符串和数字之间显示转换

Number(str);

String(num);

~ 位运算符(字位操作“非”)

~x = -(x+1);

3.2 显示解析数字字符串

var a = "42";  
var b = "42px";

Number(a); // 42  
parseInt(a); // 42

Number(b); // NaN  
parseInt(b); // 42  
  1. 解析允许字符串含有非数字字符,从左到右遇到非数字字符就停止

  2. 转换不允许出现非数字字符,否则会失败并返回NaN

  3. parseInt(...)针对的是字符串值,非字符串值会先被强制类型转换为字符串在进行解析

3.3 显示转换为布尔值

条件语句中的判断表达式或值 如果没有用Boolean()或!!,则会自动隐式进行ToBoolean()转换

使用Boolean()和!!来进行显示强制转换为布尔值

4. 隐式强制类型转换

4.1 || 和 &&:逻辑运算符【返回两者中中的一个且仅一个值】

  • ||: 条件判断为true则返回第一个操作数,反之返回第二个

  • &&: 与||相反,条件判断为false则返回第二个操作数,反之返回第一个。也叫作守护运算符: a && foo() => 只有当条件判断a通过时才会执行foo,否则a && foo()会被终止,foo()不会被调用

4.2 宽松相等与严格相等

宽松相等== 允许在相等比较中进行强制类型转换,而 严格相等=== 不允许

  • NaN 与 NaN 不相等

  • +0 与 -0 不相等

  • 对象的=====原理一样,即两个对象指向同一个值时就视为相等,并不发生强制类型转换

  • 宽松相等中如果两个值的类型不同,则会对其中一者或两者进行强制类型转换后再进行比较。

  • null udefined

  • 42 == [42]

    对象与费对象进行相等比较时,会先对非对象进行ToPromitive操作,然后根据valueOf()或toString返回一个简单值再进行比较

    对象[42]会先调用ToPromitive操作返回'42',然后再比较

4.3 抽象关系比较

x <= y =>被处理为 y > x然后将结果反转

五、语法

1. 语句和表达式

1.1 语句的结果值

语句都有一个结果值,可以通过浏览器控制台来获取结果,但是在代码中是无法获取这个结果值的。

以下代码就无法运行,因为语法不允许获得语句的结果并将其赋值给另一个变量。

var a, b;  
a = if(true) {  
    b = 4 + 38;
}

ES7规范中有一项“do表达式”提案,使用do{...}表达式执行一个代码块,并返回其中最后一个语句的结果值

var a, b;  
a = do {  
    if(true) {
        b = 4 + 38;
    }
}

1.2 表达式的副作用

var a = 42;  
var b = a++;  
console.log(a, b); // 43, 42

var a = 42;  
var b = ++a;  
console.log(a, b); // 43, 43

var a = 42;  
++a++; // ReferencrError,先执行a++返回42,然后执行++42报错,因为++无法直接在42这样的值上产生副作用。

var a, b; a = b = 42 => 正常,a、b的值都是42

var a = b = 42 => 严格模式下报错,因为b没有进行申明,非严格模式下创建一个全局变量

2. 运算符优先级

&& > || > ? :

&& || 左关联

a && b && c => (a && b) && c

a || b || c => (a || b) || c

? : 右关联

a ? b : c ? d : c => a ? b : (c ? d : c)

综合

a && b || c ? c || b ? a : c && b : a

((a && b) || c) ? ((c || b) ? a : (c && b)) : a

3. try...finally

finally中的代码会在try或catch之后执行,且无论在什么情况下一定会执行(可以看作个回调函数)。

function foo() {  
    try{
        return 42; // throw也是相同的效果
    } finally {
        console.log('Finally');
    }
    console.log('never runs');
}

console.log(foo());  
// Finally
// 42
  • return 42先执行,然后将foo()函数的返回值设置为42,try执行完毕后接着执行finally。

  • 如果finally中抛出异常,函数会在此处终止,如果此前try中已经有return设置了返回值,则该值会被丢弃,抛出异常。【continue和break语句也是如此】

  • finally中的return会覆盖掉try和catch中return返回的值

米奇

继续阅读此作者的更多文章