ES6读书笔记(三)

前言

前段时间整理了ES6的读书笔记:《ES6读书笔记(一)》《ES6读书笔记(二)》,现在为第三篇,本篇内容包括:

  • 一、Promise
  • 二、Iterator和for of循环
  • 三、Generator
  • 四、async

本文笔记也主要是根据阮一峰老师的《ECMAScript 6 入门》和平时的理解进行整理的,希望对你有所帮助,喜欢的就点个赞吧!

一、Promise

1. 执行顺序
let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved
复制代码
2.异步加载图片:
function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}
复制代码
3.用Promise对象实现Ajax:
const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出错了', error);
});
复制代码
4.then方法返回的是一个新的Promise实例(不是原来那个Promise实例)
5.如果 Promise 状态已经变成resolved,再抛出错误是无效的:
const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok
复制代码

上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了

6.跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应,通俗的说法就是“Promise 会吃掉错误”:
const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    resolve(x + 2);   // 会报错,因为x没有声明
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);  // 虽然以上有错误,但没有阻塞后面的代码
// Uncaught (in promise) ReferenceError: x is not defined
// 123
复制代码
7.
const promise = new Promise(function (resolve, reject) {
  resolve('ok');
  setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test
复制代码

上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误,相当于是js引擎去执行了这个回调,而不是在promise内部执行。

一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法:

  • ①如果有错误,但没有去catch,则会阻塞promise内部的代码,但不会阻塞外部的代码;
  • ②如果有catch,但是没有错误,则会跳过catch,继续执行后面的代码;
  • ③如果有catch,然后被catch捕获了错误,那依旧可以继续执行后面的代码;
  • ④如果有catch,catch捕获到了前面的错误,但catch内部又有错误的话,则会阻塞后面的代码,除非后面再链式调用catch捕获该错误。

以上总结就是只要promise内部有错误没有被捕获,就会阻塞内部代码,但不会阻塞外部代码。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
//--------------------------------------------
const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
    y + 2;  // y 没有声明会报错,且这个错误未被捕获,会阻塞后面的代码
}).then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
复制代码
8.Promise.prototype.finally()
  • ①finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作:
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
复制代码

上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

  • ②finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

  • ③finally本质上是then方法的特例:

promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);
复制代码
  • ④finally方法总是会返回原来的值:
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})

// resolve 的值是 2
Promise.resolve(2).finally(() => {})

// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})

// reject 的值是 3
Promise.reject(3).finally(() => {})
复制代码
9.Promise.all(数组或具有 Iterator 接口,且返回的每个成员都是 Promise 实例)
  • ①如果参数全为fulfilled,则返回对应的数组结果(是等全部得到结果了再一起返回),但如果有一个是rejected,则返回第一个rejected的返回值,状态就为rejected。

  • ②catch后会变为resolved:

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
// reject(“world”);
})
.then(result => result)
.catch(e => e);        // catch后会变为resolved

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));   // 传入的p2有自己的catch,所以不会触发这里的catch,所以没有捕获到错误,所以就相当于都是执行正确的,所以会有结果
// ["hello", Error: 报错了]
复制代码
10.Promise.race

参数中谁率先改变了状态,就返回谁的状态,这意味着只返回一个结果

11.Promise.resolve()
  • ①Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。 所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve方法:
const p = Promise.resolve();

p.then(function () {
  // ...
});
复制代码
  • ②立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three
复制代码
12.Promise.reject()

Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致:

const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};
Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true
复制代码

上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

13.如果对于一个函数,不管是同步或异步,都想使用then方法指定下一流程,可使用以下方式,让它是同步时就按同步执行,是异步时就按异步执行:

不要直接使用promise.resolve(),因为如果是同步函数,会在本轮事件循环末尾才会执行:

const f = () => console.log('now');
Promise.resolve().then(f);  // then才是微任务,resolve时还是同步的
console.log('next');
// next
// now
复制代码
  • ①使用async:
const f = () => console.log('now');
(async () => f())()
.then(...)
.catch(...);

console.log('next');
// now
// next
复制代码
  • ②使用new Promise():
const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next
复制代码
  • ③一个提案,提供Promise.try方法替代上面的写法:浏览器目前会报错
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
复制代码

二、Iterator和for of循环

1. Iterator(遍历器)的概念

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

Iterator 的遍历过程是这样的:

  • (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  • (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  • (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  • (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

一个模拟next方法返回值的例子:

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}
复制代码
2.ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。
const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};
复制代码
3.原生具备 Iterator 接口的数据结构如下:不含对象
  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象
4.数组的Symbol.iterator属性:
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
复制代码
5.对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。
6.有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法):
  • ①解构赋值
  • ②扩展运算符:这样就可对有Iterator接口的数据结构使用扩展运算符转为数组,而对于没有Iterator接口的类数组,可采用Array.from转为数组,这样就具有了Iterator接口
  • ③yield*

yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口:

let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

var iterator = generator();

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
复制代码
  • ④其他场合 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口:

  • for...of

  • Array.from()

  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))

  • Promise.all()

  • Promise.race()

7. Iterator接口与Generator函数

Symbol.iterator方法的最简单实现:

let myIterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
    yield 3;
  }
}
[...myIterable] // [1, 2, 3]

// 或者采用下面的简洁写法

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// "hello"
// "world"
复制代码
8.for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。
let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo" 这也说明了for in遍历了自身及原型上的可枚举属性
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}
复制代码
9. Map遍历得到的是数组,Set遍历得到的是单个值:
let map = new Map().set('a', 1).set('b', 2);
Map;   // {"a" => 1, "b" => 2}
for (let pair of map) {
  console.log(pair);
}
// ['a', 1]
// ['b', 2]

for (let [key, value] of map) {
  console.log(key + ' : ' + value);
}
// a : 1
// b : 2
复制代码
10. 可用Array.from将不具有iterator接口的类数组对象转为数组,这样也就具有了iterator接口:
let arrayLike = { length: 2, 0: 'a', 1: 'b' };

// 报错
for (let x of arrayLike) {
  console.log(x);
}

// 正确
for (let x of Array.from(arrayLike)) {
  console.log(x);
}
复制代码
11.循环对比:
  • for in 会遍历原型可枚举属性,为遍历对象而生,尽管对象没有iterator接口
  • forEach不能中途跳出循环
  • for of 可中途跳出循环,不会遍历原型可枚举属性,针对数组

三、Generator

1. 执行Generator(生成器)返回一个遍历器对象,这个遍历器对象可以依次遍历Generator函数内部的每一个状态,yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”):
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next()  // 遇到第一个yield,暂停,然后返回yield后面的表达式的值
// { value: 'hello', done: false }

hw.next()  // 从上次暂停的地方往下执行,遇到第二个yield后暂停,返回值
// { value: 'world', done: false }

hw.next()  // 从上次暂停的地方往下执行,发现没有yield了,所以一直往下执行,直到遇到// return,如果没有return则返回undefined
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }
复制代码

定义方式:

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }   // 推荐这种写法
function*foo(x, y) { ··· }
复制代码
2. 遍历器对象的next方法的运行逻辑如下:
  • (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

  • (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

  • (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

  • (4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

3. Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);
复制代码
4. yield表达式如果用在另一个表达式之中,必须放在圆括号里面:
function* demo() {
  console.log('Hello' + yield);     // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield));     // OK
  console.log('Hello' + (yield 123)); // OK
}
复制代码

yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号:

function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}
复制代码
5.与 Iterator 接口的关系:

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口:

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]
复制代码

上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。

Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身:

function* gen(){
  // some code
}

var g = gen();

g[Symbol.iterator]() === g
// true
复制代码
6.yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;  // yield i表达式是没有返回值的,或者说返回undefined
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false } 相当于给reset赋值为true,重置了i的值
复制代码

next参数的值是传给上一个yield表达式的返回值,所以这也意味着第一个next的参数是无效的,所以不需要传,即第一个next是用于启动遍历器对象:

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}  yield (x + 1)的返回值是undefined,所以乘2再除3得到的是NaN
a.next() // Object{value:NaN, done:true}  5 + NaN + undefined为NaN

var b = foo(5);
b.next() // { value:6, done:false }  5+1得到6
b.next(12) // { value:8, done:false }  12赋给yield (x + 1),然后乘2除3得到8,即(y / 3)的值
b.next(13) // { value:42, done:true }  同理

//-----------------------------------------
function* dataConsumer() {
  console.log('Started');
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return 'result';
}

let genObj = dataConsumer();
genObj.next();
// Started
// {value: undefined, done: false}
genObj.next('a')
// 1. a
// {value: undefined, done: false}
genObj.next('b')
// 2. b
// {value: "result", done: true}
复制代码
7. for of 循环:

for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法:

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5 没有6,因为一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象
复制代码
8. Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获:

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};

var i = g();
i.next();  // 要捕获错误,必须先执行一次next来启动遍历器对象

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
复制代码
9. throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法:
var gen = function* gen(){
  try {
    yield console.log('a');
  } catch (e) {
    // ...
  }
  yield console.log('b');
  yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c
复制代码
10. Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数:

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }
复制代码

如果return方法调用时,不提供参数,则返回值的value属性为undefined:

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return() // { value: undefined, done: true }
复制代码

如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会推迟到finally代码块执行完再执行。

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
复制代码

上面代码中,调用return方法后,就开始执行finally代码块,然后等到finally代码块执行完,再执行return方法。

11. 在一个 Generator 函数里面执行另一个 Generator 函数:
function* bar() {
  yield 'x';
  yield* foo();  // 加了*号就是返回遍历器内部值,相当于调用了*后面变量的iterator接口,否则返回遍历器对象
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"
复制代码
3.1 异步应用

1.异步简单说就是不连续的执行任务,类似一个协程的过程:

function* asyncJob() {
  // ...其他代码
  var f = yield readFile(fileA);
  // ...其他代码
}
复制代码

其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线,协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。

2.Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

3.传值调用:先计算参数值再传入函数体内使用。 传名调用:直接将参数表达式传入函数体内,使用到时再进行求值。

4.Generator的异步应用中何时调用第一步,何时调用第二步,此时就需要使用thunk函数,相当于“传名调用”,编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫做 Thunk 函数:

function f(m) {
  return m * 2;
}

f(x + 5);

// 等同于

var thunk = function () {
  return x + 5;
};

function f(thunk) {
  return thunk() * 2;
}
复制代码

5.JavaScript 语言的 Thunk 函数

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数,类似柯里化:

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);
复制代码

四、async

1. async 函数其实就是 Generator 函数的语法糖,可以再等待第一阶段得到结果后自动执行第二阶段,而不是像Generator那样手动执行。*换成了async,yield换成了await。
2. async函数对 Generator 函数的改进,体现在以下加点:
  • ①内置执行器
  • ②更语义化
  • ③适应性
  • ④返回promise
3. async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数:

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"
复制代码

如果没有return,则then方法回调函数的参数则得到的是undefined

async function f() {
  await Promise.resolve('hello world'); // 不会执行
}
f().then(a=>{console.log(a)})   // undefined
复制代码
4. Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数:

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
复制代码

上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log

5. 一般await后面是接promise对象,返回该对象的结果,如果不是promise对象,则直接返回对应的值:
async function f() {
  // 等同于
  // return 123;
  return await 123;  // return要放await前面,否则会报错
}

f().then(v => console.log(v))
// 123
复制代码
6. await后面的promise状态如果为reject,则会被catch到:
async function f() {
  await Promise.reject('出错了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
复制代码

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}
复制代码

为了防止有错误或reject中断代码的执行,则需要使用catch来处理,或者使用try catch:

async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hello world
复制代码

如果有多个await命令,可以统一放在try...catch结构中:

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}
复制代码
7. 使用async注意点:
  • ①catch错误,防止代码中断
  • ②对于不存在继发关系的异步操作,应该让它们同步进行,而不是顺序执行:
let foo = await getFoo();
let bar = await getBar();

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
复制代码
  • ③await命令只能用在async函数之中,如果用在普通函数,就会报错,如用在forEach中会报错,因为是并发执行,应该使用for循环:
unction dbFuc(db) { //这里不需要 async
  let docs = [{}, {}, {}];

  // 可能得到错误结果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}
复制代码

上面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}
复制代码
  • ④async 函数可以保留运行堆栈。
const a = () => {
  b().then(() => c());
};
复制代码

上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。

改成async函数:

const a = async () => {
  await b();
  c();
};
复制代码

上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c(),错误堆栈将包括a()。

8. async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里:
async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}
复制代码
9. 继发/并发顺序输出:
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
复制代码

上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求:

async function logInOrder(urls) {
  // 并发读取远程URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
复制代码
10. 异步遍历器:asyncIterator,部署在Symbol.asyncIterator属性上面,最大的语法特点就是调用遍历器的next方法,返回的是一个 Promise 对象。
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();

asyncIterator
.next()
.then(iterResult1 => {
  console.log(iterResult1); // { value: 'a', done: false }
  return asyncIterator.next();
})
.then(iterResult2 => {
  console.log(iterResult2); // { value: 'b', done: false }
  return asyncIterator.next();
})
.then(iterResult3 => {
  console.log(iterResult3); // { value: undefined, done: true }
});
复制代码

可改写为:

async function f() {
  const asyncIterable = createAsyncIterable(['a', 'b']);
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  console.log(await asyncIterator.next());
  // { value: 'a', done: false }
  console.log(await asyncIterator.next());
  // { value: 'b', done: false }
  console.log(await asyncIterator.next());
  // { value: undefined, done: true }
}
复制代码

异步遍历器的next方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve以后再调用。这种情况下,next方法会累积起来,自动按照每一步的顺序运行下去,所以也可以这样:

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
  asyncIterator.next(), asyncIterator.next()
]);

console.log(v1, v2); // a b
复制代码
11. for await...of

for...of循环用于遍历同步的 Iterator 接口。新引入的for await...of循环,则是用于遍历异步的 Iterator 接口:

async function f() {
  for await (const x of createAsyncIterable(['a', 'b'])) {
    console.log(x);
  }
}
// a
// b
复制代码

如果next方法返回的 Promise 对象被reject,for await...of就会报错,要用try...catch捕捉。

async function () {
  try {
    for await (const x of createRejectingIterable()) {
      console.log(x);
    }
  } catch (e) {
    console.error(e);
  }
}
复制代码

注意,for await...of循环也可以用于同步遍历器:

(async function () {
  for await (const x of ['a', 'b']) {
    console.log(x);
  }
})();
// a
// b
复制代码
12. 异步 Generator 函数

就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象:

async function* gen() {
  yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
复制代码
13. yield* 语句

yield*语句也可以跟一个异步遍历器:

async function* gen1() {
  yield 'a';
  yield 'b';
  return 2;
}

async function* gen2() {
  // result 最终会等于 2
  const result = yield* gen1();
}
复制代码

与同步 Generator 函数一样,for await...of循环会展开yield*:

(async function () {
  for await (const x of gen2()) {  // 也是相当于执行了gen的遍历器
    console.log(x);
  }
})();
// a
// b
复制代码

最后

因为比较多,所以目前只整理到这里,后续有些比较重要难懂的模块会分开更新,同时包括ES6的部分,希望对你有所帮助,如有不合理的地方欢迎指正,喜欢的就关注一波吧,后续会持续更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值