1. 奇特的 ~ 运算符

它首先将值强制类型转换为 32 位数字,然后执行字位操作“非”(对每一个字 位进行反转)。这与 ! 很相像,不仅将值强制类型转换为布尔值 <,还对其做字位反转。

~x 大致等同于 -(x+1)。很奇怪,但相对更容易说明问题: ~42; // -(42+1) ==> -43

var a = "Hello World";

if (~a.indexOf( "lo" )) { 
  // 找到匹配!
}

如果 indexOf(..) 返回 -1,~ 将其转换为假值 0,其他情况一律转换为真值。

与indexOf实用中,~ 比>= 0和== -1更简洁。


2. 显式解析数字字符串

解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换两者之间还是有明显的差别。

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

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

Number( b );    // NaN
parseInt( b );  // 42

解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停 止。而转换不允许出现非数字字符,否则会失败并返回 NaN。

parseInt(..) 先将参数强制类型转换为字符串再进行解析。


3. valueOf()和toString()

当JavaScript需要将一个对象转换为原始值时(例如在进行数学运算时),会自动调用valueOf()方法。如果对象没有定义valueOf()方法,或者valueOf()方法返回的不是原始值,JavaScript会继续调用toString()方法。

  • 情况一:

var a = {
  valueOf: function () {
    return 42;
  },
  toString: function () {
    return 4;
  }
};
var b = a + '2'; // '442' 隐式转换没有在执行a.toString()。
  • 情况二:

var a = {
  valueOf: function () {
    return {};
  },
  toString: function () {
    return 4;
  }
};
var b = a + '2'; // '42', a.valueOf执行得到{},所以又继续执行toString得到4

4. {} + [] 和 [] + {} 结果为什么不一样

  • 对于 {} + [],{} 被解释为一个空的代码块,而 + [] 被解释为对空数组进行一元加法操作。所以结果是 0。

  • 对于 [] + {}, “[object Object]”


5. Promise 链式流

如果这个 Promise 链中的某个步骤出错了怎么办?错误和异常是基于每个 Promise 的,这 意味着可能在链的任意位置捕捉到这样的错误,而这个捕捉动作在某种程度上就相当于在 这一位置将整条链“重置”回了正常运作

function delay(time) {
   return new Promise(function (resolve, reject) {
     setTimeout(resolve, time);
   });
 }
 delay(100) // 步骤1 
   .then(function STEP2() {
     console.log("step 2 (after 100ms)");
     return delay(2000);
   })
   .then(function STEP3() {
     foo.bar();
     console.log("step 3 (after another 200ms)"); // 不执行因为foo.bar()报错了。
   })
   .then(function STEP4() {
     // STEP3捕获到错误,所以这里永远不会执行到
   },(e) => {
     console.log(e);
     return delay(200);
   })
   .then(function STEP5(v) {
     console.log(v, "step 5 (after another 50ms)"); // 执行,STEP4捕捉到这个错误,返回给了STEP5
   })

6. try..catch

try..catch 它只能是同步的,无法用于异步代码模式

function foo() {
  setTimeout(function () {
    baz.bar();
  }, 100);
}
try {
  foo();
  // 后面从 `baz.bar()` 抛出全局错误 
} catch (err) {
  console.log('err')
  // 永远不会到达这里
}

7. 迭代消息传递

除了能够接受参数并提供返回值之外,生成器甚至提供了更强大更引人注目的内建消息输入输出能力,通过 yield 和 next(..) 实现。

function* foo(x) {
  var y = x * (yield "Hello");
  return y;
}
var it = foo(6);
var res = it.next(); // 第一个next(),并不传入任何东西
console.log(res.value); // Hello
res = it.next(7); // 向等待的yield传入7
console.log(res.value); // 42

yield 和 next(..) 调用有一个不匹配。面的代码片段有一个 yield 和两个 next(..) 调用。为什么会有这样的不匹配呢?

因为第一个 next(..) 总是启动一个生成器,并运行到第一个 yield 处。不过,是第二个 next(..) 调用完成第一个被暂停的 yield 表达式,第三个 next(..) 调用完成第二个 yield, 以此类推。

启动 生成器时一定要用不带参数的 next()。

第一个 next() 调用(没有参数的)基本上就是在提出一个问题:“生成器 *foo(..) 要给我 的下一个值是什么”。谁来回答这个问题呢?第一个 yield “hello” 表达式。

但是,稍等!与 yield 语句的数量相比,还是多出了一个额外的 next()。所以,最后一个 it.next(7) 调用再次提出了这样的问题:生成器将要产生的下一个值是什么。但是,再没 有 yield 语句来回答这个问题了,是不是?那么谁来回答呢?

return 语句回答这个问题!

如果你的生成器中没有 return 的话——在生成器中和在普通函数中一样,return 当然不 是必需的——总有一个假定的/隐式的return;(也就是return undefined;),它会在默认 情况下回答最后的 it.next(7) 调用提出的问题。


8. 多个迭代器

同一个生成器的多个实例可以同时运行,它们甚至可以彼此交互

function* foo() {
  var x = yield 2;
  z++;
  var y = yield(x * z);
  console.log(x, y, z);
}
var z = 1;
var it1 = foo();
var it2 = foo();
var val1 = it1.next().value; // 2 <-- yield 2
var val2 = it2.next().value; // 2 <-- yield 2
val1 = it1.next(val2 * 10).value; // 40   <-- x:20,  z:2
val2 = it2.next(val1 * 5).value; // 600  <-- x:200, z:3
it1.next(val2 / 2); // y:300 // 20 300 3
it2.next(val1 / 4); // y:10 // 200 10 3

我们简单梳理一下执行流程:

  1. *foo() 的两个实例同时启动,两个 next() 分别从 yield 2 语句得到值 2。

  2. val2 10也就是2 10,发送到第一个生成器实例it1,因此x得到值20。z从1增加到 2,然后 20 * 2 通过 yield 发出,将 val1 设置为 40。

  3. val1 5 也就是 40 5,发送到第二个生成器实例 it2,因此 x 得到值 200。z 再次从 2递增到 3,然后 200 * 3 通过 yield 发出,将 val2 设置为 600。

  4. val2 / 2 也就是 600 / 2,发送到第一个生成器实例 it1,因此 y 得到值 300,然后打印出x y z的值分别是20 300 3。

  5. val1 / 4 也就是 40 / 4,发送到第二个生成器实例 it2,因此 y 得到值 10,然后打印出x y z的值分别为200 10 3。


9. 如何使用生成器来生成值

  • 直接使用函数闭包的版本

var gimmeSomething = (function () {
  var nextVal;
  return function () {
    if (nextVal === undefined) {
      nextVal = 1;
    } else {
      nextVal = (3 * nextVal) + 6;
    }
    return nextVal;
  };
})();
gimmeSomething();  // 1
gimmeSomething();  // 9
gimmeSomething();  // 33
gimmeSomething();  // 105
  • 标准的迭代器接口版本

var generate = 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
      };
    }
  }
}

var g = generate();  // g对象叫作迭代器,因为它的接口中有一个 next() 方法

for (var v of g) {
  console.log(v);
  // 不要死循环!
   if (v > 500) {
    break;
  }
}
for..of 循环在每次迭代中自动调用 next(),它不会向 next() 传入任何值,并且会在接收 到 done:true 之后自动停止。

[Symbol.iterator]: function(){ return this; }将 g 构建成为一 个 iterable(可迭代)。现在它既是 iterable,也是迭代器。

for..of 循环期望 g 是 iterable,于是它寻找并调用它的 Symbol.iterator 函数。我们将这个函数定义为就是简单的 return this,也就是把自身返回,而 for..of 循环并不知情。

从 ES6 开始,从一个 iterable 中提取迭代器的方法是:iterable 必须支持一个函数,其名称是专门的 ES6 符号值 Symbol.iterator。调用这个函数时,它会返回一个迭代器。通常每 次调用会返回一个全新的迭代器,虽然这一点并不是必须的。


10. 生成器迭代器

了解了迭代器的背景,让我们把注意力转回生成器上。可以把生成器看作一个值的生产者,我们通过迭代器接口的 next() 调用一次提取出一个值。
所以,严格说来,生成器本身并不是 iterable,尽管非常类似——当你执行一个生成器,就得到了一个迭代器

function *foo(){ .. }
var it = foo();

可以通过生成器实现前面的这个 something 无限数字序列生产者,类似这样:

function *something() {
  var nextVal;
  while (true) {
    if (nextVal === undefined) {
      nextVal = 1;
    } else {
      nextVal = (3 * nextVal) + 6;
    }
    yield nextVal;
  }
}
var s = something();
g.next().value;

通常在实际的 JavaScript 程序中使用 while..true 循环是非常糟糕的主意,至少如果其中没有 break 或 return 的话是这样,因为它有可能会同步地无限循环,并阻塞和锁住浏览器 UI。但是,如果在生成器中有 yield 的话,使用这 样的循环就完全没有问题。因为生成器会在每次迭代中暂停,通过 yield 返 回到主程序或事件循环队列中。简单地说就是:“生成器把 while..true 带回 了 JavaScript 编程的世界!” for(;;){} 也可以实现无限循环

这段代码不仅更简洁,我们不需要构造自己的迭代器接口,实际上也更合理,因为它更清 晰地表达了意图。比如,while..true 循环告诉我们这个生成器就是要永远运行:只要我 们一直索要,它就会一直生成值。

现在,可以通过 for..of 循环使用我们雕琢过的新的 *something() 生成器。

你可以看到, 其工作方式基本是相同的:

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

但是,不要忽略了这段 for (var v of something()) .. !我们并不是像前面的例子那样把 something 当作一个值来引用,而是调用了 *something() 生成器以得到它的迭代器供 for.. of 循环使用。

如果认真思考的话,你也许会从这段生成器与循环的交互中提出两个问题:

  1. 为什么不能用 for (var v of something) .. ?因为这里的 something 是生成器,并不是 iterable。我们需要调用something() 来构造一个生产者供 for..of 循环迭代。

  2. something() 调用产生一个迭代器,但 for..of 循环需要的是一个 iterable,对吧?是 的。生成器的迭代器也有一个Symbol.iterator 函数,基本上这个函数做的就是 return this,和我们前面定义的 iterable something 一样。换句话说,生成器的代器也是一个 iterable !


11. 停止生成器

在前面的例子中,看起来似乎 *something() 生成器的迭代器实例在循环中的 break 调用之 后就永远留在了挂起状态。

其实有一个隐藏的特性会帮助你管理此事。for..of 循环的“异常结束”(也就是“提前终 止”),通常由 break、return 或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。

尽管 for..of 循环会自动发送这个信号,但你可能会希望向一个迭代器手工发送这个信号。 可以通过调用 return(..) 实现这一点。

如果在生成器内有 try..finally 语句,它将总是运行,即使生成器已经外部结束。如果需 要清理资源的话(数据库连接等),这一点非常有用:

function* something() {
    try {
      var nextVal;
      while (true) {
        if (nextVal === undefined) {
          nextVal = 1;
        } else {
          nextVal = (3 * nextVal) + 6;
        }
        yield nextVal;
      }
    }
    // 清理子句 
    finally {
      console.log("cleaning up!");
    }
  }

之前的例子中,for..of 循环内的 break 会触发 finally 语句。但是,也可以在外部通过 return(..) 手工终止生成器的迭代器实例:

var it = something();
for (var v of it) {
  console.log(v);
  if (v > 500) {
    console.log(
      // 完成生成器的迭代器
      it.return("Hello World").value
    );
  }
}

调用 it.return(..) 之后,它会立即终止生成器,这当然会运行 finally 语句。另外,它 还会把返回的value设置为传入return(..)的内容,这也就是”Hello World”被传出 去的过程。现在我们也不需要包含 break 语句了,因为生成器的迭代器已经被设置为 done:true,所以 for..of 循环会在下一个迭代终止。


12. 异步迭代生成器

// 模拟异步ajax请求
function delay(time = 1000) {
  setTimeout(() => {
    it.next(1);
  }, time);
}


function *mainFun() {
  var data = yield delay();
  console.log(data);  // 1
}

var it = mainFun();

// 启动
it.next();

生成器中使用的 yield,实际上并不会阻塞整个程 序,它只是暂停或阻塞了生成器本身的代码。

在yield delay()中,首先调用delay(),它没有返回值(即返回undefined),所以 我们发出了一个调用来请求数据,但实际上之后做的是yield undefined。这没问题,因为这段代码当前并不依赖 yield 出来的值来做任何事情。

这里并不是在消息传递的意义上使用 yield,而只是将其用于流程控制实现暂停 / 阻塞。实 际上,它还是会有消息传递,但只是生成器恢复运行之后的单向消息传递。

所以,生成器在 yield 处暂停,本质上是在提出一个问题:“我应该返回什么值来赋给变量 text ?”谁来回答这个问题呢?

看一下 delay(..)。如果这个 Ajax 请求成功,我们调用: it.next( data );

这会用响应数据恢复生成器,意味着我们暂停的 yield 表达式直接接收到了这个值。然后 随着生成器代码继续运行,这个值被赋给局部变量 text。


13. 支持 Promise 的 Generator Runner

function run(gen) {
  var args = [].slice.call(arguments, 1);
  // 在当前上下文中初始化生成器
  var it = gen.apply(this, args);
  return Promise.resolve().then(function handleNext(val) {
    // 对下一个yield出的值运行
    var next = it.next(val);

    return (function handleResult(next) {
      // 生成器运行完毕了吗?
      if (next.done) {
        return next.value;
      // 否则继续运行
      } else {
        return Promise.resolve(next.value).then(
          // 成功就恢复异步循环,把决议的值发回生成器
          handleNext, 
          // 如果value是被拒绝的 promise, 就把错误传回生成器进行出错处理
          function handleError(err) {
            return Promise.resolve(it.throw(err)).then(handleResult);
        });
      }
    })(next);
  });
}


function mockRequest(data) { // 模拟异步请求
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve(data)
    }, 1500)
  })
}

function *main(a, b) {
  const r1 = yield mockRequest('r1')
  console.log(r1)
  const r2 = yield mockRequest('r2')
  console.log(r2)
  const r3 = yield mockRequest(r1 + r2)
  console.log(r3)
}

run(main)

效果如同async/await语法糖,是不是有种看完了以为是很有用的功能,但是又被骗的感觉。


14. 生成器中的 Promise 并发

function* main(a, b) {
  const p1 =  mockRequest('r1', 2000);
  const p2 =  mockRequest('r2', 1000);
  const r1 = yield p1;
  console.log(r1)
  const r2 = yield p2;
  console.log(r2)
  const r3 = yield mockRequest(r1 + r2, 1000)
  console.log(r3)
}

p1 和 p2 是并发执行。 1s后不会打印出r2,需要等待r1执行完成,2s后先打印r1在打印r2后面再等待1s后执行打印r3。

然后我们使用接下来的两个 yield 语句等待并取得 promise 的决议(分别写入 r1 和 r2)。 如果p1先决议,那么yield p1就会先恢复执行,然后等待yield p2恢复。如果p2先决 议,它就会耐心保持其决议值等待请求,但是 yield p1 将会先等待,直到 p1 决议。

不管哪种情况,p1 和 p2 都会并发执行,无论完成顺序如何,两者都要全部完成,然后才 会发出 r3 = yield request..Ajax 请求。


15. 生成器委托

function* foo() {
  console.log("*foo() starting");
  yield 3;
  yield 4;
  console.log("*foo() finished");
}

function* bar() {
  yield 1;
  yield 2;
  yield* foo();
  yield 5;
}
var it = bar();

这里的 yield *foo() 委托是如何工作的呢? 首先,和我们以前看到的完全一样,调用foo()创建一个迭代器。

然后yield 把迭代器实例控制(当前 bar() 生成器的)委托给 / 转移到了这另一个 *foo() 迭代器。

所以,前面两个 it.next() 调用控制的是 bar()。但当我们发出第三个 it.next() 调用时, foo() 现在启动了,我们现在控制的是 foo() 而不是 bar()。这也是为什么这被称为委托: bar() 把自己的迭代控制委托给了 foo()。

一旦 it 迭代器控制消耗了整个 foo() 迭代器,it 就会自动转回控制 bar()。

以上内容选摘自 `你不知道的JavaScript` ,内容部分有部分做删改,根据博主自己的理解进行了一些文案上的调整,基本意思不变,如果有内容误导,可通过邮箱通知博主加以改正,谢谢合作