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的部分,希望对你有所帮助,如有不合理的地方欢迎指正,喜欢的就关注一波吧,后续会持续更新。

原文地址:https://www.cnblogs.com/BetterMan-/p/10428013.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


原文连接:https://www.cnblogs.com/dupd/p/5951311.htmlES6之前已经出现了js模块加载的方案,最主要的是CommonJS和AMD规范。commonjs主要应用于服务器,实现同步加载,如nodejs。AMD规范应用于浏览器,如requirejs,为异步加载。同时还有CMD规范,为同步加载方案如seaJS。ES6在语言规格的层面上,实现了模块功能,而且...
以为Es6,javascript第一次支持了module。ES6的模块化分为导出(export)与导入(import)两个模块,其中在项目中,我们会经常看到一种用法import * as obj from,这种写法是把所有的输出包裹到obj对象里。示例一 1 2 3 4 5 6 7 // index.js export function fn1(data){ console.log(1) } export f.
视频讲解关于异步处理,ES5的回调使我们陷入地狱,ES6的Promise使我们脱离魔障,终于、ES7的async-await带我们走向光明。今天就来学习一下 async-await。async-await和Promise的关系经常会看到有了 async-await、promise 还有必要学习吗、async await优于promise的几个特点,接收了这些信息后,就蒙圈了。现在才知道...
TypeScript什么是TypeScript?TypeScript是由微软开发的一款开源的编程语言TypeScript是JavaScript的超集,遵循最新的ES5 ES6规范,TypeScript扩展了JavaScript的语法TypeScript更像后端 Java c# 这样的面向对象语言可以让js开发大型企业项目谷歌也在大力支持TypeScript的推广,React ,VUE 都集成了TypeScriptTypeScript安装安装-- 安装npm install -g type
export class AppComponent { title = 'Tour of heroes'; hero: Hero = { id: 1, name: '张三' };}export class Hero { id: number; name: string;}就是这一段,看起来有点晕,这里是实例化一个Hero类型的对象hero,还是创建一个变量?后面是赋值,但是不知道什么意思?hero: Hero = { id: 1, na.
用 async/await 来处理异步昨天看了一篇vue的教程,作者用async/ await来发送异步请求,从服务端获取数据,代码很简洁,同时async/await 已经被标准化,是时候学习一下了。先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。 写一个async 函数async function timeout() {return 'hello world'
es6的语法已经出了很长的时间了,在使用上也可以通过babel这类的编译工具转译为浏览器可以识别的es5的语法,但是依旧有很多开发在写代码的时候,依旧没有用es6的语法,而是习惯使用老的语法,这篇文章主要会介绍解构赋值基本用法以及在实际使用场景中相比es5语法的优势,让大家从根本上了解es6语法的优势基本用法数组解构让我们一起先来看数组解构的基本用法:let [a, b, c] ...
参考链接1 参考链接2写法1 - 使用 function 关键字function greeter(fn: (a: string) =&gt; void) { fn("Hello, World");}function printToConsole(s: string) { console.log(s);}greeter(printToConsole);(a: string) =&gt; void上述语法的含义:表示一个函数,接收一个字符串作为输入参数,没有返回参数。可
ES6简介-ECMAScript是javascript标准-ES6就是ECMAScript的第6个版本-ECMAScript6.0(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。ES6新增加的功能:1.let
ES6  在ES5的基础上增加了一些新的特性和写法,特别在函数写法上,增加了箭头函数 1.正经的函数写法//普通的传递值的写法functionsum1(x,y){returnx+y;}constres=sum1(2,3);console.log(res);//传递对象的方式,调用时需要传递一个对象过去function
ES5及ES6es表示ECMASCript,他是从es3,es5,es6,es5是2009.12月发布的,es6是2015.6月发布的。vue2完全支持es5的(vue3完全支持es6的),react完全支持es6es5的新特性严格模式(对应的相反的称为怪异模式)'usestrict'//一般用于相关的设计上面书写一个严格模式底下的代码就需要按照严格模
ES5的严格模式所谓严格模式,从字面上就很好理解,即更严格的模式,在这种模式下执行,浏览器会对JS的要求更苛刻,语法格式要求更细致,更符合逻辑。怪异模式:就是我们之前一直使用的开发模式,就叫怪异模式。因为很多时候出来的结果是非常怪异的,所以才称之为怪异模式。'usestrict'//一般用
相同点export与exportdefault均可用于导出常量、函数、文件、模块可在其它文件或模块中通过import+(常量|函数|文件|模块)名的方式,将其导入,以便能够对其进行使用不同点一、在一个文件或模块中,export、import可以有多个,exportdefault仅有一个//model.jsle
24.class类 25.class中的extend 26.super关键字 27.super应用 28.class属性 30.静态成员和实例成员 31.构造函数问题 32.构造函数原型 33.原型链 34.js查找机制 35.原型对象中this指向 36.扩展内置对象方法 37.call方法 38.借用父构造函数
什么是ES6ES的全称是ECMAScript,它是由ECMA国际标准化组织,制定的一项脚本语言的标准化规范。泛指2015年发布的es2015版极其后续版本ES6中新增语法letES6中新增的用于声明变量的关键字。注意:使用let关键字声明的变量才具有块级作用域,使用var声明的变量不具备块级作用域特
命名修饰符let:不能重复声明变量、块级作用域leta=1;leta=2;//报错const:初始化常量,必须给初始值,否则报错、在同一个作用域内,const定义的常量不能修改其值、块级作用域consta=10a=100//报错,不能重复声明解构constobj={name:'jack'age:18sex:'
ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。1{2leta=10;3varb=1;4}56a//ReferenceError:aisnotdefined.7b//1上面代码在代码块之中,分别用let和var声明了两个变量。然后在代码块之外调用
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><metahttp-equiv="X-UA-Compatib
一,RegExp构造函数es5中,RegExp构造函数的参数有两种情况。1,参数是字符串,第二个参数表示正则表达式的修饰符(flag)。2,参数是一个正则表达式,返回一个原有正则表达式的拷贝。es6中,如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而
一、字符的Unicode表示法JavaScript允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的Unicode码点。表示法只限于码点在\u0000~\uFFFF之间的字符,超过该范围需要用两个双字节表示ES6改进:将码点放入大括号,就能正确解读该字符。转换参考:https://blog.csdn.net/hezh1994/ar