本来是想说说生成器Generator的,但我发现和迭代有关,所以要先讲讲迭代器Iterator,可迭代器又会扯到Symbol,所以拉出来一起说说;
Symbol
Symbol是ES6引进的一种新数据类型,它代表一个独一无二的值,至此JavaScript一共有7种数据类型,分别是:Undefinded
、String
、Symbol
、Object
、Null
、Number
、boolean
。
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
对象,分别有male
和female
两个属性,我们在应用的时候可能只需要区分这两者不一样即可,并不关心他们实际值是多少,可以将值设置为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
属性。原生已实现该接口的数据类型有Array
、Map
、Set
、NodeList
、String
以及函数的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入门