资讯专栏INFORMATION COLUMN

ES6学习笔记3-Set和Map、Promise、Iterator、Generator、async

sevi_stuo / 2932人阅读

摘要:去除数组的重复成员这表明,在内部,两个是相等。返回一个布尔值,表示该值是否为的成员。使用回调函数遍历每个成员没有返回值。对象特点对象有三种状态进行中已完成,又称和已失败。方法是的别名,用于指定发生错误时的回调函数。

Set和Map数据结构 Set

新的数据结构Set类似于数组,但是成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。接受一个数组(或类似数组的对象)作为参数,用来初始化。

const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

因为Set的成员都是唯一的,所以可用set去除数组重复成员。向Set加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。Set内部判断两个值是否不同类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。Array.from方法可以将 Set 结构转为数组。

// 去除数组的重复成员
[...new Set(array)]

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set  // Set(1) {NaN} 这表明,在 Set 内部,两个NaN是相等。
Set 实例的属性和方法 Set 实例的属性

size:返回Set实例的成员总数。

constructor:指向构造函数,默认就是Set函数。

Set 实例的方法

操作方法

add(value):添加某个值,返回Set结构本身。

delete(value):删除某个值,返回一个布尔值,表示删除是否成功。

has(value):返回一个布尔值,表示该值是否为Set的成员。

clear():清除所有成员,没有返回值。

遍历操作

keys():返回键名的遍历器对象。

values():返回键值的遍历器对象。

entries():返回键值对的遍历器对象。

forEach():使用回调函数遍历每个成员,没有返回值。第一个参数是一个处理函数。该函数的参数依次为键值、键名、集合本身。第二个参数,表示绑定的this对象。

由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

Set的遍历顺序就是插入顺序。通过该特性,当用Set保存一个回调函数列表,调用时就能保证按照添加顺序调用。

Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。扩展运算符(...)可用于 Set 结构。

Set.prototype[Symbol.iterator] === Set.prototype.values
// true
WeakSet

WeakSet 是一个构造函数,可以使用new命令,创建 WeakSet 数据结构。可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。

const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

上面的代码中,a数组的成员成为 WeakSet 的成员,而不是a数组本身。

WeakSet 结构有以下三个方法:

WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。

WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。

WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

WeakSet的特点

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,与Set不同的是:

WeakSet 的成员只能是对象,而不能是其他类型的值。

WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

WeakSet 没有size属性,不可遍历。

WeakSet的以上特性决定WeakSet适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakMap 里面的引用就会自动消失。

Map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。
Map 数据结构类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map作为构造函数,可以接受任何具有 Iterator 接口的数据结构作为参数,比如数组。

//Map构造函数接受数组作为参数,实际上执行的是下面的算法。
const items = [
  ["name", "张三"],
  ["title", "Author"]
];

const map = new Map();

items.forEach(
  ([key, value]) => map.set(key, value)
);

如果对同一个键多次赋值,后面的值将覆盖前面的值。

如果读取一个未知的键,则返回undefined。

只有对同一个对象的引用,Map 结构才将其视为同一个键,因为Map 的键实际上是跟内存地址绑定。

const map = new Map();
map.set(["a"], 555);
map.get(["a"]) // undefined

判断Map键是否相等

Map键是对象类型的,内存地址相同才相同。

Map键是简单类型(数字、字符串、布尔值)的,两个值严格相等视为一个键。0和-0是同一个键。

Map键将NaN和其自身视为同一个键。

Map实例的属性和方法 Map实例的属性

size:返回 Map 结构的成员总数。

Map实例的方法

set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。

get(key):读取key对应的键值,如果找不到key,返回undefined。

has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中。

delete(key):删除某个键,返回true。如果删除失败,返回false。

clear():清除所有成员,没有返回值。

遍历方法

keys():返回键名的遍历器。

values():返回键值的遍历器。

entries():返回所有成员的遍历器。

forEach():使用回调函数遍历Map的每个成员。第一个参数是一个处理函数。该函数的参数依次为键值、键名、集合本身。第二个参数,表示绑定的this对象。

Map 的遍历顺序就是插入顺序。

Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。但是

WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

WeakMap的键名所指向的对象,不计入垃圾回收机制。

WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

WeakMap结构有助于防止内存泄漏。

WeakMap没有遍历操作(即没有key()、values()和entries()方法),也没有size属性,也不支持clear方法。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。

Promise

Promise对象特点:

Promise对象有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。

一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就不会再变了。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

状态一改变,即调用Promise 对象的 then方法。

缺点:

Promise一旦新建它就会立即执行,无法中途取消。

如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。

当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

基本用法

Promise对象是一个构造函数,用来生成Promise实例。

var promise = new Promise(
   function(resolve, reject) {
     // ... some code
     if (/* 异步操作成功 */){
         resolve(value);
     } else {
         reject(error);
    } 
});

Promise构造函数接受一个函数作为参数,该函数在Promise构造函数返回新建对象前被调用,被传递resolve和reject函数。resolve和reject函数由JavaScript引擎提供,不用自己部署。若参数函数抛出一个错误,那么该promise 状态为rejected。函数的返回值被忽略。

resolve函数:将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),将传给resolve函数的参数传递出去
reject函数:将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),将传给Promise函数的参数传递出去。
简而言之,如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。

resolve函数可以传递一个Promise实例。当传递的是一个Promise实例时,其自身状态无效,其状态由该Promise实例决定。

var p1 = new Promise(function (resolve, reject) {
  // ...
});

var p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
})

上面代码中p2的resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。

注意,这时p1的状态就会传递给p2,也就是说,这时p2自己的状态无效了,由p1的状态决定p2的状态如果p1的状态是Pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调函数将会立刻执行。

Promise.prototype.then()

the()方法返回一个新的Promise。因此可以采用链式写法。

promise.then(onFulfilled, onRejected);
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。这两个函数都接受Promise对象传出的值作为参数。若省略这两个参数,或者提供非函数,不会产生任何错误。

注意:

如果 onFulfilled 或者 onRejected 抛出一个错误,或者返回一个拒绝的 Promise ,then 返回一个 rejected Promise。

如果 onFulfilled 或者 onRejected 返回一个 resolves Promise,或者返回任何其他值,或者未返回值,then 返回一个 resolved Promise。

onFulfilled 或者 onRejected是被异步调用的。异步调用指的是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时执行。

getJSON("/posts.json").then(function(json) {
  return json.post; 
}).then(function(post) {
  // ...
});

上面的代码中第一个回调函数完成以后,会将返回的json.post作为参数,传入第二个回调函数。若前一个回调函数返回的是一个Promise对象,这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

setTimeout(function(){
    console.log("aaa");
});
// using a resolved promise, the "then" block will be triggered instantly, but its handlers will be triggered asynchronously as demonstrated by the console.logs
var resolvedProm = Promise.resolve(33);

var thenProm = resolvedProm.then(function(value){
    console.log("this gets called after the end of the main stack. the value received and returned is: " + value);
    return value;
});
// instantly logging the value of thenProm
console.log(thenProm);

// using setTimeout we can postpone the execution of a function to the moment the stack is empty
setTimeout(function(){
    console.log(thenProm);
});

//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//this gets called after the end of the main stack. the value received and returned is: 33
//aaa
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 33}

上面代码中:setTimeout(fn, 0)在下一轮“事件循环”开始时执行,onFulfilled 在本轮“事件循环”结束时执行,console.log(thenProm)则是立即执行,因此最先输出。

若then中无对应的回调函数,则then返回的新promise将会保持原promise的状态进行调用。
例如:

function timeout(ms) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, ms);
    });
  }
  timeout(100).then(null, (value) => {
    console.log("aaa");
  }).then((value) => {
    console.log("ccc");
  }, (t) => {
    console.log("ffffd");
  });

//ccc

上面代码中,timeout函数中的 Promise状态是resolve,但是第一个then中没有对应的回调函数,因此第一个then返回的是resolve状态的Promise。所以第二个then立马被调用,输出"ccc"。

Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。该方法返回一个新的Promise。

p.catch(onRejected);

p.catch(function(reason) {
   // 拒绝
});

onRejected 抛出一个错误,或者返回一个拒绝的 Promise,则catch返回一个 rejected Promise,否则返回一个resolved Promise。

getJSON("/posts.json").then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log("发生错误!", error);
});

上面代码中,getJSON方法返回一个 Promise 对象,如果该对象状态变为Resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。

一般来说,不要在then方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

因为第二种写法可以捕获前面then方法执行中的错误,所以建议总是使用catch方法,而不使用then方法的第二个参数。

重要解析:

var promise = new Promise(function(resolve, reject) {
  resolve("ok");
  throw new Error("test");
});
promise.then(function(value) { console.log(value) });
//ok


var 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

上面代码中第一个例子中,throw 在resolve语句后面,抛出的错误,已经被捕获并处理。但是Promise 的状态因为resolve("ok")语句已改变,所以不会再改变。
上面代码中第二个例子中抛出错误时,Promise函数体已经运行结束,所以无法捕捉到该错误,就出现了在console中出现"ok"并抛出异常的现象。
详见Promise源码中的tryCallTwo和doResolve函数

Promise.all()

Promise.all(iterable):当在可迭代参数中的所有promises被resolve,或者任一 Promise 被 reject时,返回一个新的promise。
iterable:一个可迭代对象,例如 Array。

状态判定

(1)iterable为空(比如[]),返回一个同步的resolved Promise。
(2)iterable未包含任何的promises(比如[1,2,3]),返回一个异步的resolved Promise。

(3)iterable中的所有promises都是resolve,返回一个异步的resolved Promise。

以上情况中,iterable内的所有值将组成一个数组,传递给回调函数。

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "foo");
}); 

Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // [3, 1337, "foo"] 
});

(4)只要iterable中的promises有一个被rejected,就立即返回一个异步的rejected Promise。此时第一个被reject的实例的返回值,会传递给回调函数。

 Promise.all([1,2,3, Promise.reject(555)]);
//Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 555}
如何理解返回一个异步的Promise
var p = Promise.all([]); // will be immediately resolved
var p2 = Promise.all([1337, "hi"]); // non-promise values will be ignored, but the evaluation will be done asynchronously
console.log(p);
console.log(p2)
setTimeout(function(){
    console.log("the stack is now empty");
    console.log(p2);
});

// Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: Array(0)}
// Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
// the stack is now empty
// Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: Array(2)}
Promise.race()

Promise.race(iterable):方法返回一个新的异步的promise,参数iterable中只要有一个promise对象"完成(resolve)"或"失败(reject)",新的promise就会立刻"完成(resolve)"或者"失败(reject)",并获得之前那个promise对象的返回值或者错误原因。

var resolvedPromisesArray = [Promise.resolve(33), Promise.resolve(44)];

var p = Promise.race(resolvedPromisesArray);
// immediately logging the value of p
console.log(p);

// using setTimeout we can execute code after the stack is empty
setTimeout(function(){
    console.log("the stack is now empty");
    console.log(p);
});

//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//98
//the stack is now empty
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 33}

若iterable为空,则返回的promise永远都是pending状态。
若iterable里面包含一个或多个非promise值并且/或者有一个resolved/rejected promise,则新生成的Promise的值为数组中的能被找到的第一个值。

var foreverPendingPromise = Promise.race([]);
var alreadyResolvedProm = Promise.resolve(666);

var arr = [foreverPendingPromise, alreadyResolvedProm, "non-Promise value"];
var arr2 = [foreverPendingPromise, "non-Promise value", Promise.resolve(666)];
var p = Promise.race(arr);
var p2 = Promise.race(arr2);

console.log(p);
console.log(p2);
setTimeout(function(){
    console.log("the stack is now empty");
    console.log(p);
    console.log(p2);
});

//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//the stack is now empty
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 666}
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "non-Promise value"}
Promise.resolve()

Promise.resolve返回一个Promise对象。

Promise.resolve(value);
Promise.resolve(promise);
Promise.resolve(thenable);

Promise.resolve方法的参数:

参数是一个Promise实例:Promise.resolve将不做任何修改、原封不动地返回这个实例。

参数是一个thenable对象:thenable对象指的是具有then方法的对象。Promise.resolve方法将该对象转为Promise对象后,就会立即执行thenable对象的then方法。

let thenable = {
then: function(resolve, reject) {
  resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value);  // 42
});
//thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出42。

其他情况:Promise.resolve方法返回一个新的Promise对象,状态为Resolved。Promise.resolve方法的参数,会同时传给回调函数。

var p = Promise.resolve("Hello");

p.then(function (s){
console.log(s)
});
// Hello
//返回Promise实例的状态从一生成就是Resolved,所以回调函数会立即执行

Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。因此,回调函数会立即执行。

Promise.reject(reason);

Promise.reject()方法的参数,会原封不动地作为返回的新Promise的[[PromiseValue]]值,变成后续方法的参数。

Iterator(遍历器)

JavaScript原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6又添加了Map和Set。一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。任何数据结构只要部署了Iterator接口,就称这种数据结构是”可遍历的“(iterable)。

Symbol.iterator属性本身是一个函数,执行这个函数,就会返回一个遍历器。属性名Symbol.iterator是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内。

遍历器对象的根本特征: 具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,该对象具有value和done两个属性。

内置可迭代对象:String, Array, TypedArray, Map and Set
接受可迭代对象作为参数的:Map([iterable]), WeakMap([iterable]), Set([iterable])、WeakSet([iterable])、Promise.all(iterable), Promise.race(iterable) 以及 Array.from()。

一个对象如果要有可被for...of循环调用的Iterator接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的Iterator接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

如果Symbol.iterator方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。

调用Iterator接口的场合

解构赋值:对数组和Set结构进行解构赋值时,会默认调用Symbol.iterator方法。

扩展运算符:扩展运算符(...)也会调用默认的iterator接口。因此,可通过(...)方便的将部署了Iterator接口的数据接口转为数组。

 let arr = [...iterable];

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

任何接受数组作为参数的场合,都调用了遍历器接口。

 for...of
 Array.from()
 Map(), Set(), WeakMap(), WeakSet()(比如new Map([["a",1],["b",2]]))
 Promise.all()
 Promise.race()

for...in循环读取键名。for...of循环读取键值,但数组的遍历器接口只返回具有数字索引的键值。

let arr = [3, 5, 7];
arr.foo = "hello";

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}
//for...of循环不返回数组arr的foo属性

Set 和 Map 结构使用for...of循环时:

遍历的顺序是按照各个成员被添加进数据结构的顺序。

Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。

ES6的数组、Set、Map均有以下方法(返回的都是遍历器对象,与Object的entries、keys、values方法不同,Object返回的均是数组。):

entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。

keys() 返回一个遍历器对象,用来遍历所有的键名。

values() 返回一个遍历器对象,用来遍历所有的键值。

for...of循环能正确识别字符串中的32位 UTF-16 字符。
可通过Array.from方法将类似数组的对象转为数组。

与其他遍历语法的比较

forEach:无法中途跳出forEach循环,break命令或return命令都不能奏效。

for...in:不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。

for...of循环可以与break、continue和return配合使用,提供了遍历所有数据结构的统一操作接口。

Generator

Generator 函数是一个普通函数,有以下特征:

function关键字与函数名之间有一个星号。

函数体内部使用yield表达式,定义不同的内部状态。

调用Generator 函数,就是在函数名后面加上一对圆括号。不过,调用 Generator 函数后,该函数并不执行,而是返回一个遍历器对象。调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性就是yield表达式或return后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。Generator 函数不能当构造器使用。

function* f() {}
var obj = new f; // throws "TypeError: f is not a constructor"
yield 表达式

遍历器对象的next方法的运行逻辑:

遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

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

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

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

function* demo() {
  console.log("Hello" + (yield)); 
  console.log("Hello" + (yield 123)); 
}
var a=demo();
a.next();
//Object {value: undefined, done: false}  第一次运行了yield之后就停止了。
a.next();
//Helloundefined
//Object {value: 123, done: false} 第二次将之前的hello打印,并运行yield 123之后停止。
a.next();
//Helloundefined
//Object {value: undefined, done: true}

yield表达式与return语句:
相似之处:能返回紧跟在语句后面的那个表达式的值。
不同之处:每次遇到yield,函数暂停执行,下一次再从该位置后继续向后执行,即使运行到最后一个yield ,其返回对象的done仍为false。return语句执行后即代表该遍历结束,返回对象的done为true。

function* helloWorldGenerator() {
  yield "hello";
  return "ending";
  yield "world";
}

var hw = helloWorldGenerator();
hw.next();
// Object {value: "hello", done: false}
hw.next();
// Object {value: "ending", done: true}
hw.next();
// Object {value: undefined, done: true}

Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。但yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

function* f() {
  console.log("执行了!")
}
var generator = f();
setTimeout(function () {
  generator.next()
}, 2000);

上面代码中函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。

yield表达式如果用在另一个表达式之中,必须放在圆括号里面。如果用作函数参数或放在赋值表达式的右边,可以不加括号。

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性。

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

function* gen(){
  // some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
next 方法的参数

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。注意,由于next方法的参数表示上一个yield表达式的返回值,所以第一次使用next方法时,不用带参数。

for...of 循环

for...of循环可以自动遍历 Generator 函数时生成的Iterator对象。

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

注意:一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

for...of的本质是一个while循环,通俗的讲,就是运行对象的Symbol.iterator方法(即遍历器生成函数),得到遍历器对象,再不停的调用遍历器对象的next方法运行,直到遍历结束。类似如下:

var it = foo();
var res = it.next();

while (!res.done){
  // ...
  res = it.next();
}
Generator.prototype.throw()

throw() 方法:向Generator函数内部抛出异常,并恢复生成器的执行,返回带有 done 及 value 两个属性的对象。

gen.throw(exception)

exception:要抛出的异常。

该方法可以在Generator 函数体外抛出错误,然后在 Generator 函数体内捕获。

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

var i = g();
i.next();

try {
  i.throw("a");
  i.throw("b");
} catch (e) {
  console.log("外部捕获", e);
}
// 内部捕获 a
// 外部捕获 b

上面代码中,遍历器对象i连续抛出两个错误。第一个错误被 Generator 函数体内的catch语句捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。

throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。遍历器对象的throw方法和全局的throw命令不一样。全局的throw命令只能被该命令外的catch语句捕获,且不会再继续try代码块里面剩余的语句了。

如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。

var g = function* () {
  while (true) {
    yield;
    console.log("内部捕获", e);
  }
};

var i = g();
i.next();

try {
  i.throw("a");
  i.throw("b");
} catch (e) {
  console.log("外部捕获", e);
}
// 外部捕获 a

throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

var gen = function* gen(){
  try {
    yield console.log("a");
    console.log("b");
  } catch (e) {
    console.log("错误被捕获");
  }
  yield console.log("c");
  yield console.log("d");
}

var g = gen();
g.next();
//a
//Object {value: undefined, done: false}
g.throw();
//错误被捕获
//c
//Object {value: undefined, done: false}
g.next();
//d
//Object {value: undefined, done: false}

上面的代码可以看出,g.throw方法是先抛出异常,再自动执行一次next方法,因此可以看到没有打印b,但是打印了c。

Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的catch捕获。

一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

Generator.prototype.return()

Generator.prototype.return可以返回给定的值,并且终结遍历Generator函数。若该方法被调用时,Generator函数已结束,则Generator函数将保持结束的状态,但是提供的参数将被设置为返回对象的value属性的值。

遍历器对象调用return方法后,返回值的value属性就是return方法的参数foo。并且,Generator函数的遍历就终止了,返回值的done属性为true,以后再调用next方法,done属性总是返回true。如果return方法调用时,不提供参数,则返回值的value属性为undefined。

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 }
g.return()      // {value: undefined, done: true}
g.return("111")  //{value: "111", done: true}

上面代码中,g.return("111")调用时, Generator函数的遍历已经终止,所以返回的对象的done值仍为true,但是value值会被设置为"111"。

如果 Generator 函数内部有try...finally代码块,那么当return方法执行时的语句在 Generator 函数内部的try代码块中时,return方法会推迟到finally代码块执行完再执行。

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next();
//Object {value: 1, done: false}
g.return(7);
//Object {value: 7, done: true}
//return执行时还未在try语句块内,所以返回{value: 7, done: true}并终止遍历。


function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
    yield 33;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next();
// Object {value: 1, done: false}
g.next();
// Object {value: 2, done: false}
g.return(7);
// Object {value: 4, done: false}
g.next();
// Object {value: 5, done: false}
g.next();
// Object {value: 7, done: true}
//return执行时已在try语句块内,运行时直接跳至finally语句块执行,并在该语句块内的代码执行完后,所以返回{value: 7, done: true}并终止遍历。
yield* 表达式

yield* expression :expression 可以是一个generator 或可迭代对象。yield * 表达式自身的值是当迭代器关闭时返回的值(即,当done时为true)。

function* foo() {
  yield "a";
  yield "b";
}

function* bar() {
  yield "x";
  yield* foo();
  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"

yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。有return语句时,若想遍历出return返回的值,则需要用var value = yield* iterator的形式获取return语句的值。

function* foo() {
  yield "a";
  yield "b";
  return "mm";
}
function* bar() {
  yield "x";
  yield* foo();
  yield "y";
}
for(var t of bar()){
 console.log(t);
}
//x
//a
//b
//y
//yield* foo()写法无法遍历出foo里面的“mm”。

function* foo() {
  yield "a";
  yield "b";
  return "mm";
}
function* bar() {
  yield "x";
  yield yield* foo();
  yield "y";
}
for(var t of bar()){
 console.log(t);
}
//x
//a
//b
//mm
//y
//yield* foo()运行返回的值就是“mm”,所以yield yield* foo()可以遍历出“mm”。

任何数据结构只要有 Iterator 接口,就可以被yield*遍历。yield*就相当于是使用for...of进行了循环。

作为对象属性的Generator函数

如果一个对象的属性是 Generator 函数,则需在属性前面加一个星号。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};
//等同于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};
Generator 函数的this

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。Generator函数不能跟new命令一起用,会报错。

function* g() {}

g.prototype.hello = function () {
  return "hi!";
};

let obj = g();
obj.hello() // "hi!"

上面代码可以看出,obj对象是Generator 函数g的实例。但是,如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。

function* g() {
  this.a = 11;
}

let obj = g();
obj.a // undefined

上面代码中,Generator函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性。

应用场景

用来处理异步操作,改写回调函数。即把异步操作写在yield表达式里面,异步操作的后续操作放在yield表达式下面。

function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
  console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
     it.next(response);
  });
}

var it = main();
it.next();
//上面为通过 Generator 函数部署 Ajax 操作。

控制流管理

利用 Generator 函数,在任意对象上部署 Iterator 接口。

作为数据结构。

Generator 函数的异步应用

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。整个 Generator 函数就是一个封装的异步任务。异步操作需要暂停的地方,都用yield语句注明。

Generator 函数可以进行数据交换。next返回值的value属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。

Generator 函数可以部署错误处理代码,捕获函数体外抛出的错误。

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw("出错了");
// 出错了

上面代码的最后一行,Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try...catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

async 函数

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function add1(x) {
  var a = resolveAfter2Seconds(20);
  var b = resolveAfter2Seconds(30);
  return x + await a + await b;
}

add1(10).then(v => {
  console.log(v);  // prints 60 after 2 seconds.
});

async function add2(x) {
  var a = await resolveAfter2Seconds(20);
  var b = await resolveAfter2Seconds(30);
  return x + a + b;
}

add2(10).then(v => {
  console.log(v);  // prints 60 after 4 seconds.
});

async 函数有多种使用形式。

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open("avatars");
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar("jake").then(…);

// 箭头函数
const foo = async () => {};

调用async函数时会返回一个 promise 对象。当这个async函数返回一个值时,promise 的 resolve 方法将会处理这个返回值;当异步函数抛出的是异常或者非法值时,promise 的 reject 方法将处理这个异常值。

async function f() {
  throw new Error("出错了");
}

f().then(
  v => console.log(v),
  e => console.log(e)
)
// Error: 出错了

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

await 命令

await expression:会造成异步函数停止执行并且等待 promise 的解决后再恢复执行。若expression是Promise 对象,则返回expression的[[PromiseValue]]值,若expression不是Promise 对象,则直接返回该expression。

async function f2() {
  var y = await 20;
  console.log(y); // 20
}
f2();

await命令后面一般是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。await命令后面的 Promise 对象如果变为reject状态,则会throws异常值,因此reject的参数会被catch方法的回调函数接收到。只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行。

async function f() {
  await Promise.reject("出错了");
  await Promise.resolve("hello world"); // 第二个await语句是不会执行的
}
错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。防止出错的方法,是将其放在try...catch代码块之中。

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error("出错了");
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了

上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。

使用注意点

最好把await命令放在try...catch代码块中。因为await命令后面的Promise对象,运行结果可能是rejected。

async function myFunction() {
 try {
   await somethingThatReturnsAPromise();
 } catch (err) {
   console.log(err);
  }
}

 // 另一种写法

 async function myFunction() {
   await somethingThatReturnsAPromise()
    .catch(function (err) {
      console.log(err);
    };
  }

多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。同时触发可以使用Promise.all。

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

await命令只能用在async函数之中,如果用在普通函数,就会报错。

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

  // 报错。 因为await用在普通函数之中
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

异步遍历的接口

异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。

   asyncIterator
  .next()
  .then(
    ({ value, done }) => /* ... */
  );

上面代码中,asyncIterator是一个异步遍历器,调用next方法以后,返回一个 Promise 对象。因此,可以使用then方法指定,这个 Promise 对象的状态变为resolve以后的回调函数。回调函数的参数,则是一个具有value和done两个属性的对象,这个跟同步遍历器是一样的。

一个对象的同步遍历器的接口,部署在Symbol.iterator属性上面。同样地,对象的异步遍历器接口,部署在Symbol.asyncIterator属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator属性有值,就表示应该对它进行异步遍历。

for await...of

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

async function f() {
  for await (const x of createAsyncIterable(["a", "b"])) {
    console.log(x);
  }
}
// a
// b

上面代码中,createAsyncIterable()返回一个异步遍历器,for...of循环自动调用这个遍历器的next方法,会得到一个Promise对象。await用来处理这个Promise对象,一旦resolve,就把得到的值(x)传入for...of的循环体。

异步Generator函数

在语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。

async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}

上面代码中,异步操作前面使用await关键字标明,即await后面的操作,应该返回Promise对象。凡是使用yield关键字的地方,就是next方法的停下来的地方,它后面的表达式的值(即await file.readLine()的值),会作为next()返回对象的value属性。

Class

constructor定义构造方法,this关键字代表实例对象。定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。类的数据类型就是函数,类的原型的constructor指向类自身。使用的时候,对类使用new命令。

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return "(" + this.x + ", " + this.y + ")";
  }
  static distance() {
      
   }
}
typeof Point // "function"
Point === Point.prototype.constructor // true

类的一般的方法都定义在类的prototype属性上面。在类的实例上面调用方法,其实就是调用原型上的方法。类的内部所有定义的方法,都是不可枚举的。类的静态方法只能用类来调用,不能用类的实例调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。父类的静态方法,可以被子类继承。

class Point {
  constructor(){
    // ...
  }
  toString(){
    // ...
  }
  toValue(){
    // ...
  }
}

// 等同于
Point.prototype = {
  toString(){},
  toValue(){}
};

类的属性名,可以采用表达式。

let methodName = "getArea";
class Square{
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}
//Square类的方法名getArea,是从表达式得到的。
constructor方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

constructor方法默认返回实例对象(即this),也可以指定返回另外一个对象。类的构造函数,不使用new是没法调用的,会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

class Foo {
  constructor() {
    return Object.create(null);
  }
}
new Foo() instanceof Foo
// false

上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。

类的实例对象

生成类的实例对象的写法,也是使用new命令。如果忘记加上new,像函数那样调用Class,将会报错。类里面定义的属性除了定义在this上的,其他都是定义在原型上的。定义在this上的属性各实例对象各自有一份。类的所有实例共享一个原型对象。

Class不存在变量提升(hoist),因此先使用,后定义会报错。

new Foo(); // ReferenceError
class Foo {}
Class表达式

类也可以使用表达式的形式定义。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。

如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

const MyClass = class { /* ... */ };

采用Class表达式,可以写出立即执行的Class。

let person = new class {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
}("张三");
person.sayName(); // "张三"
// person是一个立即执行的类的实例。

ES6不提供私有方法。

this的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦多带带使用该方法,很可能报错。注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

class Logger {
  printName(name = "there") {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property "print" of undefined

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来多带带使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。

一个比较简单的解决方法是,在构造方法中绑定this。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  // ...
}

另一种解决方法是使用箭头函数。

还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this。

function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== "function") {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}

const logger = selfish(new Logger());
严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。

name属性

本质上,ES6的类只是ES5的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。name属性总是返回紧跟在class关键字后面的类名。

class Point {}
Point.name // "Point"
Class的继承

Class之间可以通过extends关键字实现继承。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + " " + super.toString(); // 调用父类的toString()
  }
}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

ES6的继承实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

如果子类没有定义constructor方法,以下方法会被默认添加。因此,不管有没有显式定义,任何一个子类都有constructor方法。

constructor(...args) {
  super(...args);
}
类的prototype属性和__proto__属性

Class同时有prototype属性和__proto__属性,因此同时存在两条继承链。

子类的__proto__属性,表示构造函数的继承,总是指向父类。

子类prototype属性的__proto__属性,总是指向父类的prototype属性。

类的继承是按照下面的模式实现的。

class A {
}

class B {
}

Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(B, A);
const b = new B();

而Object.setPrototypeOf方法的实现如下:

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

因此,就得到如下结果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
Extends 的继承目标

extends关键字后面可以跟多种类型的值。

class B extends A {
}

上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。

class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

上面代码中,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。A.prototype是一个对象,所以A.prototype.__proto__指向构造函数(Object)的prototype属性。

class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

//等同于

class C extends null {
  constructor() { return Object.create(null); }
}

上面代码中,子类继承null。

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。因此,可以使用这个方法判断,一个类是否继承了另一个类。

Object.getPrototypeOf(ColorPoint) === Point //true
super 关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。

(一) super作为函数调用时,代表父类的构造函数,且super()只能用在子类的构造函数之中,用在其他地方就会报错。ES6 要求,子类的构造函数必须执行一次super函数。

class A {}

class B extends A {
constructor() {
  super();
}
}

子类B的构造函数之中的super(),代表调用父类的构造函数。super()在这里相当于A.prototype.constructor.call(this)

(二) super作为对象时,在普通方法中,指向父类的原型对象(当指向父类的原型对象时,定义在父类实例上的方法或属性,是无法通过super调用的。);在静态方法中,指向父类。

class A {
    p() {
      return 2;
    }
    static m() {
      console.log("父类的m方法被调用")
    }
  }

 class B extends A {
    constructor() {
      super();
      console.log(super.p()); // 2
    }
    static show() {
      super.m();
    }
  }

  let b = new B();
  B.show(); //父类的m方法被调用

上面代码中,子类B的constructor中的super.p()在普通方法中,指向A.prototype,所以super.p()就相当于A.prototype.p()。子类B的show方法中的super.m()在静态方法中,所以super.m()就相当于A.m()。

ES6 规定,通过super调用父类的方法时,super会绑定子类的this。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()会绑定子类B的this,导致输出的是2。也就是说,实际上执行的是super.print.call(this)。

通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  
 constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。

注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

class A {}
class B extends A {
  constructor() {
    super();
    console.log(super); // 报错
  }
}
//console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。
实例的__proto__属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。

class Point{

}
class ColorPoint extends Point{
   constructor(){
      super();
  }
}
var p1 = new Point();
var p2 = new ColorPoint();
p2.__proto__.__proto__ === p1.__proto__ // true
原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些。

Boolean()

Number()

String()

Array()

Date()

Function()

RegExp()

Error()

Object()

extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。
注意,继承Object的子类,有一个行为差异。

class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}
var o = new NewObj({attr: true});
console.log(o.attr === true);  // false

上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为ES6改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6规定Object构造函数会忽略参数。

Class的取值函数(getter)和存值函数(setter)

在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。存值函数和取值函数是设置在属性的descriptor对象上的。

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return "getter";
  }
  set prop(value) {
    console.log("setter: "+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// "getter"
//代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。
Class的静态方法

在一个方法前,加上static关键字,则是静态方法。静态方法不会被实例继承,而是直接通过类来调用。因此在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

class Foo {
  static classMethod() {
    return "hello";
  }
}
Foo.classMethod() // "hello"
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。静态方法可以与非静态方法重名。父类的静态方法,可以被子类继承。

class Foo {
  static bar () {
    this.baz();
  }
  static baz () {
    console.log("hello");
  }
  baz () {
    console.log("world");
  }
}

Foo.bar() // hello

上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。

Class的静态属性和实例属性

静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。因为ES6明确规定,Class内部只有静态方法,没有静态属性。所以目前只有下面这种写法。

//为Foo类定义了一个静态属性prop
class Foo {
}

Foo.prop = 1;
Foo.prop // 1

ES7有一个静态属性的提案,目前Babel转码器支持。这个提案规定:

类的实例属性可以用等式,写入类的定义之中。

class MyClass {
  myProp = 42;

  constructor() {
    console.log(this.myProp); // 42
  }
}

类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。

// 老写法
class Foo {
   // ...
}
Foo.prop = 1;

// 新写法
class Foo {
  static prop = 1;
}

类的私有属性

目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。#也可以用来写私有方法。私有属性可以指定初始值,在构造函数执行时进行初始化。

class Point {
  #x;
  constructor(x = 0) {
    #x = +x;
  }
  get x() { return #x }
  set x(value) { #x = +value }
  #sum() { return #x; } 
}

上面代码中,#x就表示私有属性x,在Point类之外是读取不到这个属性的。还可以看到,私有属性与实例的属性是可以同名的(比如,#x与get x())。

new.target属性

ES6为new命令引入了一个new.target属性,(在构造函数中)返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error("必须使用new生成实例");
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error("必须使用new生成实例");
  }
}

var person = new Person("张三"); // 正确
var notAPerson = Person.call(person, "张三");  // 报错
//上面代码确保构造函数只能通过new命令调用。

Class内部调用new.target,返回当前Class。子类继承父类时,new.target会返回子类。在函数外部使用new.target会报错。

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}

var obj = new Square(3           
               
                                           
                       
                 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83292.html

相关文章

  • GeneratorAsync/Await

    摘要:以往的异步方法无外乎回调函数和。出错了出错了总结接口遍历器对象除了具有方法,还可以具有方法和方法。函数调用函数,返回一个遍历器对象,代表函数的内部指针。 引言 接触过Ajax请求的会遇到过异步调用的问题,为了保证调用顺序的正确性,一般我们会在回调函数中调用,也有用到一些新的解决方案如Promise相关的技术。 在异步编程中,还有一种常用的解决方案,它就是Generator生成器函数。顾...

    Eastboat 评论0 收藏0
  • es6 promise面试

    摘要:执行函数会返回一个遍历器对象,每一次函数里面的都相当一次遍历器对象的方法,并且可以通过方法传入自定义的来改变函数的行为。函数可以通过配合函数更轻松更优雅的实现异步编程和控制流管理。它和构造函数的不同点类的内部定义的所有方法,都是不可枚举的。 let const的命令 在ES6之前,声明变量只能用var,var方式声明变量其实是很不合理的,准确的说,是因为ES5里面没有块级作用域是很不合...

    timger 评论0 收藏0
  • JavaScript异步编程:GeneratorAsync

    摘要:从开始,就在引入新功能,来帮助更简单的方法来处理异步编程,帮助我们远离回调地狱。而则是为了更简洁的使用而提出的语法,相比这种的实现方式,更为专注,生来就是为了处理异步编程。 从Promise开始,JavaScript就在引入新功能,来帮助更简单的方法来处理异步编程,帮助我们远离回调地狱。 Promise是下边要讲的Generator/yield与async/await的基础,希望你已...

    leon 评论0 收藏0
  • 【面试篇】寒冬求职季之你必须要懂的原生JS(中)

    摘要:如果你还没读过上篇上篇和中篇并无依赖关系,您可以读过本文之后再阅读上篇,可戳面试篇寒冬求职季之你必须要懂的原生上小姐姐花了近百个小时才完成这篇文章,篇幅较长,希望大家阅读时多花点耐心,力求真正的掌握相关知识点。 互联网寒冬之际,各大公司都缩减了HC,甚至是采取了裁员措施,在这样的大环境之下,想要获得一份更好的工作,必然需要付出更多的努力。 一年前,也许你搞清楚闭包,this,原型链,就能获得...

    andycall 评论0 收藏0
  • 【面试篇】寒冬求职季之你必须要懂的原生JS(中)

    摘要:如果你还没读过上篇上篇和中篇并无依赖关系,您可以读过本文之后再阅读上篇,可戳面试篇寒冬求职季之你必须要懂的原生上小姐姐花了近百个小时才完成这篇文章,篇幅较长,希望大家阅读时多花点耐心,力求真正的掌握相关知识点。 互联网寒冬之际,各大公司都缩减了HC,甚至是采取了裁员措施,在这样的大环境之下,想要获得一份更好的工作,必然需要付出更多的努力。 一年前,也许你搞清楚闭包,this,原型链,就...

    Mike617 评论0 收藏0

发表评论

0条评论

sevi_stuo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<