muwoo / blogs

📚一个前端的博客。
2.32k stars 350 forks source link

Symbols, Iterators, Generators, Async/Await, Async Iterators  的那些事 #21

Closed muwoo closed 6 years ago

muwoo commented 6 years ago

JS 有很多特性,往往需要关联在一起才能更好地理解彼此,如果单独去看某一部分,可能理解其功能设计就会比较吃力。这篇文章主要是将其联系在一起探讨一下他们的出现背景和使用方式。

Symbols

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

那么为什么需要这样一种数据类型呢?

1. 为对象向后兼容的添加新属性

某些情况下,我们需要为 object 对象添加一些属性,但同时不会影响属性已经存在的一些方法。比如:for infor of循环。或者Object.keysObject.getOwnPropertyNames()JSON.stringify()

举个例子🌰:现在我们需要向服务端发送一个通过 JSON.stringify 转换过的字符串,我们可以这么写

var myObject = { 
    firstName:  'mu', 
    lastName:  'woo'
}

var myObjectStr = JSON.stringify(myObject)

ajax.post(url, {str: myObjectStr})

如果我们此时需要再为myObject添加一个新的属性newproperty,那么我们 ajax 请求的myObjectStr 属性便会又多了一个属性newproperty。这个时候如果你添加一个Symbol类型的属性,那么JSON.stringify将依然返回之前那样的数据格式:

var newproperty = Symbol()

var myObject = { 
    firstName:  'mu', 
    lastName:  'woo',
    [newproperty]: 'test'
}

var myObjectStr = JSON.stringify(myObject) // "{"firstName":"mu","lastName":"woo"}"

ajax.post(url, {str: myObjectStr})

2. 避免命名冲突

有些情况,我们希望我们的属性是独一无二的,通过 Symbol 这种方式,可以不断向全局添加新属性(并且可以添加对象属性),而不用担心名称冲突。 假如我们现在需要向 Array对象中添加一个新的属性toUpperCase,我们可能会这么做:

Array.prototype.toUpperCase = function () {
   return this.map(function (item) { 
       return item.toUpperCase() 
    })
}

var myArray = ["mu", "woo"]

myArray.toUpperCase() // ["MU", "WOO"]

设想一下,假如我们再引入其他的库,或者说 ES2019 支持了Array.prototype.toUpperCase这样的特性,那么我们的命令将会存在冲突。所以,Symbol 作为独一无二的命名就可以解决这样的问题:

var toUpperCase = Symbol('this is a function')

Array.prototype[toUpperCase] = function () {
   return this.map(function (item) { 
       return item.toUpperCase() 
    })
}

var myArray = ["mu", "woo"]

myArray[toUpperCase]() // ["MU", "WOO"]

3. 通过Symbol,调用语言内部使用的方法

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。比如instanceof:对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是FooSymbol.hasInstance

class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}

[1, 2, 3] instanceof new MyClass() // true

我们也可以对其进行改写:

class Even {
  static [Symbol.hasInstance](obj) {
    return Number(obj) % 2 === 0;
  }
}

// 等同于
const Even = {
  [Symbol.hasInstance](obj) {
    return Number(obj) % 2 === 0;
  }
};

1 instanceof Even // false
2 instanceof Even // true
12345 instanceof Even // false

Iterator

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

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

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

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

下面是一个模拟next方法返回值的例子。

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

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

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。 凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象: