ES6相关内容

对象的属性操作

有四个操作会忽略enumerablefalse的属性

  • for..in循环:只遍历对象自身和继承的可枚举属性
  • Object.keys():返回对象自身可枚举的属性键名
  • JSON.stringify():只串行化自身可枚举的属性
  • Object.assign():忽略enumerablefalse的属性,只拷贝自身的可枚举属性

可枚举(enumerable),最初引入的目的是为了让某些属性可以规避for..in操作,不然所有内部属性和方法都会被遍历。如对象原型的toString方法,以及数组的length属性。而且只有for..in方法能遍历到继承的属性,其他都不行

ES6的属性遍历方法
  1. for..in
    for..in循环遍历对象自身和继承的可枚举属性(不含Symbol属性)
  2. Object,keys(obj)
    object.keys 返回一个数组,包含对象自身(不含继承)的所有可枚举属性(不含Symbol属性)
  3. Objcet.getOwnPropertyNames(obj)
    Objcet.getOwnPropertyNames(obj)返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)的键名
  4. Objcet.getOwnPropertySymbols(obj)
    Objcet.getOwnPropertySymbols(obj)返回一个数组,包含对象自身的所有Symbol属性的键名
  5. Reflect.oweKeys(obj)
    Reflect.oweKeys(obj)返回一个数组,包含对象自身所有的键名,不管是Symbol或字符串,也不管是否可枚举

以上5种方法遍历对象的键名,遵守一下规则

  • 首先遍历数值键,按照数值升序排列
  • 其次遍历所有字符串键,按照加入时间升序排列
  • 最后遍历Symbol键,按照加入时间升序排列
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

对象新方法Object.assign

Object.assign用来将源对象所有可枚举属性,复制到目标对象中。至少需要两个对象作为参数,第一个是目标对象,后面的都是源对象

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 } 

如果目标对象不是对象参数的话

  • 如果是首参数,那么会将其转为对象
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()
//}

实际上是看非对象参数能否有对应的类进行实例,比如2是Number型,能由Number进行实例化得到,就相当于Number实例化了一个2的对象,而Number继承了Object,就相当于转化成对象了,对应也可以用在BooleanString类型

注意:但是Null和undefined是无法转为对象,没有对应的类给其实例化,所以他们作为首参数的话,会报错

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
  • 非对象不出现在首参数

注意:如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有些不同。参数都会转成对象,如果无法转成对象,那就跳过,不会报错。但是除了字符串会以数组形式拷贝进目标对象,其他值(这里说的是基本数据类型)不会产生效果

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可以用来处理数组,但会把数组视为对象

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

其中,4会覆盖,5会覆盖2,因为它们在数组的同一位置,对应位置覆盖,数组其实就是特殊的排列对象,只不过是有序的而已

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(取值函数),那么会先求值,再进行赋值

// 源对象
const source = {
   //属性是取值函数
   get foo(){return 1}
};
//目标对象
const target = {};
Object.assign(target,source);
//{foo ; 1}  此时foo的值是get函数的求值结果 

常见用途

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);
    //根据原型对象,创建新的空对象,再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对象此时的值最好都是简单类型的值,否则函数会失效。

    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类

传统生成实例对象是通过构造函数,如

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的构造函数Point,对应ES6Point类的构造方法

ES6的“类”Class,可以看作构造函数的另一种写法

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

即,类的数据类型就是函数,类本身就指向构造函数
当然使用也直接对类使用new命令,与构造函数用法一致

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

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

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的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方法对应的是构造函数,返回的实例都不是该构造函数了,所以肯定也不同了

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函数会进行变量提升==

静态方法
类相当于实例原型,类中定义的方法,相当于原型上的方法,都会被实例继承,但如果在类的方法前,加上static关键字,就表示该方法不会被实例继承,而是通过类来调用,成为静态方法

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关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

所以,如果静态方法包含this关键字,this指向该类,而不是实例

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

Foo.bar() // hello

父类的静态方法,可以被子类继承

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

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

静态方法也是可以从super对象上调用的。

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

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

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

Super关键字可以说是超类,指向父类对象

问题:静态方法是怎么做到的?

私有方法和私有属性
只能在类内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码封装,但ES6不提供,只能变通模拟实现

  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关键字实现继承

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方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用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 的行为完全一致。
最后,父类的静态方法,也会被子类继承。

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

class B extends A {
}

B.hello()  // hello world

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类

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

因此,可以使用这个方法判断,一个类是否继承了另一个类。

Super关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

  1. super作为函数调用,代表父类的构造函数。ES6规定,子类的构造函数必须执行一次super函数
class A {}

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

注意,super虽然代表了父类A的构造函数,但是返回的是子类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指向当前正在执行的函数,可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}
  1. super作为对象时,在普通方法时,指向父类的原型对象;在静态方法时指向父类。
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规定,在子类普通方法中通过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。也就是说,实际上执行的是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作为对象,用在静态方法中,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在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

另外,在子类的静态方法中通过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

因为实例都获取不了子类的静态方法,所以也很好理解

注意: 使用super的时候,要显式指定是作为函数,还是对象使用,否则会报错(函数就加括号(),对象不用,但后面要加属性)

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // 报错
  }
}

类的 prototype 属性和 __proto__属性

ES5中,每个对象都有__proto__属性,指向对应构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__,因此存在两条继承链

(1) 子类的__proto__属性,表示构造函数的继承,总是指向父类
(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属性

这样的结果是因为,类的继承按照下面模式实现的

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

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A)

let 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;

两条继承链,可以这么理解: 作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例

即对象对应对象,原型对应原型

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

实例的__proto__属性

子类实例的__proto__属性,指向子类,其实就一个原型链的问题。不作多解释

问题:画出原型链的表达图!


Set和Map数据结构

ES6新的数据结构Set。类似数组,但是成员值都是唯一的
Set本身是一个构造函数,生成Set数据结构

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不会添加重复的值,这个方法可以用来进行数组去重
如下,Set函数接受一个数组(或者其他具有iterable接口的其他数据结构)作为参数,用来初始化

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结构转换成数组的话可以用[...Set]或者用Array.form,(...是扩展运算符,内部使用for...of循环,一个针对iterator(可遍历)结构的循环)

// 去除数组的重复成员
[...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"

就本身来说,我自己项目中出现过,比如用户进行前端添加人员(后台接受的是人员ID数组,但在前端得显示添加的人员姓名),根据数据结构不同有几个不同方法

  1. 初始化两个数组一个人员ID数组,一个人员Name数组,当用户点击添加的时候进行任一判断存不存在数组中,不存在就push
  2. 初始化一个对象,人员ID是键,值为人员Name,进行添加的时候用人员ID作为属性判断值是不是为空,空就添加
  3. 初始化一个Set结构,当用户进行添加重复人员的时候,Set结构不会做出相应,在用户界面看到的一直都会是去重后的人员数组

==注意:第1种方法解决的话每次添加需要去循环一下数组,时间复杂度是O(n),第2种的话直接利用对象的键去查找有无值,性能上会比第一种好很多,第3种的话就省去了判断这个环节,去取值的话也直接用键去取就行了,性能也比较高==

Set实例属性和方法
  • Set.prototype.constructor:构造函数,默认是Set函数
  • Set.prototype.size:返回Set实例的成员总数

Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)

操作方法:

  • 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(): 使用回调函数遍历每个成员

keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

WeakSet

结构与Set类似,也是不重复值的集合。区别在于

  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可以接受一个数组或类数组的成员对象作为参数,如

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

a数组有两个成员,也是数组,数组也属于对象,则成员对象会自动成为WeakSet成员
而如果数组成员不是对象,就会出错,就像

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

传统JS的对象,本质上是键值对(Hash)的集合,传统只能用字符串当作键,这给它使用带来限制,如

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

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

原意是想把DOM节点存为对象的键,但对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]
Map数据结构的"键"可以接受各种类型的值(包括对象),是一种“值-值”对应的感觉。

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接受一个数组的话

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构造函数接受数组作为参数的时候,执行的是下面的算法

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

const map = new Map();

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

事实上,不仅仅是数组,任何具有Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator 对象》一章)都可以当作Map构造函数的参数。这就是说,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键是简单类型的值的话(数字,字符串,布尔值),两个值严格(严格的意思就是三等号为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结构的成员总数
  2. Map.prototype.set(key, value)
    该方法设置键名key的值为value,返回整个Map结构,若键key已经有值那就更新键值,由于是返回整个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所有成员

转换成数组的话用...很方便

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方法,与数组的forEach方法类似,也可以实现遍历。

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

forEach方法还可以接受第二个参数,用来绑定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);

第二个参数,指定对象上下文,传入是哪个对象变量,this指向reporter

Map与其他数据结构互相转换

  1. Map转为数组
    前面提过,最方便的就是使用扩展运算符(...
const myMap = new Map()
    .set(true, 7)
    .set({foo: 3}, ['abc'])
[...myMap]
//[ [true, 7], [{foo: 3}, ['abc'] ] ]
  1. 数组转为Map
    将数组传入Map构造函数,就可以转为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],数组的话直接拼接数组的值

  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
    要区分两种情况,一种是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的键名有非字符串,这时候可以选择转为数组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 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 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节点作为键名去保存,因为当DOM节点删除的时候,状态也会自动消失,不用担心内存泄漏

总结

SetWeakSetMapWeakMap一个是类数组,一个类对象,可以说是数组和对象的扩展应用把,比数组和对象有更强大的功能,也可以互相转换成数组和对象


Promise对象

简单来说,是一个容器,保存着某个未来才会结束的事件(通常是一个异步操作)的结果,比如一个请求,请求需要时间,等到响应后才会返回结果。语法上说,Promise是一个对象,它可以获取异步操作的消息。有了Promise对象,可以将异步操作以同步操作流程表达出来,避免了层层嵌套的回调。

Promise对象会将所有执行函数放在then之后执行,而不管你的操作是不是异步的,就是说同步的,经过Promise包装后就会变成异步执行

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

那么有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。

第一种写法是用async函数来写

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

上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步,就像下面的写法。

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

需要注意的是,async () => f()会吃掉f()抛出的错误。所以,如果想捕获错误,要使用promise.catch方法。

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

Generator(生成器)函数

是ES6提供的一种异步编程解决方案

从语法上,可以理解成一个状态机,封装了多个内部状态
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的都是遍历器对象,可以依次遍历Generator函数内部的每一个状态

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

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

调用Generator函数后,该函数并不执行,返回的是一个指向内部状态的指针对象,也就是遍历器对象
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。就是说,每一次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator函数是分段执行,yield表达式是暂停执行的标记,而next可以恢复执行

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 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束

yield表达式

yield表达式相当于一个暂停标志,遇到它,便返回一个对象,value值就是yield后面带的值,继续调用next往下执行到下一个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函数中

如果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表达式用作函数参数或放在赋值表达式的右边可以不加括号

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方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象

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

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

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

next方法的参数

yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个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 函数f,如果next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined。当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增

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

使用for...of循环,依次显示 5 个yield表达式的值。这需要注意的是,一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以return语句返回的6,不包括在for...of循环中

原生的js对象没有遍历接口,无法使用for...of循环,通过Generator函数为它加上这个接口就可以用了。

除了for...of循环以外,扩展运算符(...)解构赋值Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。

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 循环
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

throw()

next(),throw(),return()共同点
next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让Generator函数恢复执行,并且使用不同的语句替换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方法没有参数,就相当于替换成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函数内部,调用另一个Generator函数。需要在前者函数体内部,自己手动完成遍历

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函数,在bar里面调用foo,需要手动遍历foo。如果多个Generator函数嵌套,写起来就比较麻烦

ES6提供yield*表达式,作为用在在一个Generator函数中调用另一个Generator函数

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"

实际上,任何数据结构,只要有Iterator接口,都可以被yield*遍历,也就是说yield*可以遍历含有遍历器对象或遍历器对象接口的对象

yield*后面的Generator函数没有return语句时,相当于是for..of的简写形式,有return语句的时候,则将这个数值返回获取,如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。

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方法的时候,屏幕上会有输出,这是因为函数foo的return语句,向函数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' ]

上面代码有两次遍历。第一次是扩展运算符遍历函数logReturned返回的遍历器对象,第二次是yield*语句遍历函数genFuncWithReturn返回的遍历器对象。但是,函数genFuncWithReturnreturn语句的返回值The result,会返回给函数logReturned内部的result变量,因此会有终端输出。

而且是先执行一次函数代码,再执行遍历

yield*可以很方便取出嵌套数组的所有成员

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

内部运行,如果是数组的话执行if操作,不然就输出
使用扩展运算符...也是一样的效果

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

下面是一个稍微复杂的例子,使用yield*语句遍历完全二叉树。

// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉树
function make(array) {
  // 判断是否为叶节点
  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 函数的this

Generator 函数总是返回一个遍历器,ES6规定这个遍历器是Generator的实例,也继承了Generator函数的prototype对象上的方法

function* g() {}

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

let obj = g();

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

上面代码表明,Generator 函数g返回的遍历器obj,是g的实例,而且继承了g.prototype。但是,如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。

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

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

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

Generator 函数也不能跟new命令一起用,会报错,因为不是构造函数。

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

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

问题:能否让Generator函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this

下面一个变通方法。首先,生成一个空对象,使用call方法绑定Generator函数内部的this,这样,构造函数调用以后,这个空对象就是Generator函数的实例对象了

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 对象。这个对象执行三次next方法(因为F内部有两个yield表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在obj对象上了,因此obj对象也就成了F的实例。

上面代码中,执行的是遍历器对象f,但是生成的对象实例是obj,有没有办法将两个对象统一

一个办法是将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改成构造函数,就可以对它执行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 函数的应用

(1)异步操作的同步化表达

如Ajax,是个典型的异步操作,通过 Generator 函数部署 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函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined

(2)控制流管理

如果有一个多步操作非常耗时,采用回调函数,可能会像下面这样

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

上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。

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

然后,使用一个函数,按次序自动执行所有步骤。

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函数未结束,就继续调用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

(3)部署Iterator接口
利用Generator函数,可以在任何对象上部署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函数的异步应用


async函数

使异步操作更加方便,是 Generator 函数的改进
前文有一个 Generator 函数,依次读取两个文件。

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

上面代码的函数gen可以写成async函数,就是下面这样。

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函数对 Generator 函数的改进,体现在四点:

  1. 内置执行器
    Generator 函数的执行必须依靠执行器,或者自行调用next方法, 而async函数内置执行器,与普通函数一模一样,只需要一行
asyncReadFile();
  1. 更好的语义
    asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  1. 更广的适用性
    async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolvedPromise 对象)。

  2. 返回值是 Promise。

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本用法
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函数内部return语句返回的值,会成为then方法回调函数的参数

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

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

上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到

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命令的参数是数值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处理。

这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法,但是借助await命令就可以让程序停顿指定的时间。下面给出了一个简化的sleep实现。

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命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

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

上面代码中,getFoogetBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

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

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

上面两种写法,getFoo和getBar都是同时触发,这样就会缩短程序的执行时间。

第三点,await命令只能用在async函数之中,普通函数中的话会报错。

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

  // 报错
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

上面代码会报错,因为await用在普通函数之中了。但是,如果将forEach方法的参数改成async函数,也有问题。

function 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);
  }
}

如果确实希望多个请求并发执行,可以使用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 函数的实现原理

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

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

// 等同于

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

所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

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 函数与 Promise、Generator 函数的比较。

假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

首先是 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) {
    /* 忽略错误,继续执行 */
  }).then(function() {
    return ret;
  });

}

虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(thencatch等等),操作本身的语义反而不容易看出来。

接着是 Generator 函数的写法。

function chainAnimationsGenerator(elem, animations) {

  return spawn(function*() {
    let ret = null;
    try {
      for(let anim of animations) {
        ret = yield anim(elem);
      }
    } catch(e) {
      /* 忽略错误,继续执行 */
    }
    return ret;
  });

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容

  • 本文为阮一峰大神的《ECMAScript 6 入门》的个人版提纯! babel babel负责将JS高级语法转义,...
    Devildi已被占用阅读 1,985评论 0 4
  • 在此处先列下本篇文章的主要内容 简介 next方法的参数 for...of循环 Generator.prototy...
    醉生夢死阅读 1,442评论 3 8
  • 第3章 基本概念 3.1 语法 3.2 关键字和保留字 3.3 变量 3.4 数据类型 5种简单数据类型:Unde...
    RickCole阅读 5,128评论 0 21
  • [TOC] 参考阮一峰的ECMAScript 6 入门参考深入浅出ES6 let和const let和const都...
    郭子web阅读 1,781评论 0 1
  • 简介 基本概念 Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍...
    呼呼哥阅读 1,075评论 0 4