ES6相關內容

對象的屬性操作

有四個操作會忽略enumerablefalse的屬性

  • for..in循環(huán):只遍歷對象自身和繼承的可枚舉屬性
  • Object.keys():返回對象自身可枚舉的屬性鍵名
  • JSON.stringify():只串行化自身可枚舉的屬性
  • Object.assign():忽略enumerablefalse的屬性,只拷貝自身的可枚舉屬性

可枚舉(enumerable),最初引入的目的是為了讓某些屬性可以規(guī)避for..in操作,不然所有內部屬性和方法都會被遍歷。如對象原型的toString方法,以及數(shù)組的length屬性。而且只有for..in方法能遍歷到繼承的屬性,其他都不行

ES6的屬性遍歷方法
  1. for..in
    for..in循環(huán)遍歷對象自身和繼承的可枚舉屬性(不含Symbol屬性)
  2. Object,keys(obj)
    object.keys 返回一個數(shù)組,包含對象自身(不含繼承)的所有可枚舉屬性(不含Symbol屬性)
  3. Objcet.getOwnPropertyNames(obj)
    Objcet.getOwnPropertyNames(obj)返回一個數(shù)組,包含對象自身的所有屬性(不含Symbol屬性,但是包括不可枚舉屬性)的鍵名
  4. Objcet.getOwnPropertySymbols(obj)
    Objcet.getOwnPropertySymbols(obj)返回一個數(shù)組,包含對象自身的所有Symbol屬性的鍵名
  5. Reflect.oweKeys(obj)
    Reflect.oweKeys(obj)返回一個數(shù)組,包含對象自身所有的鍵名,不管是Symbol或字符串,也不管是否可枚舉

以上5種方法遍歷對象的鍵名,遵守一下規(guī)則

  • 首先遍歷數(shù)值鍵,按照數(shù)值升序排列
  • 其次遍歷所有字符串鍵,按照加入時間升序排列
  • 最后遍歷Symbol鍵,按照加入時間升序排列
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

對象新方法Object.assign

Object.assign用來將源對象所有可枚舉屬性,復制到目標對象中。至少需要兩個對象作為參數(shù),第一個是目標對象,后面的都是源對象

let obj1 = { a: 1 }
let obj2 = { b: 2 }
let obj3 = { c: 3 }
Object.assign(obj1, obj2, obj3)
obj1
// { a: 1 , b: 2, c: 3 } 

PS: 如果目標對象跟源對象有同名屬性,或多個源對象有同名屬性,則后面的屬性會覆蓋前面的屬性

var obj1 = { a: 1, b:1 };
var obj2 = { b: 2, c:2 };
var obj3 = { c: 3 }; 
Object.assign(obj1, obj2, obj3)
obj1
// { a: 1 , b: 2, c: 3 } 

如果目標對象不是對象參數(shù)的話

  • 如果是首參數(shù),那么會將其轉為對象
Object.assign(2)
Number {[[PrimitiveValue]]: 2}
__proto__: Number
[[PrimitiveValue]]: 2

typeof(Object.assign(2))
// "object"

實際上的過程如下

var a = Object.assign(2)

undefined
a.__proto__
// Number {
//constructor: ? Number(), 
//toExponential: ?,
//toFixed: ?, toPrecision, .....
//}
a.__proto__.__proto__
//{constructor: ?, __defineGetter__: ?, __defineSetter__: ?, hasOwnProperty: ?, __lookupGetter__: ?, …}
//constructor : ? Object()
//}

實際上是看非對象參數(shù)能否有對應的類進行實例,比如2是Number型,能由Number進行實例化得到,就相當于Number實例化了一個2的對象,而Number繼承了Object,就相當于轉化成對象了,對應也可以用在Boolean,String類型

注意:但是Null和undefined是無法轉為對象,沒有對應的類給其實例化,所以他們作為首參數(shù)的話,會報錯

var c = Object.assign(null)
var c = Object.assign(undefined)
//VM337:1 Uncaught TypeError: Cannot convert undefined or null to object
  //  at Function.assign (<anonymous>)
    //at <anonymous>:1:16
  • 非對象不出現(xiàn)在首參數(shù)

注意:如果非對象參數(shù)出現(xiàn)在源對象的位置(即非首參數(shù)),那么處理規(guī)則有些不同。參數(shù)都會轉成對象,如果無法轉成對象,那就跳過,不會報錯。但是除了字符串會以數(shù)組形式拷貝進目標對象,其他值(這里說的是基本數(shù)據(jù)類型)不會產(chǎn)生效果

var a = Object.assign( {a:1}, 2) 
var b = Object.assign( {a:1}, '123') 
var c = Object.assign({a:1}, true)
var d = Object.assign({a:1}, null)
var e = Object.assign({a:1}, undefined)
// a  {a: 1}
// b  {0: "1", 1: "2", 2: "3", a: 1}
// c  {a: 1}
// d  {a: 1}
// e  {a: 1}

Object.assign只拷貝自身屬性,不可枚舉屬性(enumerable為false)和繼承的屬性都不拷貝

var a = Object.assign({ dwb: 'qkf'})
Object.defineProperty(a, 'zmf' , {
    enumerable: false,
    value: 'zmf'
})
// a  {dwb: "qkf", zmf: "zmf"}
var b = Object.assign({},a)
// b  {dwb: "qkf"}

class B {
}
B.prototype.zmf = 'zmf'
var b = new B()
// b.zmf  "zmf"
b.dwb = 'qkf'
// b 
B {dwb: "qkf"}
    dwb : "qkf"
    __proto__ : 
        zmf : "zmf"
        constructor : class B
        __proto__ : Object

注意: Object.assign可以用來處理數(shù)組,但會把數(shù)組視為對象

Object.assign([1, 2, 3], [4, 5])
// [4,5,3]

其中,4會覆蓋,5會覆蓋2,因為它們在數(shù)組的同一位置,對應位置覆蓋,數(shù)組其實就是特殊的排列對象,只不過是有序的而已

Object.assign方法實行的是淺拷貝,而不是深拷貝。也就是說,如果源對象某個屬性的值是對象,那么目標對象拷貝得到的是這個對象的引用

var object1 = { a: { b: 1 } }; 
var object2 = Object.assign({}, object1);
object1.a.b = 2;
console.log(object2.a.b);
// 2 

Object.assign只能將屬性值進行賦值,如果屬性值是一個get(取值函數(shù)),那么會先求值,再進行賦值

// 源對象
const source = {
   //屬性是取值函數(shù)
   get foo(){return 1}
};
//目標對象
const target = {};
Object.assign(target,source);
//{foo ; 1}  此時foo的值是get函數(shù)的求值結果 

常見用途

1. 為對象添加屬性

class Point{
    constructor(x,y){
        Object.assign(this, {x,y})
    }
}

上面的方法可以為對象Point類的實例對象添加屬性x和屬性y

2. 為對象添加方法

// 方法也是對象
// 將兩個方法添加到類的原型對象上
// 類的實例會有這兩個方法
Object.assign(SomeClass.prototype,{
    someMethod(arg1,arg2){...},
    anotherMethod(){...}
});

3. 克隆對象
//克隆對象的方法

function clone(origin){
    //獲取origin的原型對象
    let originProto = Obejct.getPrototypeOf(origin);
    //根據(jù)原型對象,創(chuàng)建新的空對象,再assign
    return Object.assign(Object.create(originProto),origin);
} 

4. 為屬性指定默認值

    // 默認值對象
    const DEFAULTS = {
       logLevel : 0,
       outputFormat : 'html'
    };
     
    // 利用assign同名屬性會覆蓋的特性,指定默認值,如果options里有新值的話,會覆蓋掉默認值
    function processContent(options){
       options = Object.assign({},DEFAULTS,options);
       console.log(options);
       //...
    }

處于assign淺拷貝的顧慮,DEFAULTS對象和options對象此時的值最好都是簡單類型的值,否則函數(shù)會失效。

    const DEFAULTS = {
      url: {
        host: 'example.com',
        port: 7070
      },
    };
     
    processContent({ url: {port: 8000} })
    // {
    //   url: {port: 8000}
    // }

上面的代碼,由于url是對象類型,所以默認值的url被覆蓋掉了,但是內部缺少了host屬性,形成了一個bug。


Super關鍵字

super關鍵字指向當前對象的原型對象,在ES6做繼承的時候很有用,但是只能用在對象的方法中,在其他地方會報錯

// 報錯
const obj = {
  foo: super.foo
}

// 報錯
const obj = {
  foo: () => super.foo
}

// 報錯
const obj = {
  foo: function () {
    return super.foo
  }
}

Class類

傳統(tǒng)生成實例對象是通過構造函數(shù),如

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2); 

而在ES6中,引入Class(類)的概念,作為對象模板,通過class關鍵字,可以定義類

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

上述定義一個“類”,里面有一個constructor方法,這就是構造方法,用于定義初始化變量,this指向實例對象。上述ES5的構造函數(shù)Point,對應ES6Point類的構造方法

ES6的“類”Class,可以看作構造函數(shù)的另一種寫法

class Point {
    // ...
}
typeof Point  // 'function'
Point === Point.prototype.constructor //true

即,類的數(shù)據(jù)類型就是函數(shù),類本身就指向構造函數(shù)
當然使用也直接對類使用new命令,與構造函數(shù)用法一致

class Bar {
  doStuff() {
    console.log('stuff');
  }
}

var b = new Bar();
b.doStuff() // "stuff"

構造函數(shù)的prototype屬性,在 ES6 的“類”上面繼續(xù)存在。事實上,類的所有方法都定義在類的prototype屬性上面。

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

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

類的方法都是定義在prototype對象上面,所有類的方法可以添加在prototype對象上。Object.assign可以很方便一次向類添加多個方法

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

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

關于Object.assign的詳細用法,下面能介紹到

類內部所有定義的方法,都是不可枚舉的(non-enumerable)

class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

上述中,toStringPoint類內部定義的方法,是不可枚舉的,跟ES5的行為不一致

var Point = function (x, y) {
  // ...
};

Point.prototype.toString = function() {
  // ...
};

Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

ES5中prototype寫的方法是可枚舉的

問題,為什么Class類的方法是不可枚舉?

constructor
一個類必須有constructor方法,沒有定義的話會默認添加一個空的constructor方法。該方法默認返回實例對象(即this),如果返回其他對象,那這個實例對象就不是該類的對象了,也很好理解
由于constructor方法對應的是構造函數(shù),返回的實例都不是該構造函數(shù)了,所以肯定也不同了

class Foo {
  constructor() {
    return Object.create(null);
    //這里本來是return this,返回Foo類實例對象的,但是返回了一個新建的空對象,所以就不是該類的實例了
  }
}

new Foo() instanceof Foo
// false

與 ES5 一樣,實例的屬性除非顯式定義在其本身(即定義在this對象上),否則都是定義在原型上(即定義在class上)

//定義類
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }

}

var point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true

xy都是實例對象point自身的屬性(定義在this變量上的)
實際上對類實例化的化,會自動調用類的constructor的方法,進行實例的初始化變量之類的操作,最后返回實例的this

==類不存在變量提升,與繼承有關,ES5函數(shù)會進行變量提升==

靜態(tài)方法
類相當于實例原型,類中定義的方法,相當于原型上的方法,都會被實例繼承,但如果在類的方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是通過類來調用,成為靜態(tài)方法

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

Foo類的classMethod方法前有static關鍵字,表明該方法是一個靜態(tài)方法,可以直接在Foo類上調用(Foo.classMethod()),而不是在Foo類的實例上調用。如果在實例上調用靜態(tài)方法,會拋出一個錯誤,表示不存在該方法。

所以,如果靜態(tài)方法包含this關鍵字,this指向該類,而不是實例

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

Foo.bar() // hello

父類的靜態(tài)方法,可以被子類繼承

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

上面代碼中,父類Foo有一個靜態(tài)方法,子類Bar可以調用這個方法。

靜態(tài)方法也是可以從super對象上調用的。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"

Super關鍵字可以說是超類,指向父類對象

問題:靜態(tài)方法是怎么做到的?

私有方法和私有屬性
只能在類內部訪問的方法和屬性,外部不能訪問。這是常見需求,有利于代碼封裝,但ES6不提供,只能變通模擬實現(xiàn)

  1. 將私有方法移出模塊,再用call將其上下文更改
 class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}

上面代碼中,foo是公開方法,內部調用了bar.call(this, baz)。這使得bar實際上成為了當前模塊的私有方法。

還有一種方法是利用Symbol值的唯一性,將私有方法的名字命名為一個Symbol值。

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{

  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};

上面代碼中,bar和snaf都是Symbol值,一般情況下無法獲取到它們,因此達到了私有方法和私有屬性的效果。但是也不是絕對不行,Reflect.ownKeys()依然可以拿到它們。(關于SymbolReflect下面說)

const inst = new myClass();

Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]

上面代碼中,Symbol 值的屬性名依然可以從類的外部拿到

Class繼承

可以通過extends關鍵字實現(xiàn)繼承

class Point{
    
}
class ColorPoint extends Point {
    
}

ColorPoint通過extends關鍵字,繼承了Point類的所有屬性和方法

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方法和toString方法之中,都出現(xiàn)了super關鍵字,它在這里表示父類的構造函數(shù),用來新建父類的this對象。

子類必須在constructor方法中調用super方法,否則新建實例時會報錯。這是因為子類自己的this對象,必須先通過父類的構造函數(shù)完成塑造,得到與父類同樣的實例屬性和方法,然后再對其進行加工,加上子類自己的實例屬性和方法。如果不調用super方法,子類就得不到this對象。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正確
  }
}

上面代碼中,子類的constructor方法沒有調用super之前,就使用this關鍵字,結果報錯,而放在super方法之后就是正確的。

let cp = new ColorPoint(25, 8, 'green');

cp instanceof ColorPoint // true
cp instanceof Point // true

上面代碼中,實例對象cp同時是ColorPointPoint兩個類的實例,這與 ES5 的行為完全一致。
最后,父類的靜態(tài)方法,也會被子類繼承。

 class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world

ES5 的繼承,實質是先創(chuàng)造子類的實例對象this,然后再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機制完全不同,實質是先將父類實例對象的屬性和方法,加到this上面(所以必須先調用super方法),然后再用子類的構造函數(shù)修改this。

Object.getPrototypeOf()
Object.getPrototypeOf方法可以用來從子類上獲取父類

Object.getPrototypeOf(ColorPoint) === Point
// true

因此,可以使用這個方法判斷,一個類是否繼承了另一個類。

Super關鍵字
super這個關鍵字,既可以當作函數(shù)使用,也可以當作對象使用。在這兩種情況下,它的用法完全不同。

  1. super作為函數(shù)調用,代表父類的構造函數(shù)。ES6規(guī)定,子類的構造函數(shù)必須執(zhí)行一次super函數(shù)
class A {}

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

注意,super雖然代表了父類A的構造函數(shù),但是返回的是子類B的實例,即super內部的this指的是B的實例,因此super()在這里相當于A.prototype.constructor.call(this)

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

new.target指向當前正在執(zhí)行的函數(shù),可以看到,在super()執(zhí)行時,它指向的是子類B的構造函數(shù),而不是父類A的構造函數(shù)。也就是說,super()內部的this指向的是B
作為函數(shù)時,super()只能用在子類的構造函數(shù)之中,用在其他地方就會報錯

class A {}

class B extends A {
  m() {
    super(); // 報錯
  }
}
  1. super作為對象時,在普通方法時,指向父類的原型對象;在靜態(tài)方法時指向父類。
class A {
    p() {
        return 2;
    }
}
class B extends A {
    constructor(){
        super()
        console.log(super.p()) //2
    }
}

上面代碼中,子類B當中的super.p(),就是將super當作一個對象使用。這時,super在普通方法之中,指向A.prototype,所以super.p()就相當于A.prototype.p()。

但是由于super指向父類的原型對象,所以定義在父類實例上的方法或者屬性,是無法通過super調用的

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

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

p是父類A實例的屬性,super.p引用不到它
如果屬性是定義在父類的原型對象上,super就可以取到

class A {
    
}
A.prototype.x = 2
class B extends A {
    constructor(){
        super()
        console.log(super.x) //2
    }
}
let b = new B()

上面代碼中,屬性x是定義在A.prototype上面的,所以super.x可以取到它的值。

ES6規(guī)定,在子類普通方法中通過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()內部的this指向子類B的實例,導致輸出的是2,而不是1。也就是說,實際上執(zhí)行的是super.print.call(this)。

由于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
    }
    
}

上面代碼中,super.x賦值為3,給屬性賦值為3的話,等同于對this.x賦值為3,而當讀取super.x時,讀的是A.prototype.x,所以返回undefined。(也就是說,'讀'屬性的時候是父類原型,'寫'屬性的時候是實例)

如果super作為對象,用在靜態(tài)方法中,super指向父類,而不是父類的原型對象

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

上面代碼中,super在靜態(tài)方法之中指向父類,在普通方法之中指向父類的原型對象。

另外,在子類的靜態(tài)方法中通過super調用父類的方法時,方法內部的this指向當前的子類,而不是子類的實例

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

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

B.x = 3;
B.m() // 3
let b = new B()
b.x   // 2 
b.m() // b.m is not a function

因為實例都獲取不了子類的靜態(tài)方法,所以也很好理解

注意: 使用super的時候,要顯式指定是作為函數(shù),還是對象使用,否則會報錯(函數(shù)就加括號(),對象不用,但后面要加屬性)

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // 報錯
  }
}

類的 prototype 屬性和 __proto__屬性

ES5中,每個對象都有__proto__屬性,指向對應構造函數(shù)的prototype屬性。Class 作為構造函數(shù)的語法糖,同時有prototype屬性和__proto__,因此存在兩條繼承鏈

(1) 子類的__proto__屬性,表示構造函數(shù)的繼承,總是指向父類
(2) 子類prototype屬性的__proto__屬性,表示方法的繼承,總是指向父類的prototype屬性

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代碼中,子類B的__proto__屬性指向父類,子類B的prototype屬性的__proto__屬性指向父類A的prototype屬性

這樣的結果是因為,類的繼承按照下面模式實現(xiàn)的

class A {
    
}
class B {
    
}
// B 是實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype)

// B 繼承 A 的靜態(tài)屬性
Object.setPrototypeOf(B, A)

let b = new B()

Object.setPrototypeOf方法的實現(xiàn)。

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;

兩條繼承鏈,可以這么理解: 作為一個對象,子類(B)的原型(__proto__屬性)是父類(A);作為一個構造函數(shù),子類(B)的原型對象(prototype屬性)是父類的原型對象(prototype屬性)的實例

即對象對應對象,原型對應原型

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

實例的__proto__屬性

子類實例的__proto__屬性,指向子類,其實就一個原型鏈的問題。不作多解釋

問題:畫出原型鏈的表達圖!


Set和Map數(shù)據(jù)結構

ES6新的數(shù)據(jù)結構Set。類似數(shù)組,但是成員值都是唯一的
Set本身是一個構造函數(shù),生成Set數(shù)據(jù)結構

const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
  console.log(i);
}
// 2 3 5 4

add()方法可以向Set結構加入成員,結果顯示Set不會添加重復的值,這個方法可以用來進行數(shù)組去重
如下,Set函數(shù)接受一個數(shù)組(或者其他具有iterable接口的其他數(shù)據(jù)結構)作為參數(shù),用來初始化

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

const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
 
const set = new Set(document.querySelectorAll('div'));
set.size // 56

// 類似于
const set = new Set();
document
 .querySelectorAll('div')
 .forEach(div => set.add(div));
set.size // 56

而將Set結構轉換成數(shù)組的話可以用[...Set]或者用Array.form,(...是擴展運算符,內部使用for...of循環(huán),一個針對iterator(可遍歷)結構的循環(huán))

// 去除數(shù)組的重復成員
[...new Set(array)] 

const items = new Set([1,2,3,4,5,5,6,6])
const array = Array.from(items)
console.log(array)
//[1, 2, 3, 4, 5, 6]

當然也可以去除字符串里的重復字符

[...new Set('ababbc')].join('')
// "abc"

就本身來說,我自己項目中出現(xiàn)過,比如用戶進行前端添加人員(后臺接受的是人員ID數(shù)組,但在前端得顯示添加的人員姓名),根據(jù)數(shù)據(jù)結構不同有幾個不同方法

  1. 初始化兩個數(shù)組一個人員ID數(shù)組,一個人員Name數(shù)組,當用戶點擊添加的時候進行任一判斷存不存在數(shù)組中,不存在就push
  2. 初始化一個對象,人員ID是鍵,值為人員Name,進行添加的時候用人員ID作為屬性判斷值是不是為空,空就添加
  3. 初始化一個Set結構,當用戶進行添加重復人員的時候,Set結構不會做出相應,在用戶界面看到的一直都會是去重后的人員數(shù)組

==注意:第1種方法解決的話每次添加需要去循環(huán)一下數(shù)組,時間復雜度是O(n),第2種的話直接利用對象的鍵去查找有無值,性能上會比第一種好很多,第3種的話就省去了判斷這個環(huán)節(jié),去取值的話也直接用鍵去取就行了,性能也比較高==

Set實例屬性和方法
  • Set.prototype.constructor:構造函數(shù),默認是Set函數(shù)
  • Set.prototype.size:返回Set實例的成員總數(shù)

Set實例的方法分為兩大類:操作方法(用于操作數(shù)據(jù))和遍歷方法(用于遍歷成員)

操作方法:

  • Set.prototype.add(value): 添加某個值,返回Set結構本身
  • Set.prototype.delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功
  • Set.prototype.has(value):返回一個布爾值,表示該值是否為Set的成員
  • Set.prototype.clear():清除所有成員,沒有返回值
s.add(1).add(2).add(2);
// 注意2被加入了兩次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

遍歷方法

  • Set.prototype.keys(): 返回鍵名的遍歷器
  • Set.prototype.values(): 返回鍵值的遍歷器
  • Set.prototype.entries(): 返回鍵值對的遍歷器
  • Set.prototype.forEach(): 使用回調函數(shù)遍歷每個成員

keys方法、values方法、entries方法返回的都是遍歷器對象(詳見《Iterator 對象》一章)。由于 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),所以keys方法和values方法的行為完全一致。

WeakSet

結構與Set類似,也是不重復值的集合。區(qū)別在于

  1. WeakSet成員只能是對象,其他都不行,如
const ws = new WeakSet() 
ws.add(1)
// TypeError: WeakSet value must be an object, got the number 1
ws.add(Symbol())
//TypeError: WeakSet value must be an object, got Symbol()
  1. WeakSet中的對象都是弱引用,即垃圾回收機制不考慮WeakSet對該對象的引用,就是說其他對象不引用該對象的話,垃圾回收機制就會自動回收該對象所占用的內存,不考慮該對象存在于WeakSet之中,如
var obj = { a: { 1: 1}, b: {2:2} };
var ws = new WeakSet();
ws.add(obj)
// console.log(ws)
// WeakSet(1)
//   <entries> 
//      0 : { a: {1:1}, b:{2:2} }
obj.a = null 
obj.b = null
// console.log(ws)
// WeakSet(1)
//   <entries> 
//      0 : { a: null, b:null }

WeakSet可以接受一個數(shù)組或類數(shù)組的成員對象作為參數(shù),如

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

a數(shù)組有兩個成員,也是數(shù)組,數(shù)組也屬于對象,則成員對象會自動成為WeakSet成員
而如果數(shù)組成員不是對象,就會出錯,就像

const b = [3,4]
const ws = new WeakSet(b)
// Uncaught TypeError: Invalid value used in weak set(…)

WeakSet有三個方法

  • WeakSet.prototype.add(value): 添加新成員
  • WeakSet.prototype.delete(value): 刪除成員
  • WeakSet.prototype.has(value): 返回是否在實例中的布爾值

由于WeakSet是弱引用,所以沒有遍歷方法

Map

傳統(tǒng)JS的對象,本質上是鍵值對(Hash)的集合,傳統(tǒng)只能用字符串當作鍵,這給它使用帶來限制,如

const data = {};
const element = document.getElementById('myDiv');

data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"

原意是想把DOM節(jié)點存為對象的鍵,但對象只接受字符串作為鍵名,所以element被自動轉為字符串[object HTMLDivElement]
Map數(shù)據(jù)結構的"鍵"可以接受各種類型的值(包括對象),是一種“值-值”對應的感覺。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

如果Map接受一個數(shù)組的話

const map = new Map([
  ['name', '張三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "張三"
map.has('title') // true
map.get('title') // "Author"

實際上,Map構造函數(shù)接受數(shù)組作為參數(shù)的時候,執(zhí)行的是下面的算法

const items = [
  ['name', '張三'],
  ['title', 'Author']
];

const map = new Map();

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

事實上,不僅僅是數(shù)組,任何具有Iterator 接口、且每個成員都是一個雙元素的數(shù)組的數(shù)據(jù)結構(詳見《Iterator 對象》一章)都可以當作Map構造函數(shù)的參數(shù)。這就是說,SetMap都可以用來生成新的Map。

const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

同樣的值的兩個實例,在 Map 結構中被視為兩個鍵。

const map = new Map();

const k1 = ['a'];
const k2 = ['a'];

map
.set(k1, 111)
.set(k2, 222);

map.get(k1) // 111
map.get(k2) // 222

上面代碼中,變量k1k2的值是一樣的,但是它們在 Map 結構中被視為兩個鍵。
可以考慮到為變量開辟內存的時候,新變量開辟新的空間,就算是一樣的值都好,引用都是不一樣的,Map的鍵實際上與變量內存地址綁定,只要內存地址不一樣,就視為兩個鍵。
但是Map鍵是簡單類型的值的話(數(shù)字,字符串,布爾值),兩個值嚴格(嚴格的意思就是三等號為true的情況,也就是類型都要一樣)相等就視為一個鍵了,當然了引用類型就上述情況了。
注意: NaN雖然不嚴格相等,但是Map仍視為同一個鍵

let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

屬性和操作方法

  1. size屬性
    size屬性返回Map結構的成員總數(shù)
  2. Map.prototype.set(key, value)
    該方法設置鍵名key的值為value,返回整個Map結構,若鍵key已經(jīng)有值那就更新鍵值,由于是返回整個Map對象,所以可以用鏈式寫法
let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

  1. Map.prototype.get(key)
    get讀取key的鍵值,找不到的話返回undefinded
  2. Map.prototype.has(key)
    has方法返回一個布爾值,表示該鍵是否在當前Map對象中
  3. Map.prototype.delete(key)
    刪除某個鍵,返回成功與否的布爾值
  4. Map.prototype.clear()
    清除所有成員,沒有返回值

遍歷方法

  • Map.prototype.keys(): 返回鍵名遍歷器
  • Map.prototype.values(): 返回鍵值遍歷器
  • Map.prototype.entries(): 返回鍵值遍歷器
  • Map.prototype.forEach(): 遍歷Map所有成員

轉換成數(shù)組的話用...很方便

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one', 'two', 'three']

[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

此外,Map還有一個forEach方法,與數(shù)組的forEach方法類似,也可以實現(xiàn)遍歷。

map.forEach((value, key, map) => {
    console.log("Key: %s, value: %s", key,value) 
})

forEach方法還可以接受第二個參數(shù),用來綁定this

const reporter = {
  report: function(key, value) {
    console.log("Key: %s, Value: %s", key, value);
  }
};

map.forEach(function(value, key, map) {
  this.report(key, value);
}, reporter);

第二個參數(shù),指定對象上下文,傳入是哪個對象變量,this指向reporter

Map與其他數(shù)據(jù)結構互相轉換

  1. Map轉為數(shù)組
    前面提過,最方便的就是使用擴展運算符(...
const myMap = new Map()
    .set(true, 7)
    .set({foo: 3}, ['abc'])
[...myMap]
//[ [true, 7], [{foo: 3}, ['abc'] ] ]
  1. 數(shù)組轉為Map
    將數(shù)組傳入Map構造函數(shù),就可以轉為Map
    new Map([
        [true, 7],
        [ {foo: 3}, ['abc'] ]
    ])
    // Map {
        true => 7
        Object {foo: 3} => ['abc']
    }
  1. Map轉為對象
    如果所有Map的鍵都是字符串,它可以無損地轉為對象
    function strMapToObj(strMap){
        let obj = Object.create(null)
        for(let [k,v] of strMap){
            obj[k] = v
        }
        return obj
    }
    const myMap = new Map()
        .set('yes', true)
        .set('no', false)
    strMapToObj(myMap)
    // { yes: true, no: false } 

如果有非字符串的鍵名,那么這個鍵名會先轉字符串,再作為對象的鍵名

    function strMapToObj(strMap){
        let obj = Object.create(null)
        for(let [k,v] of strMap){
            obj[k] = v
        }
        return obj
    }
    const myMap = new Map()
        .set({ foo: 3 }, true)
        .set('no', false)
    strMapToObj(myMap)
    // { [object Object] : true, no: false } 
    const myMaps = new Map()
        .set([ 'abc', 'bcs' ], true)
        .set('no', false)
    strMapToObj(myMaps)
    // { abc,bcs : true no : false }

不同的是,js中萬物都是繼承Object的,所以轉成字符串是調用toString()方法,對象那就是[object Object],數(shù)組的話直接拼接數(shù)組的值

  1. 對象轉為Map
function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
  1. Map轉為JSON
    要區(qū)分兩種情況,一種是Map的鍵名都是字符串的時候,這時可以選擇轉為對象JSON
function strMapToJson(strMap){
    return JSON.stringify(strMapToObj(strMap)
}

let myMap = new Map().set('yes', true).set('no', false)
strMapToJson(myMap)
// '{"yes":true,"no":false}'

另一種情況是,Map的鍵名有非字符串,這時候可以選擇轉為數(shù)組JSON

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
  1. JSON轉為Map
    JSON轉為Map,正常情況下,所有鍵名都是字符串
function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}

但是,有一種特殊情況,整個 JSON 就是一個數(shù)組,且每個數(shù)組成員本身,又是一個有兩個成員的數(shù)組。這時,它可以一一對應地轉為 Map。這往往是 Map 轉為數(shù)組 JSON 的逆操作。

function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}

jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}

其實就相當于逆轉換,吃透一邊就可以理解另外一邊了

WeakMap

類似Map,跟WeakSet差不多,不詳細講了
WeakMap最典型場合就是用DOM節(jié)點作為鍵名去保存,因為當DOM節(jié)點刪除的時候,狀態(tài)也會自動消失,不用擔心內存泄漏

總結

SetWeakSet,MapWeakMap一個是類數(shù)組,一個類對象,可以說是數(shù)組和對象的擴展應用把,比數(shù)組和對象有更強大的功能,也可以互相轉換成數(shù)組和對象


Promise對象

簡單來說,是一個容器,保存著某個未來才會結束的事件(通常是一個異步操作)的結果,比如一個請求,請求需要時間,等到響應后才會返回結果。語法上說,Promise是一個對象,它可以獲取異步操作的消息。有了Promise對象,可以將異步操作以同步操作流程表達出來,避免了層層嵌套的回調。

Promise對象會將所有執(zhí)行函數(shù)放在then之后執(zhí)行,而不管你的操作是不是異步的,就是說同步的,經(jīng)過Promise包裝后就會變成異步執(zhí)行

const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now

那么有沒有一種方法,讓同步函數(shù)同步執(zhí)行,異步函數(shù)異步執(zhí)行,并且讓它們具有統(tǒng)一的 API 呢?回答是可以的,并且還有兩種寫法。

第一種寫法是用async函數(shù)來寫

const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next

上面代碼中,第二行是一個立即執(zhí)行的匿名函數(shù),會立即執(zhí)行里面的async函數(shù),因此如果f是同步的,就會得到同步的結果;如果f是異步的,就可以用then指定下一步,就像下面的寫法。

(async () => f())()
.then(...)

需要注意的是,async () => f()會吃掉f()拋出的錯誤。所以,如果想捕獲錯誤,要使用promise.catch方法。

(async () => f())()
.then(...)
.catch(...)

Generator(生成器)函數(shù)

是ES6提供的一種異步編程解決方案

從語法上,可以理解成一個狀態(tài)機,封裝了多個內部狀態(tài)
執(zhí)行Generator函數(shù)會返回一個遍歷器對象,也就是說,Generator函數(shù)除了狀態(tài)機,還是一個遍歷器對象生成函數(shù)。返回的都是遍歷器對象,可以依次遍歷Generator函數(shù)內部的每一個狀態(tài)

形式上,Generator 函數(shù)是一個普通函數(shù),但是有兩個特征。一是,function關鍵字與函數(shù)名之間有一個星號;二是,函數(shù)體內部使用yield表達式,定義不同的內部狀態(tài)(yield在英語里的意思就是“產(chǎn)出”)。

function* helloWorldGenerator(){
    yield 'hello'
    yield 'world'
    return 'ending'
}
var hw = helloWorldGenerator() 

調用Generator函數(shù)后,該函數(shù)并不執(zhí)行,返回的是一個指向內部狀態(tài)的指針對象,也就是遍歷器對象
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態(tài)。就是說,每一次調用next方法,內部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行,直到遇到下一個yield表達式(或return語句)為止。換言之,Generator函數(shù)是分段執(zhí)行,yield表達式是暫停執(zhí)行的標記,而next可以恢復執(zhí)行

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

總結一下,調用 Generator 函數(shù),返回一個遍歷器對象,代表 Generator 函數(shù)的內部指針。以后,每次調用遍歷器對象的next方法,就會返回一個有著valuedone兩個屬性的對象value屬性表示當前的內部狀態(tài)的值,是yield表達式后面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束

yield表達式

yield表達式相當于一個暫停標志,遇到它,便返回一個對象,value值就是yield后面帶的值,繼續(xù)調用next往下執(zhí)行到下一個yield表達式,沒有的話就一直運行到結束,直到return,將return后的值作為value,如果沒有return,返回對象的value屬性值就是undefined

function* helloWorldGenerator() { 
    console.log('111');
    yield 'hello'; 
    yield 'world';
    console.log('end')
}

var hw = helloWorldGenerator();

hw.next()
// 111 
// Object { value: "hello", done: false }

hw.next()
// Object { value: "world", done: false }

hw.next()
// 'end'
// Object { value: undefined, done: true }

由于這個功能,等于給javascript提供了手動的惰性求值(Lazy Evaluation)語法功能

注意:yield表達式只能用在Generator函數(shù)中

如果yield表達式放在另一個表達式中,要放圓括號

function* demo(){
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError
  console.log('Hello' + (yield));
  console.log('Hello' + (yield 123));
}

var dm = demo()
dm.next()
// Object { value: undefined, done: false }
dm.next()
// Helloundefined 
// Object { value: 123, done: false }
dm.next()
// Helloundefined 
// Object { value: undefined, done: true }
dm.next()
// Object { value: undefined, done: true }

yield表達式用作函數(shù)參數(shù)或放在賦值表達式的右邊可以不加括號

function foo(a,b){
  return a+b;
}

function* memo(){
  foo(yield 'a',yield'b')
  var input = yield
 }
 var mm = memo()
 mm.next()
 // Object { value: "a", done: false }
 mm.next()
 // Object { value: "b", done: false }
 mm.next()
 // Object { value: undefined, done: false }
 mm.next()
 // Object { value: undefined, done: true }

Iterator 接口的關系
任何一個對的Symbol.iterator方法,等于該對象的遍歷器生成函數(shù),調用該函數(shù)會返回該對象的一個遍歷器對象

由于Generator函數(shù)就是遍歷器生成器,可以將Generator函數(shù)賦值給對象的iterator屬性,從而使該對象具有Iterator接口

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

上面代碼中,Generator 函數(shù)賦值給Symbol.iterator屬性,從而使得myIterable對象具有了 Iterator 接口,可以被...運算符遍歷了。

next方法的參數(shù)

yield表達式本身沒有返回值,或者說總是返回undefinednext方法可以帶一個參數(shù),該參數(shù)就會被當作上一個yield表達式的返回值。

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    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 }

上面代碼先定義了一個可以無限運行的 Generator 函數(shù)f,如果next方法沒有參數(shù),每次運行到yield表達式,變量reset的值總是undefined。當next方法帶一個參數(shù)true時,變量reset就被重置為這個參數(shù)(即true),因此i會等于-1,下一輪循環(huán)就會從-1開始遞增

for...of循環(huán)

for...of循環(huán)可以自動遍歷 Generator 函數(shù)運行時生成的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

使用for...of循環(huán),依次顯示 5 個yield表達式的值。這需要注意的是,一旦next方法的返回對象的done屬性為truefor...of循環(huán)就會中止,且不包含該返回對象,所以return語句返回的6,不包括在for...of循環(huán)中

原生的js對象沒有遍歷接口,無法使用for...of循環(huán),通過Generator函數(shù)為它加上這個接口就可以用了。

除了for...of循環(huán)以外,擴展運算符(...)解構賦值Array.from方法內部調用的,都是遍歷器接口。這意味著,它們都可以將 Generator 函數(shù)返回的 Iterator 對象,作為參數(shù)。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 擴展運算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解構賦值
let [x, y] = numbers();
x // 1
y // 2

// for...of 循環(huán)
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

throw()

next(),throw(),return()共同點
next(),throw(),return()這三個方法本質上是同一件事,可以放在一起理解。它們的作用都是讓Generator函數(shù)恢復執(zhí)行,并且使用不同的語句替換yield表達式。

next()是將yield表達式替換成一個值

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相當于將 let result = yield x + y
// 替換成 let result = 1;

上面代碼中,第二個next(1)方法就相當于將yield表達式替換成一個值1。如果next方法沒有參數(shù),就相當于替換成undefined

throw是將yield表達式替換成一個throw語句。

gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 相當于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));

return()是將yield表達式替換成一個return語句

gen.return(2); // Object {value: 2, done: true}
// 相當于將 let result = yield x + y
// 替換成 let result = return 2;

** yield* 表達式**

如果在Generator函數(shù)內部,調用另一個Generator函數(shù)。需要在前者函數(shù)體內部,自己手動完成遍歷

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

function* bar() {
  yield 'x';
  // 手動遍歷 foo()
  for (let i of foo()) {
    console.log(i);
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// x
// a
// b
// y

上面代碼中,foobar都是Generator函數(shù),在bar里面調用foo,需要手動遍歷foo。如果多個Generator函數(shù)嵌套,寫起來就比較麻煩

ES6提供yield*表達式,作為用在在一個Generator函數(shù)中調用另一個Generator函數(shù)

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"

實際上,任何數(shù)據(jù)結構,只要有Iterator接口,都可以被yield*遍歷,也就是說yield*可以遍歷含有遍歷器對象或遍歷器對象接口的對象

yield*后面的Generator函數(shù)沒有return語句時,相當于是for..of的簡寫形式,有return語句的時候,則將這個數(shù)值返回獲取,如果被代理的 Generator 函數(shù)有return語句,那么就可以向代理它的 Generator 函數(shù)返回數(shù)據(jù)。

function* foo() {
  yield 2;
  yield 3;
  return "foo";
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}

var it = bar();

it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

上面代碼在第四次調用next方法的時候,屏幕上會有輸出,這是因為函數(shù)foo的return語句,向函數(shù)bar提供了返回值。

function* genFuncWithReturn() {
  yield 'a';
  yield 'b';
  return 'The result';
}
function* logReturned(genObj) {
  let result = yield* genObj;
  console.log(result);
}

[...logReturned(genFuncWithReturn())]
// The result
// 值為 [ 'a', 'b' ]

上面代碼有兩次遍歷。第一次是擴展運算符遍歷函數(shù)logReturned返回的遍歷器對象,第二次是yield*語句遍歷函數(shù)genFuncWithReturn返回的遍歷器對象。但是,函數(shù)genFuncWithReturnreturn語句的返回值The result,會返回給函數(shù)logReturned內部的result變量,因此會有終端輸出。

而且是先執(zhí)行一次函數(shù)代碼,再執(zhí)行遍歷

yield*可以很方便取出嵌套數(shù)組的所有成員

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

內部運行,如果是數(shù)組的話執(zhí)行if操作,不然就輸出
使用擴展運算符...也是一樣的效果

[...iterTree(tree)] // ["a", "b", "c", "d", "e"]

下面是一個稍微復雜的例子,使用yield*語句遍歷完全二叉樹。

// 下面是二叉樹的構造函數(shù),
// 三個參數(shù)分別是左樹、當前節(jié)點和右樹
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍歷函數(shù)。
// 由于返回的是一個遍歷器,所以要用generator函數(shù)。
// 函數(shù)體內采用遞歸算法,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉樹
function make(array) {
  // 判斷是否為葉節(jié)點
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

Generator 函數(shù)的this

Generator 函數(shù)總是返回一個遍歷器,ES6規(guī)定這個遍歷器是Generator的實例,也繼承了Generator函數(shù)的prototype對象上的方法

function* g() {}

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

let obj = g();

obj instanceof g // true
obj.hello() // 'hi!'

上面代碼表明,Generator 函數(shù)g返回的遍歷器obj,是g的實例,而且繼承了g.prototype。但是,如果把g當作普通的構造函數(shù),并不會生效,因為g返回的總是遍歷器對象,而不是this對象。

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

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

上面代碼中,Generator 函數(shù)g在this對象上面添加了一個屬性a,但是obj對象拿不到這個屬性。

Generator 函數(shù)也不能跟new命令一起用,會報錯,因為不是構造函數(shù)。

function* F() {
  yield this.x = 2;
  yield this.y = 3;
}

new F()
// TypeError: F is not a constructor

問題:能否讓Generator函數(shù)返回一個正常的對象實例,既可以用next方法,又可以獲得正常的this

下面一個變通方法。首先,生成一個空對象,使用call方法綁定Generator函數(shù)內部的this,這樣,構造函數(shù)調用以后,這個空對象就是Generator函數(shù)的實例對象了

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

上面代碼中,首先是F內部的this對象綁定obj對象,然后調用它,返回一個 Iterator 對象。這個對象執(zhí)行三次next方法(因為F內部有兩個yield表達式),完成 F 內部所有代碼的運行。這時,所有內部屬性都綁定在obj對象上了,因此obj對象也就成了F的實例。

上面代碼中,執(zhí)行的是遍歷器對象f,但是生成的對象實例是obj,有沒有辦法將兩個對象統(tǒng)一

一個辦法是將obj換成F.prototype

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

再將F改成構造函數(shù),就可以對它執(zhí)行new命令了

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

Generator 函數(shù)的應用

(1)異步操作的同步化表達

如Ajax,是個典型的異步操作,通過 Generator 函數(shù)部署 Ajax 操作,可以用同步的方式表達。

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(); 

上面代碼的main函數(shù),就是通過 Ajax 操作獲取數(shù)據(jù)??梢钥吹?,除了多了一個yield,它幾乎與同步操作的寫法完全一樣。注意,makeAjaxCall函數(shù)中的next方法,必須加上response參數(shù),因為yield表達式,本身是沒有值的,總是等于undefined

(2)控制流管理

如果有一個多步操作非常耗時,采用回調函數(shù),可能會像下面這樣

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

采用 Promise 改寫上面代碼

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }).catch(Error = > { })
  .done();

上面代碼已經(jīng)把回調函數(shù),改成了直線執(zhí)行的形式,但是加入了大量 Promise 的語法。Generator 函數(shù)可以進一步改善代碼運行流程。

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
} 

然后,使用一個函數(shù),按次序自動執(zhí)行所有步驟。

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函數(shù)未結束,就繼續(xù)調用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

(3)部署Iterator接口
利用Generator函數(shù),可以在任何對象上部署Iterator接口

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

Generator函數(shù)的異步應用


async函數(shù)

使異步操作更加方便,是 Generator 函數(shù)的改進
前文有一個 Generator 函數(shù),依次讀取兩個文件。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

上面代碼的函數(shù)gen可以寫成async函數(shù),就是下面這樣。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async函數(shù)對 Generator 函數(shù)的改進,體現(xiàn)在四點:

  1. 內置執(zhí)行器
    Generator 函數(shù)的執(zhí)行必須依靠執(zhí)行器,或者自行調用next方法, 而async函數(shù)內置執(zhí)行器,與普通函數(shù)一模一樣,只需要一行
asyncReadFile();
  1. 更好的語義
    asyncawait,比起星號和yield,語義更清楚了。async表示函數(shù)里有異步操作,await表示緊跟在后面的表達式需要等待結果。
  1. 更廣的適用性
    async函數(shù)的await命令后面,可以是 Promise 對象和原始類型的值(數(shù)值、字符串和布爾值,但這時會自動轉成立即 resolvedPromise 對象)。

  2. 返回值是 Promise。

async函數(shù)的返回值是 Promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了。你可以用then方法指定下一步的操作。

進一步說,async函數(shù)完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。

基本用法
async 函數(shù)有多種使用形式。

// 函數(shù)聲明
async function foo() {}

// 函數(shù)表達式
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(…);

// 箭頭函數(shù)
const foo = async () => {};

語法

async函數(shù)返回一個 Promise 對象
async函數(shù)內部return語句返回的值,會成為then方法回調函數(shù)的參數(shù)

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

f().then(v => console.log(v))
// "hello world"

上面代碼中,函數(shù)f內部return命令返回的值,會被then方法回調函數(shù)接收到

async函數(shù)內部拋出錯誤,會導致返回的 Promise 對象變?yōu)?code>reject狀態(tài)。拋出的錯誤對象會被catch方法回調函數(shù)接收到

async function f() {
  throw new Error('出錯了');
}

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

await 命令

正常情況下,await命令后面是一個 Promise 對象,返回該對象的結果。如果不是 Promise 對象,就直接返回對應的值

async function f(){ 
    // 等同于
    // return 123
    return await 123;
}
f().then(v => {console.log(v)})
// 123

上面代碼中,await命令的參數(shù)是數(shù)值123,這時等同于return 123。

另一種情況是,await命令后面是一個thenable對象(即定義then方法的對象),那么await會將其等同于 Promise 對象

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(
      () => resolve(Date.now() - startTime),
      this.timeout
    );
  }
}

(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime);
})();
// 1000

上面代碼中,await命令后面是一個Sleep對象的實例。這個實例不是 Promise 對象,但是因為定義了then方法,await會將其視為Promise處理。

這個例子還演示了如何實現(xiàn)休眠效果。JavaScript 一直沒有休眠的語法,但是借助await命令就可以讓程序停頓指定的時間。下面給出了一個簡化的sleep實現(xiàn)。

function sleep(interval) {
  return new Promise(resolve => {
    setTimeout(resolve, interval);
  })
}

// 用法
async function one2FiveInAsync() {
  for(let i = 1; i <= 5; i++) {
    console.log(i);
    await sleep(1000);
  }
}

one2FiveInAsync();

使用注意點
第一點, await命令后面的 Promise 對象,運行結果可能是rejected,所以最好把await命令放在try...catch代碼塊中

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

// 另一種寫法

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

第二點,多個await命令后面的異步操作,如果不存在繼發(fā)關系,最好讓它們同時觸發(fā)。

let foo = await getFoo();
let bar = await getBar();

上面代碼中,getFoogetBar是兩個獨立的異步操作(即互不依賴),被寫成繼發(fā)關系。這樣比較耗時,因為只有getFoo完成以后,才會執(zhí)行getBar,完全可以讓它們同時觸發(fā)。

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

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise; 

上面兩種寫法,getFoo和getBar都是同時觸發(fā),這樣就會縮短程序的執(zhí)行時間。

第三點,await命令只能用在async函數(shù)之中,普通函數(shù)中的話會報錯。

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

  // 報錯
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

上面代碼會報錯,因為await用在普通函數(shù)之中了。但是,如果將forEach方法的參數(shù)改成async函數(shù),也有問題。

function dbFuc(db) { //這里不需要 async
  let docs = [{}, {}, {}];

  // 可能得到錯誤結果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}

上面代碼可能不會正常工作,原因是這時三個db.post操作將是并發(fā)執(zhí)行,也就是同時執(zhí)行,而不是繼發(fā)執(zhí)行。正確的寫法是采用for循環(huán)。

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

  for (let doc of docs) {
    await db.post(doc);
  }
}

如果確實希望多個請求并發(fā)執(zhí)行,可以使用Promise.all方法。當三個請求都會resolved時,下面兩種寫法效果相同。

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

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

// 或者使用下面的寫法

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

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

async 函數(shù)的實現(xiàn)原理

async 函數(shù)的實現(xiàn)原理,就是將 Generator 函數(shù)和自動執(zhí)行器,包裝在一個函數(shù)里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

所有的async函數(shù)都可以寫成上面的第二種形式,其中的spawn函數(shù)就是自動執(zhí)行器。

下面給出spawn函數(shù)的實現(xiàn),基本就是前文自動執(zhí)行器的翻版。

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

與其他異步處理方法的比較

我們通過一個例子,來看 async 函數(shù)與 Promise、Generator 函數(shù)的比較。

假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始后一個。如果當中有一個動畫出錯,就不再往下執(zhí)行,返回上一個成功執(zhí)行的動畫的返回值。

首先是 Promise 的寫法

function chainAnimationsPromise(elem, animations) {

  // 變量ret用來保存上一個動畫的返回值
  let ret = null;

  // 新建一個空的Promise
  let p = Promise.resolve();

  // 使用then方法,添加所有動畫
  for(let anim of animations) {
    p = p.then(function(val) {
      ret = val;
      return anim(elem);
    });
  }

  // 返回一個部署了錯誤捕捉機制的Promise
  return p.catch(function(e) {
    /* 忽略錯誤,繼續(xù)執(zhí)行 */
  }).then(function() {
    return ret;
  });

}

雖然 Promise 的寫法比回調函數(shù)的寫法大大改進,但是一眼看上去,代碼完全都是 Promise 的 API(thencatch等等),操作本身的語義反而不容易看出來。

接著是 Generator 函數(shù)的寫法。

function chainAnimationsGenerator(elem, animations) {

  return spawn(function*() {
    let ret = null;
    try {
      for(let anim of animations) {
        ret = yield anim(elem);
      }
    } catch(e) {
      /* 忽略錯誤,繼續(xù)執(zhí)行 */
    }
    return ret;
  });

}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 本文為阮一峰大神的《ECMAScript 6 入門》的個人版提純! babel babel負責將JS高級語法轉義,...
    Devildi已被占用閱讀 2,122評論 0 4
  • 在此處先列下本篇文章的主要內容 簡介 next方法的參數(shù) for...of循環(huán) Generator.prototy...
    醉生夢死閱讀 1,486評論 3 8
  • 第3章 基本概念 3.1 語法 3.2 關鍵字和保留字 3.3 變量 3.4 數(shù)據(jù)類型 5種簡單數(shù)據(jù)類型:Unde...
    RickCole閱讀 5,489評論 0 21
  • [TOC] 參考阮一峰的ECMAScript 6 入門參考深入淺出ES6 let和const let和const都...
    郭子web閱讀 1,908評論 0 1
  • 簡介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同。本章詳細介紹...
    呼呼哥閱讀 1,134評論 0 4

友情鏈接更多精彩內容