目录
零、辅助学习的前置知识
1、由作用域,作用域链,变量对象,执行环境来讲闭包
- 闭包的概念:
其实这里用到了一个闭包的概念,闭包就是能访问自由变量的函数,自由变量的定义为:在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。也就是如果一个函数内部使用了非该函数作用域内的变量,那么这个变量对该函数来说就是自由变量。
其实据我自己的查阅,这里对闭包的概念定义是有很多种的,例如,即使函数是在当前词法作用域之外执行,也能访问它所在的词法作用域内的变量,那么这个函数是一个闭包,又或者,闭包是指有权访问另一个函数作用域中变量的函数,再或者,能够访问自由变量的函数,就是一个闭包。 - 作用域,作用域链,变量对象,执行环境:
贴一个讲得很好的博客博客地址,这里贴博客里面的两个图,简单来说就是,首先有一个执行环境栈,每执行一个函数,都会有一个“执行环境”被入栈,每个函数的执行环境都储存着该环境的作用域链,作用域链是一个栈,最近的作用域在栈顶,“作用域”其实是变量对象这个实体的指针,所以作用域链里其实是存了一大堆变量对象的指针,注意,作用域链是逐渐叠加的,也就是,在执行环境栈里,从底到顶如果有ABC三个函数的执行环境,那么他们的作用域链是,B的作用域链=A的作用域链+B内部的变量的对象的指针,C的作用域链=B的作用域链+C内部的变量的对象的指针。
- 作用域链是在函数被初始化时就创建了,但这时的作用域链是不完全的!比如还是从底到顶ABC,没有执行到C时,C的作用域链内只有B的作用域链,还没有创建C内部的变量对象(万一这个函数根本不执行呢,每个函数在执行之前都把变量对象全创建一遍多占内存呀)。
还有,从底到顶ABC,是在函数B内执行的时候又发现了C,所以把C的执行环境入栈了,所以C的作用域链里,初始是它当前的外部环境,而它当前的外部环境是B的作用域链,所以初始是B的作用域链。
如果仨函数是在全局里并排的,那么他们仨的外部环境都是全局作用域,是不会连在一块的。
还有!如果一个函数A内有一个函数B,那么由于函数作用域的存在,B不在全局执行环境里,因此函数B目前没有被声明,也就没有初始的作用域链的!只有执行A时,B才被声明,这时候B的作用域链就可以初始化为A的作用域链(也就是执行栈,在B入栈前,栈顶A的作用域链)。
因此,所有被声明但没有执行的函数,都有作用域链,但只有执行到它,才会创建它自己的内部变量对象,然后加在自己的作用域链栈顶,这时候作用域链才圆满了,才创建该函数的执行环境,指向这条作用域链,然后推入执行环境栈。 - 变量对象:
执行环境最耗内存的就是变量对象了,理论上每一条执行环境,都是一个函数的执行触发的(最外层的script标签可以理解为一个广义的main函数嘛),而这个函数的执行会导致一个该函数内部的变量对象的产生,这个变量对象就占内存了,而作用域链上如果有其他的变量对象,也只是储存变量对象的地址,不会重复创建。 - 执行环境的销毁:
理论上讲,在一个函数执行完后,它的执行环境就应该销毁了,执行它时创建的变量对象也应该释放了,但是呢,如果一个函数的作用域链里指向了其他函数的变量对象,那么,这个变量对象不会随着另一个函数的执行结束而销毁,会一直存着,这样的作用域链上有其他函数的变量对象的函数,就叫闭包,只有闭包自己执行完了,闭包自己的执行环境销毁时,作用域链上可以释放的变量对象才会被释放(如果又被其他闭包捕获了,那么还是不能释放)。 - 综上,闭包的三个定义也就很好理解了,所谓的可以访问自由变量,访问其他函数内的变量,可以记录执行环境等等,其实核心就是捕获,或者说‘封闭’了其他函数的变量对象,触发闭包显然会导致一部分内存无法释放,是挺占空间的,但它也有优点,例如,一个函数每次执行结束,它创建的内部变量对象就没了,但它的声明还在,作用域链就还在,上面的其他变量对象没有被清空,因此,可以把一部分想要长期保存用于该函数的对象,存在某个被封闭的变量对象内,而不用存在全局变量对象内(全局变量对象一定在所有作用域链上),这样就可以避免全局变量被污染,可以存一小部分专用于该函数的“全局”变量。
2、构造器和原型,原型链
老生常谈的知识,这里只强调一个点,构造器内this.的那些东西,是该构造器new的每个对象都单独拥有一份的,而prototype这个对象默认每个构造器都有,构造器new出来的每个对象也都有一个proto指针指向构造器的这个prototype对象,因此,为了减少new多个对象大量占用内存,可以把一些方法写在构造器的prototype里。
3、构造器函数对象的属性
如果仔细观察会发现,虽然每个对象的原型链尽头都是obj对象,但是!有一些Object自己的方法,只能通过Object.这样的方式访问,而Object是个函数呀?这就是js的一个,函数也是对象的概念!那么如何给函数对象添加属性呢?并不是在函数内通过this.,或者直接function,var之类的声明内部私有变量,其实,函数内的代码体,只是这个函数对象的属性之一,这个函数对象还可以拥有其他属性,而其他属性跟这段代码是没有关系的,因此,new出来的对象,也肯定是没法访问到这些“其他属性”了,而只有唯一的Object这个函数对象,可以访问到这些属性。
如何给一个函数对象添加属性也很简单,在声明完一个函数后,如函数名为f,只要通过f[],或者f.的方法,就把f当成一个普通对象来注册属性就可以了。但是,console.log打印这个函数名的话,会发现只会打印代码体,而不像打印普通对象一样可以看到对象内的属性,这还是有点尴尬的,所以如果要给一个函数对象注册属性,最好还是有个文档记录一下吧。
简单来说就是,this的属性在每个实例里都有,prototype的属性是每个实例里指向的都是同一个地方,而构造器对象的属性,是只属于构造器对象的,哪个实例都访问不到。
4、a instanceof B
这玩意用于判断对象B的prototype在不在a的原型链上,如果在的话,有三种可能,第一,B是构造器对象,a是B new出来的对象,那a的原型链里肯定有B的原型,第二,a不是B new出来的,但B new出来的对象是a的某个祖先或者父亲,第三,B是个普通对象,不是构造器对象,但是它在原型链上,也可以用来判断类型,比如,obj instanceof String,返回false,说明不是字符串对象…类似如此,但是由于左右两边都得是对象,所以这个判断起来还是有点局限性。
5、js垃圾回收机制
是引用计数的方式,例如一个值是个数组[1,2,3],然后有一个变量引用这个数组如arr=[1,2,3],那么这块内存的引用计数就是1,当是0时,这块内存会被垃圾回收机制定期清空,但这也导致了不好的情况,比如闭包导致某些变量对象被封闭,进而这些变量引用的内存就无法释放,进而导致内存泄漏,大量占用内存。
es6提出了一种不会影响垃圾回收的方法,即弱引用的set和map,下面正文再写。
一、let,const,和各种作用域,变量提升的相关笔记
- let和const:
let和const不会变量提升,因此如果在声明前使用,会报错阻止代码运行,此外var和function会变量提升,function在前,var在后,所以函数名和变量名重复的话,无论是以怎样的顺序定义的,变量都会覆盖函数。
还有,这个使用会报错的使用,包括任何形式的使用,比如typeof 一个let声明的变量(在声明的那行代码前),那也会报错!但如果typeof一个压根不存在的变量,反而只会打印undefined,而不是报错。
以及不能重复声明,let,const,var,函数参数名,函数名,只要有let或者const,同层作用域就不能再声明重名变量了。 - 块级作用域,全局作用域,函数作用域,:
在es5中变量提升会到函数作用域头顶或全局作用域头顶,es6中,var还是升到函数作用域或全局作用域头顶,但let和const是不会提升的,且能够识别花括号,因此有了块级作用域。 - 函数提升:
上面分析了变量,这里讲下函数提升,在es5中函数的整个声明会提升到函数作用域或全局作用域头部(变量提升是只提升了变量,但值是undefined,函数是整个声明提上去),但在es6中,由于有了块级作用域,理论上函数不会穿越块级作用域提升到外面,但实际上,为了兼容老代码,这里一般是这样处理的,函数整个声明是提升到块级作用域头部,但函数名会作为一个var变量穿越块级提升到函数或全局作用域头部,如下代码,具体执行时,f是undefined,会报错,因为f作为一个函数声明提升到块级作用域头部了,但又作为一个undefined变量提升到函数作用域头部了…
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
- 数据类型与声明方式:
es5只有两种声明变量的方式,var和function,es6多了let,const,import和class,现在一共是6种声明方式。数据类型分基本类型和引用类型,基本类型是number,Boolean,symbol,string,null,undefined,引用是array,function,obj。 - 顶层对象的属性赋值与全局变量的赋值:
下面是阮一峰博客里的一段原文,正常来讲,给对象添加属性都是对象.或者对象[],但是全局环境下var和function声明的变量会被加进window对象,这种挂钩是不利于模块化编程的,且并不合理,因此在es6中进行了改进,首先为了兼容旧代码,var和function声明的变量还是window对象,但let,const,class声明的变量不再属于window对象。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
- 未捕获的SyntaxError:词法声明不能出现在单个语句上下文中:
就是下面这样的代码会报错,倒不是说let只能在花括号里定义,而是不能出现在这样的“单个语句上下文”,要么在花括号里,要么在全局。
if (true) let x = 1;
但下面这个在圆括号内的是可以的
for(let i=0;i<10;i++)var y=2;
二、解构赋值
十二、Symbol
- 它是一个永远无法重复的值,通过Symbol函数来创建,可以传入一个描述如Symbol(‘abc’),也可以不传入描述,也可以传入相同描述,无论咋样,每个Symbol()的返回值全部都是不一样的,不管传入的字符串是啥,传与不传。
- Symbol值不可隐式转换为字符串,但可以显式转换为字符串,如下代码,转换的字符串是带着Symbol和括号的,所有的一块全部转换成字符串,但是隐式转换(如Symbol(‘My symbol’)+‘abc’)这样是会报错的。
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
- Symbol可以隐式或显式转换为布尔值,但不可以显式或隐式转换为数字,任何一个symbol值都是true,在if里判断的话会隐式转换为true,但是不能跟数字相加,会报类型错误,它不管显式隐式都无法转换成数字。
- 提一嘴显式转换,就是诸如Number(),Boolean(),string()把这些构造函数直接当成函数调用,传入想要转换的变量,返回值就是转换后的,注意是返回值,不是直接把变量的值修改了,得给变量赋值才能改啊。
- 将symbol作为对象的属性名的几种方法,注意!不能通过obj.的方式赋值,因为.后面是跟的字符串,也就是键名,而不会解析出变量内的内容,比如obj.x,会给obj添加一个key为x的属性,而不是解析到x变量内存的值,再把这个值作为建名,虽然说起来简单,不强调一下的话有可能会注意不到的。
因此,只能通过方括号,或obj的定义属性的方法来把一个symbol值设置为属性名。特别注意!obj[“Symbol()”]这样是不可以定义一个symbol属性的,这样就是简单的把它当作一个字符串,添加了一个属性名是这个字符串的属性,它打印后看起来仿佛一个symbol属性,但它不是!!通过Obj的getOwnPropertySymbols()的方法,会发现确实它不是一个symbol类型的属性名。
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
- 用于消除魔术字符串:
魔术字符串是一种与代码强藕合的字符串,例如判断传入值是一个具体的字符串,然后执行相应的代码,这样会导致想要通过调用这个函数来实现某些功能时,可能被迫必须传入一个固定的字符串,这就是魔术字符串。
魔术字符串会导致一个固定的字符串与代码耦合度太高,一个较好的修改方法是改判断传入值是否与一个变量储存的值相等,如果相等,则执行对应的代码,这样与变量绑定,实际判断的字符串是可以随意修改的,代码风格就会更好。
但是呢!如果这里只是把固定的字符串赋值给了一个变量,缺点是多个变量间的值可能会重复(在需求多个变量储存多个不同状态时,重复是会出问题的),因此,改为赋值symbol,就可以避免这种问题。 - 关于对象内键名为symbol类型的属性的访问方法:
如果有一个变量储存着键名,那么用obj[]的方法就可以访问,但显然不能只依赖变量,然而它又无法通过for in或for of,或者对象+点,或者obj[‘symbol()’]之类的方式访问到,有一个简单的方法是Obj对象里的getOwnPropertySymbols(),这个方法会返回一个数组,数组里储存着所有symbol类型的键名,注意是键名不是键值。注意Object.getOwnPropertyNames()是不能获取到symbol的,只能用symbol单独的方法来获取。 - 实现“私有”属性:
结合第7条可知,symbol作为键名时,常规的遍历和常规的访问方法都无法得到这个值,因此它就像一个私有属性一样,可以完成一些特殊需求。 - 对象的内部方法:
如果希望一个对象内的一个方法是内部的,那么就可以用一个symbol 值作为该方法的属性名,然后,把这个属性名存在Symbol这个构造器对象内的某个变量里就可以了,这样就可以通过对象symbol.key来调用这个方法,相对来说是比较内部的,一般的简单方法很难访问到它。 - Symbol构造器对象内已有的一些symbol值:
可以这样理解,我们利用symbol不好访问的特点,可以给对象注册“内部”属性,但是又得用变量存着这些symbol值,虽然get…symbol也能得到键名,但symbol键名看起来不知道是啥意思,哪怕有“描述”,描述可以重复的呀,所以还是要一个有含义的变量名存着symbol值。symbol构造器对象内就分了十几个变量出来存了十几个symbol值,这些symbol值就是对象内可能存在的内部属性,一个对象想调用的话,就得先获取到这些symbol值,然后看看自己有没有这个属性…
十三、set和map
- set:
类似一个数组,可以传入一个具有遍历器接口的数据结构用于初始化,特点是会去除重复属性,用set可以很方便的去除数组内的重复值,只要再用展开符…把set展开成数组就完事了。多提一嘴重复的判断,这个判断是类似于精确相等运算符===的,也就是类型和数值都相等,区别在于NaN不精确等于NaN,但是在set里,重复的NaN会被认为重复。
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
-
set实例的方法大体可以分为两类,都写在构造器原型里,一类是操作数据,包括add增,delete删,has查,clear清除。以及四个遍历方法,四个方法里三个是遍历器生成函数,也就是set部署了三个遍历器接口,keys遍历键名,values遍历键值,entries遍历键值对,通过for(let item of set.keys())这样的方式来指明通过哪个接口遍历。如果不指名的话,默认for(let item of set)的遍历器接口是values,也就是Set.prototype[Symbol.iterator]指向的是values这个遍历器生成函数。数组有forEach(),set这个进阶版无重复数组也有forEach,传入一个回调函数用于处理每个成员。
-
补充一句,set里面如果想加进去一个symbol值,只会让set的大小加一,里面并不能存进去这个symbol值,用下标访问打印出来是undefined。
-
weakset是一种弱引用类型的set,里面不能存放具体值,只能存放指针,也就是广义的对象,例如数组对象,函数对象,普通的对象,它的特点在于不影响垃圾回收机制,如果没有其他引用了,那么weakset里面的成员会消失,所以可以用来临时存放成员,也可以避免内存泄漏。
-
weakset也有set的增删查方法,但是没有清除,因为弱引用也不需要。此外也没有size和遍历,因为成员随时消失,它并不是一个可遍历对象,没有遍历器接口。
-
map是升级版的对象,普通的obj限定键名必须是字符串,但是map的键名无所谓啥都行。它有set和get方法,set是通过传入键和值来加入一个成员,get是通过键来获取这个成员。此外也一样可以用一个数组来初始化map,数组内需要是一个个键值对子数组。
-
要特别注意的是,如果键名是个引用类型,必须通过同一个引用才能获取到对应的值,如map.set([‘a’], 555);
map.get([‘a’]) // undefined
前后两次不同的[‘a’]其实是俩引用,因此后面这个是未定义的。 -
简单来说,如果键是一个基本类型(简单类型),那么只要严格相等,就是同一个键,但如果是一个引用类型,则必须地址相等。此外,跟set一样,NaN和NaN会被认为相等,即是同一个键。
-
map的方法跟set差不多,都是增删查清,还有get和set,遍历也是一样的4种,3个构造器生成函数+一个forEach。这里可以发现,map这个升级版obj是可以遍历的,它不光有遍历器接口,还一下有了仨…且遍历的线性顺序就是按照插入顺序来的。但是跟set不一样的是,map的默认for of不是值遍历,是键值对遍历。
-
map也有一个弱引用版本weakmap,它的键名必须是对象,值无所谓,键名必须是对象,如果想要给一个对象添加一些数值,而又不是给对象添加成员,例如添加对这个对象的描述性字符串或者其他什么,就可以用weakmap来保存,weakmap既可以给对象记录一些值,又不会产生引用而影响内存回收,当这个对象不再被其他引用时,对应的weakmap成员也就消失了,很合理。
-
weakmap的方法,跟weakset差不多,首先没有遍历,也没有size,其次也没有clear,get,set,has和delete。
十四、Proxy
十七、遍历器
遍历器实际上就是一个指针对象,该对象会包含一个next函数,每次调用next,都会返回一个对象,这个对象内储存着被遍历对象的一个成员,每次调用next都会返回下一个成员。下面是一个数组的遍历器生成函数的实现方法:
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
需求为:给生成函数传入一个待遍历数组,返回该数组的遍历器对象,内包含一个next函数,该函数的返回值是一个包含数组元素的对象,value储存元素的值,done储存遍历的状态(当前是否遍历到末尾)。
其实这里就用到了闭包的概念,一个专属于该构造器的,存在于被封闭的变量对象内的变量用于记录下一个需要访问的下标,每次调用next,都判断一下当前待访问下标是否超出可访问范围,如果超出,代表遍历结束。
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制
如上,引用自阮一峰大佬的博客,遍历器对象其实就是一种统一的访问机制,给每个可遍历的数据结构,无论是数组,还是对象,都设置一个同名属性,且使用同名方法(如next)来访问每个元素,这种统一的访问机制,使得for…of这种访问方式得以成立。
要注意的是!es6规定,可遍历对象,遍历器的属性名是一个symbol值,由上面分析可得知symbol值由于无法通过常规方式访问,是一种比较内部的写法,可以有效避免误操作把遍历器接口给覆盖了,因此,虽然强行规定遍历器接口是个字符串也行,但为了提高健壮性,还是用了symbol值,这个值被存在Symbol构造器对象的Symbol.iterator属性里了,要访问一个对象的构造器的话,或者给一个对象设置构造器,需要obj[Symbol.iterator]这样才行。
也可以说,只要一个对象/数据类型设置了这个属性,那就能通过for…of来遍历了,es6原生给一部分数据类型设置了遍历器接口,如下:
Array
Map
Set
String
TypedArray
函数的 arguments 对象
NodeList 对象
(话说,我猜啊,每个类型的遍历器接口应该是在这个类型的构造器对象里边了,然后只要把这个类型的实例,传给这个类型的构造器的遍历器生成函数就行了。)
可见普通的自定义构造器生成的对象不能遍历,还有Object这个构造器也没有遍历接口。
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。一个对象如果要具备可被for…of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
简单来说,普通的对象并不是一个线性结构,因此不具备默认的线性遍历方式,但如果有相关需求,也可以给一个对象添加遍历。
如下是给链表的节点对象部署遍历接口,因为从任何一个节点开始都能够遍历,所以遍历器接口可以部署在节点构造函数的原型上。
构造器接口函数的返回值是一个对象,对象里有一个next函数,next的返回是一个个元素对象,这个最外层的接口函数内的变量对象空间,可以储存一些next函数专用的变量,通常会储存一个标识当前遍历状态的变量,比如链表的遍历需要储存一个下一个待遍历节点的指针,数组的遍历需要储存下标,遍历图或树的话可以根据需求设置下一个遍历状态的寻找和储存。
function Obj(value) {
this.value = value;
this.next = null;
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = { next: next };
var current = this;
function next() {
if (current) {
var value = current.value;
current = current.next;
return { done: false, value: value };
}
return { done: true };
}
return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i); // 1, 2, 3
}
下面再贴个类数组对象的遍历器函数,因为是遍历这个对象内的内容,所以接口部署在对象内部就行了。
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
}
return { value: undefined, done: true };
}
};
}
};
再强调一点,给遍历器接口传入一个待遍历对象后,返回的是一个包含next函数的对象啊!这时候遍历还没开始呢。
遍历器对象除了具有next()方法,还可以具有return()方法和throw()方法。如果你自己写遍历器对象生成函数,那么next()方法是必须部署的,return()方法和throw()方法是否部署是可选的。
再引大佬博客的一段原文如上,也就是,遍历器函数返回的包含几个方法的对象,next是必须的,此外还可以部署return和throw,如下是return:
function readLinesSync(file) {
return {
[Symbol.iterator]() {
return {
next() {
return { done: false };
},
return() {
file.close();
return { done: true };
}
};
},
};
}
先不管传入的是啥,return函数会返回遍历终止的对象,也就是提前结束遍历,在以下两个情况下,由于遍历被提前终止了,就会调用遍历器的return方法。
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情况二
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
一个是break会中途停止循环,另一个throw抛出错误也会停止循环,这种时候就会调用return。
补充一个所有的遍历方法:
- 最基本的用游标的for循环,for(let i=0;i<n;i++),写起来比较麻烦
- forEach:array.forEach(function(currentValue, index, arr), thisValue),可以用于遍历数组,传给forEach一个函数,可以对数组内的每个元素(元素是第一个传入参数,第二个是下标,第三个是被遍历的这个数组,后面俩是可选的)作出处理,算是基础for的一种升级写法,但是缺点是没法中途终止循环。
- for…in:这个可以遍历键名,但是主要是遍历对象的,如果遍历数组的话,可能会出现下面的问题:
给数组的原型上加个对象,然后for in遍历,会把加的这个也遍历到的,显然不大合理。
var c=[1,2,3]
c.__proto__.p=2
for(let key in c){
console.log(key)
}
console.log(c)
还有因为__proto__跟数组构造器的prototype指向同一个对象,所以也可以Array.prototype.p=2
这样也一样。也就是手动添加进原型链的那些对象都会被遍历到,c.__proto__={x:2,y:3}
这样直接改掉整个原型对象的话,就会把x和y遍历到。所以显然不适合遍历数组。
- for…of:又简洁,又只按需求遍历,原本没法遍历的结构只要部署遍历接口就也能遍历了,就很强。
十八、Generator
这玩意的出现让遍历器的实现更加丝滑了,如下:
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
这个星号函数会返回一个包含next的对象,也就是遍历器,再next()一下就会返回一个包含值和done的对象,内部实现是这样,发现yield时,就把它后面的东西作为值,然后返回一个值和done的对象。