ES6中的Symbol、Iterator、Generator的简单理解

  本来是想说说生成器Generator的,但我发现和迭代有关,所以要先讲讲迭代器Iterator,可迭代器又会扯到Symbol,所以拉出来一起说说;

Symbol

Symbol是ES6引进的一种新数据类型,它代表一个独一无二的值,至此JavaScript一共有7种数据类型,分别是:UndefindedStringSymbolObjectNullNumberboolean

Symbol是什么

Symbol到底是什么?先来个栗子看看效果:

let s1 = Symbol();
let s2 = Symbol();
console.log(s1, typeof s1); // Symbol() "symbol"
console.log(s1 === s2); // false

let s3 = Symbol('symbol');
let s4 = Symbol('symbol');
console.log(s3, s4);    // Symbol(symbol) Symbol(symbol)
console.log(s3 === s4); // false

let s5 = Symbol.for('symbol');
let s6 = Symbol.for('symbol');
console.log(s5, s6);    // Symbol(symbol) Symbol(symbol)
console.log(s5 === s6); // true
console.log(s4 === s5); // false
  • Symbol有两种创建方式,一种是Symbol(),一种是Symbol.for();
  • 注意: Symbol不是对象,不要使用new关键字;
  • Symbol()函数可接收一个字符串为参数,仅做描述使用,不代表Symbol的唯一性;
  • Symbol.for()接收一个字符串做为参数,代表Symbol的唯一性,相同的字符串能取到唯一的Symbol;

Symbol怎么用

既然Symbol的特性代表独一无二,那典型的应用场景就是需要唯一性的场所,比如说枚举值、对象的属性名或方法名等。
* 以性别为例,我们定义一个gender对象,分别有malefemale两个属性,我们在应用的时候可能只需要区分这两者不一样即可,并不关心他们实际值是多少,可以将值设置为Symbol类型。(如果需要与服务端交互另说,这里主要是为了说明Symbol的用法,勿喷)

// 性别:常规定义
const gender = {
    male: 'male',   // 男
    female: 'female'    // 女
}
// 性别:Symbol定义
const gender = {
    male: Symbol(),
    female: Symbol()
}
  • 做为对象的属性名或方法名在ES6中也已经很常见了,可查看数组的原型对象不可以找到名称为Symbol类型的方法。
  • 举例说明,假设有一个person对象,我想扩展一个方法,但又不确定新加的方法名是否会引起冲突,所以使用Symbol类型的值做为方法名
// person对象
let person = {
    name: 'yusian',
    age: 21,
    say() {
        console.log('saying...');
    }
}
// 自定义方法名
let methods = {
    say: Symbol('say'),
    run: Symbol('run')
}
person[methods.say] = function () {
    console.log('symbol saying...');
}
person[methods.run] = function () {
    console.log('symbol running...');
}
person.say();   // saying...
person[methods.say]();  // symbol saying...
person[methods.run]();  // symbol running...
  • 为什么要说Symbol类型的方法名呢,我们以后会经常使用Symbol类型的数据做为方法名来扩展对象功能吗?我觉得不应该这样,这并不是一个很好的解决方案。比较有价值的是ES6提供的内置的Symbol值
  • 重点来了 前面所有Symbol相关的点都为这一刻,以Symbol.hasInstance为例,定义一个Person类,在类中定义一个静态方法名为Symbol.hasInstance,那么当对该类进行instanceof调用时就会执行该静态方法,返回值即为调用返回结果。
class Person {
    // 方法名一般为字符串,Symbol类型做为方法名需要加方括号
    static [Symbol.hasInstance] = function (foo) {
        console.log('hasInstance, ', foo);
        return false;
    }
}
let p = new Person();
console.log(p instanceof Person);   // false
  • 大致用法就是这个样子,更多内置Symbol值参考:MDN
  • 有了这个基础后面就可以讲Iterator了

参考链接:阮一峰ES6入门

Iterator

Iterator是一个迭代器,实现了这个接口就可进行for...of遍历,而所谓实现了这个接口简单一点讲就是对象有Symbol.iterator属性。原生已实现该接口的数据类型有ArrayMapSetNodeListString以及函数的arguments对象。
* 以数组为例,使用for…of进行遍历能分别输出数组中的每一个值;

let array = [1, 2, 3, 4, 5];
// 
for (let v of array) {
    console.log(v);
}
  • 如果是自定义的一个对象,想实现Iterator接口该如何处理呢?如果没有实现Iterator而直接进行for…of调用会抛出异常Uncaught TypeError: object is not iterable;
let object = {
    name: 'obj',
    array: ['a', 'b', 'c', 'd', 'e']
}
// 报错:`Uncaught TypeError: object is not iterable`
for (const iterator of object) {
    console.log(iterator);
}
  • 实现Iterator接口就是在对象中增加方法名为Symbol类型Symbole.iterator的方法,该方法返回一个对象,并且对象要有一个next方法,next方法又要返回一个包含value(任意类型)和done(布尔值)属性的对象。
// 简单改造一下
let object = {
    name: 'obj',
    array: ['a', 'b', 'c', 'd', 'e'],
    [Symbol.iterator]() {
        return {
            next: () => {
                return {
                    value: undefined,
                    done: true
                }
            }
        }
    }
}
  • 此时再执行for...of就能对object进行遍历操作了,但还不能有正常输出,因为next方法返回的对象中done属性决定遍历是否结束,如果为true则结束遍历。
  • 如果希望对object对象进行迭代输出array中的值,则可以如下改造:
let object = {
    name: 'obj',
    array: ['a', 'b', 'c', 'd', 'e'],
    [Symbol.iterator]() {
        let i = 0;  // 索引记录
        return {
            next: () => {
                let value = this.array[i];
                let done = ++i > this.array.length;
                return {value, done}
            }
        }
    }
}
let iterator = object[Symbol.iterator]();
console.log(iterator.next());   // { value: "a", done: false }
console.log(iterator.next());   // { value: "b", done: false }
console.log(iterator.next());   // { value: "c", done: false }
console.log(iterator.next());   // { value: "d", done: false }
console.log(iterator.next());   // { value: "e", done: false }
console.log(iterator.next());   // {value: undefined, done: true}
for (const iterator of object) {
    console.log(iterator);  // 分别打印a b c d e
}

参考链接:阮一峰ES6入门

Generator

Generator是什么

Generator是什么?有什么作用?为什么和迭代器又有关系?
* 先讲一个场景,假设有一个很长的数组,N多个元素,我想从里面取出几个用一下并且只用一两次。我们传统的方式是先将数组读入内存,随机读取。可问题是使用频率低,数组占用空间又大岂不是很浪费资源。
* Generator就可以解决这个问题,它并不会预先将所有数据都加载进来,而是生成一个迭代器,通过迭代的方式取出需要的数据,这样没有常驻内存的问题了。

也许这样讲并不全面,但对于在完全不清楚Generator是什么的情况下,有了一个比较直观的认识,然后再来看看Generator长什么样子:
* Generator也是一个函数,但是一个特殊的函数,在写法上要加一个*

function *gen() {
    // code...
}
  • 普通函数返回值都是return指定,没有return返回undefinded,但Generator返回的是一个迭代器(终于知道为什么要先讲迭代器了)
  • 函数内部使用yield表达式,每个yield对应一次迭代,每个yield表达式的结果就是迭代的结果,函数返回值是迭代结束后done为true对象中的value值;
function* gen() {
    yield 'hello';
    yield 'world';
    return 'over!'
}
let iterator = gen();
console.log(iterator.next());   // {value: "hello", done: false}
console.log(iterator.next());   // {value: "world", done: false}
console.log(iterator.next());   // {value: "over!", done: true}
  • 为了方便理解,每一次yield可以当作是一次return,只不过函数不会因此结束,只是暂停了而已,再次迭代时会继续往下执行,每次迭代到yield处分隔。
  • 既然是迭代器那就可以用for...of进行遍历,注意: return返回值不会被for…of遍历出来,因为for…of不会输出done为true的对象;
function* gen() {
    yield 'hello';
    yield 'world';
    return 'Hello World!'
}
for (const v of gen()) {
    console.log(v);
}
  • next方法可以传参,传入的参数会在上一个yield表达式左边接收,即上一个yield表达式的返回值(可以简单地理解,每次迭代包含了上一个yield左边的值,即next的传参) ,需要注意的是,第一次调用next方法是不需要传参的,传了也没用,因为没有“上一个yield”,同样generator函数也可以传参,可以在所有的yield表达式中可见;

Generator怎么用

既然generator可以通过迭代器的next方法让函数恢复继续往下执行,这就符合异步执行的条件,以一个示例来说明:

// 定义生成器函数
function* gen() {
    let data = yield request('http://example.com/xxx');
    // 可继续往下增加异步操作,每次异步操作结束后调用迭代器的next方法即可
    console.log(data);
}
// 异步请求函数
function request(url) {
    fetch(url).then(response => {
        return response.json();
    }).then(data => {
        // 第二次调用next方法,让迭代器继续往后执行
        iterator.next(data);
    })
}
// 返回迭代器
let iterator = gen();
// 第一次调用next方法,启动request请求
iterator.next();
  • 首先创建一个generator,将所有的异步操作都通过yield关键字,写到函数中;
  • 调用generator函数获取迭代器,第一次调用迭代器的next方法启动第一个异步操作;
  • 每一个异步操作结束后都调用迭代器的next方法来启动下一个异步任务,异步执行的结果可以通过next方法传到yield的返回值中;
  • 如此反复即可实现多个异步操作按步骤执行,而在程序编写上是多个yield排列,简单直观。

参考链接:阮一峰ES6入门

Leave a Reply