你不知道的JavaScript(中卷)读书笔记

903 阅读8分钟

第一部分 类型与语法

第2章 值


2.4 特殊数值

2.4.1 不是值的值

  • undefinednull类型只有一个值,名称既是类型也是值
  • undefined === 从未赋值、没有值
  • null === 曾赋过值,但目前没有值,空值

2.4.3 特殊的数字

1. 不是数字的数字

  • NaN:指无效的数字,唯一一个非自反的值,NaN !== NaN为true
  • 内建的全局工具函数 isNaN(..) 有一个严重的缺陷,它检查参数是否不是 NaN,也不是数字
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; "foo"
window.isNaN( a ); // true 
window.isNaN( b ); // true——晕!

从 ES6 开始我们可以使用工具函数 Number.isNaN(..)。 ES6 之前的浏览器的 polyfill 如下:

if (!Number.isNaN) {
    Number.isNaN = function(n) {
        return (
         typeof n === "number" &&
         window.isNaN( n )
    ); };
}

Number.isNaN( a ); // true 
Number.isNaN( b ); // false——好!

2. 无穷数

  • Infinity === Number.POSITIVE_INfiNITY

3. 零值

  • -0 除了可以用作常量以外,也可以是某些数学运算的返回值
var a = 0 / -3; // -0
var b = 0 * -3; // -0
  • 加法和减法运算不会得到负零
  • -0字符串化会返回'0',将字符串'-0'数字化则得到-0
  • JSON.stringify(-0) === JSON.stringify(-0)
JSON.stringify(-0)  === JSON.stringify(-0)  // '0' === '0'
  • JSON.parse('-0') === JSON.parse('0')
JSON.parse('-0') === JSON.parse('0')    // -0 === 0

第4章 强制类型转换


4.5 宽松相等和严格相等

4.5.2 抽象相等

非常规情况

  • NaN不等于NaN
  • +0不等于-0

1.字符串与数字之间的相待比较

(1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。

(2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

const a = 42
const b = '42'
a === b // false
a == b // true

2.其他类型和布尔类型之间的相等比较

(1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;

(2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

const x = 'true'
const y = '42'
x == y // false
const x = '42'
const y = 'false'
x == y // false

3.null和undefined之间的相等比较

(1) 如果 x 为 null,y 为 undefined,则结果为 true。

(2) 如果 x 为 undefined,y 为 null,则结果为 true。

const a = null
let b
    
a == b  // true
b == null   // true

4. 对象和非对象之间的相等比较

(1) 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;

(2) 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString()的返回值(如果存在)来进行强制类型转换。 如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

const a = 42;
const b = [ 42 ];
a == b; // true

4.5.3 比较少见的情况

1. 返回其他数字

Number.prototype.valueOf = function() {
    return 3;
};
new Number( 2 ) == 3;   // true

2. 假值的相等比较

非常规

"0" == false;   // true
false == 0;     // true
false == "";    // true
false == [];    // true
false == {};    // true
"" == 0;    // true    
"" == [];   // true
"" == {};   // true
0 == [];    // true

3. 极端情况

[] == ![];  // true
2 == [2];   // true
"" == [null];   // true

4.6 抽象关系比较

“抽象关系比较”(abstract relational comparison),分为两个部 分:比较双方都是字符串(后半部分)和其他情况(前半部分)。

  • 比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强 制类型转换为数字来进行比较。
var a = [ 42 ];
var b = [ "43" ];

a < b;  // true
b < a;  // false
  • 如果比较双方都是字符串,则按字母顺序来进行比较:
var a = [ "42" ];
var b = [ "043" ];

a < b; // false

第5章 语法


5.1 语句和表达式

5.1.3 上下文规则

1.大括号

(1) 对象常量

(2) 标签

{
    foo: bar()
}

// 标签为foo的循环
foo: for (var i=0; i<4; i++) {
    for (var j=0; j<4; j++) {
        // 如果j和i相等,继续外层循环
        if (j == i) {
            // 跳转到foo的下一个循环
            continue foo;
        }
        // 跳过奇数结果
        if ((j * i) % 2 == 1) {
            // 继续内层循环(没有标签的)
            continue;
        }
        console.log( i, j );
    }
}
// 1 0
// 2 0
// 2 1
// 3 0
// 3 2

2.代码块

[] + {}; // "[object Object]"
{} + []; // 0

第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理。第 4 章讲过 [] 会被强制类型转换为 "",而 {} 会被强制类型转换为 "[object Object]"。

但在第二行代码中,{} 被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需 要分号,所以这里不存在语法上的问题。最后 + [] 将 [] 显式强制类型转换(参见第 4 章) 为 0。

3. 对象解构

const obj = { a: 42, b: "foo" };
const { a, b } = obj;
console.log(a, b);  // 42, "foo"

4. else if 和可选代码块

if (a) { 
    // ..
}
else if (b) {
    // .. 
}
else {
    // ..
}

事实上 JavaScript 没有 else if,但 if 和 else 只包含单条语句的时候可以省略代码块的 { }。

所以上面的 else if 其实是这样的:

if (a) { 
    // ..
}
else {
    if (b) {
        // ..
    }
    else {
        // .. 
    }
}

第二部分 异步和性能

第3章 Promise


3.4 链式流

术语:决议、完成以及拒绝

术语决议(resolve)、完成(fulfill)和拒绝(reject),构造器 Promise(..):

var p = new Promise( function(X,Y){ 
    // X()用于完成
    // Y()用于拒绝 
} );

3.8 Promise局限性

3.8.1 顺序错误处理

如果构建了一个没有错误处理函数的 Promise 链,链中任何地方的任何错误都会在链中一 直传播下去,直到被查看(通过在某个步骤注册拒绝处理函数)。

 var p = foo( 42 )
 .then( STEP2 )
 .then( STEP3 );

可以在 p 上注册一个拒绝错误处理函数,对于链中任何位置出现的任何错误,这个处理函数都会 得到通知:

 p.catch( handleErrors );

3.8.2 单一值

Promise 只能有一个完成值或一个拒绝理由。在简单的例子中,这不是什么问 题,但是在更复杂的场景中,你可能就会发现这是一种局限了。

3.8.3 单决议

Promise 最本质的一个特征是:Promise 只能被决议一次(完成或拒绝)。

3.8.4 惯性

Promise 提供了一种不同的范式,因此,编码方式的改变程度从某处的个别差异到某种情 况下的截然不同都有可能。

3.8.5 无法取消的 Promise

一旦创建了一个 Promise 并为其注册了完成和 / 或拒绝处理函数,如果出现某种情况使得 这个任务悬而未决的话,你也没有办法从外部停止它的进程。

3.8.6 Promise 性能 这个特定的局限性既简单又复杂。

把基本的基于回调的异步任务链与 Promise 链中需要移动的部分数量进行比较。很显然, Promise 进行的动作要多一些,这自然意味着它也会稍慢一些。

第4章 生成器


4.1 打破完整运行

多数 JavaScript 文档和代码中的生成器声明格式都是 function* foo() { .. },而不是我这里使用的 function foo() { .. }: 唯一区别是 * 位置的风格不同。这两种形式在功能和语法上都是等同的,还 有一种是functionfoo(){ .. }(没有空格)也一样。

4.2 生成器产生值

4.2.1 生产者与迭代器

实现一个直接使用函数闭包的版本(参见本系列的《你不知道的 JavaScript(上卷)》 的“作用域和闭包”部分),类似如下:

var gimmeSomething = (function(){
    var nextVal;
    return function(){
        if (nextVal === undefined) {
            nextVal = 1; 
             
        }
        else {
            nextVal = (3 * nextVal) +6;
        }
        return nextVal;
    };
})();
gimmeSomething();
gimmeSomething();
gimmeSomething();
gimmeSomething();
// 1
// 9
// 33
// 105

迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。JavaScript 迭代器的接口,与多数语言类似,就是每次想要从生产者得到下一个值的时候调用 next()。 可以为我们的数字序列生成器实现标准的迭代器接口:

var something = (function(){
    var nextVal;
    return {
        // for..of循环需要
        [Symbol.iterator]: function(){ return this; },
        // 标准迭代器接口方法 
        next: function(){
            if (nextVal === undefined) {
                nextVal = 1;
            }
            else {
                nextVal = (3 * nextVal) + 6;
            }
            return { done:false, value:nextVal };
        }
    };
})();
something.next().value;
something.next().value;
something.next().value;
something.next().value;
// 1
// 9
// 33
// 105

ES6 还新增了一个 for..of 循环,这意味着可以通过原生循环语法自动迭代标准迭代器:

for (var v of something) {
    console.log( v );
    // 不要死循环! 
    if (v > 500) {
        break; 
    }
}
// 1 9 33 105 321 969

手工在迭代器上循环,调用 next() 并检查 done:true 条件来确定何时停止循环:

for (
    var ret;
    (ret = something.next()) && !ret.done; 
){
    console.log( ret.value );
    // 不要死循环!
    if (ret.value > 500) {
        break; 
    }
}
// 1 9 33 105 321 969

4.3 异步迭代生成器

function *main(){
    yield setTimeout(() => console.log(1000), 3000)
    console.log('在1000之后')
    yield 'vincent'
}
var it = main()
it.next()   // {value: 829, done: false}
            // 3秒后打印出1000
it.next()
            //在1000之后
            // {value: "vincent", done: false}

4.4 生成器 +Promise

function *main(){
    var dd = yield Promise.resolve(100)
    console.log('dd', dd)
    console.log('在100之后')
    yield 'vincent'
}

it = main() // main {<suspended>}
p = it.next().value // Promise {<resolved>: 100}

p.then(d => console.log(it.next(d)))
// dd 100
// 在100之后
// {value: "vincent", done: false}