1.for…in遍历和for…of遍历
ES5遍历数组的方法有很多,可以使用forEach,ES5具有遍历数组功能的还有map、filter、some、every、reduce、reduceRight等,只不过他们的返回结果不一样。但是使用foreach遍历数组的话,使用break不能中断循环,使用return也不能返回到外层函数。
使用for in 也可以遍历数组,但是会存在以下问题:
- index索引为字符串型数字,不能直接进行几何运算
- 遍历顺序有可能不是按照实际数组的内部顺序
- 使用for in会遍历数组所有的可枚举属性,包括原型。例如上栗的原型方法method和name属性 所以for in更适合遍历对象,不要使用for in遍历数组。
那么除了使用for循环,如何更简单的正确的遍历数组达到我们的期望呢(即不遍历method和name),ES6中的for of更胜一筹。
注意:for-of目前js实现的对象有array,string,argument以及后面更高级的set,Map 当我们遍历对象的时候可以使用for-in,不过这种遍历方式会把原型上的属性和方法也给遍历出来,当然我们可以通过hasOwnProperty来过滤掉除了实例对象的数据,但是for-of在object对象上暂时没有实现,但是我们可以通过Symbol.iterator给对象添加这个塑性,我们就可以使用for-of了。
总结一下:
- for-in是遍历数组,ES5就存在,遍历的是字符串类型的key,不一定按实际的内部顺序遍历,并且会遍历所有的可枚举属性,包括原型上的,所以for-in适合遍历对象,不适合遍历数组。
- for-of是ES6引入的,直接遍历对象的值,不过这种遍历方式会把原型上的属性和方法也给遍历出来,可以使用Object.
hasOwnProperty方法来解决这一问题。anyway,for-of不能用于对象上,只能用于array,string,argument以及后面更高级的set,Map。
2.引入[Symbol.iterator]使对象可直接遍历
我们前面说过,for-of是没有办法遍历,如果我试图遍历,就会报错。
const obj = {
name: '莫一直'
}
Object.prototype.method = function () {
console.log('这是原型中的方法!')
}
for (let value of obj) {
console.log(value)
}
如果我们想要让对象可遍历的话,就要给它加上[Symbol.iterator]这个属性,并且定义遍历的方式。
首先,什么是[Symbol.iterator]
JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员) 。
作者:果汁_ 链接:juejin.cn/post/701566… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
网上是这样解释的,按我的理解就是,这些复杂的对象需要统一的方法去遍历,ES6给每个复杂对象提供了一个接口,为不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员) 。
这些对象是原本就具备iterator接口的
- Array
- set容器
- map容器
- String
- 函数的 arguments 对象
让我们来改造一下这个对象,让它可以用for-of遍历
有两种方法可以实现
- 偷别的对象的iterator
- 自己设计一个iterator接口
1.偷数组的迭代器
数组迭代器必须要在对象里有length这个属性。
const obj = {
name: '莫一直',
1: 2,
2: 3,
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
Object.prototype.method = function () {
console.log('这是原型中的方法!')
}
for (let value of obj) {
console.log(value)
}
输出结果:
2.自定义迭代器
// 自定义迭代器
function myIterator(arr) {
let nextIndex = 0 // let声明的是块作用域里的属性
return {
next: function () {
return nextIndex < arr.length
? { value: arr[nextIndex++], done: false }
: { value: undefined, done: true }
}
}
}
let arr = [1, 4, 'ads']// 准备一个数据
let iteratorObj = myIterator(arr)
// 所有的迭代器对象都拥有next()方法,会返回一个结果对象
console.log(iteratorObj.next()) // { value: 1, done: false }
console.log(iteratorObj.next()) // { value: 4, done: false }
console.log(iteratorObj.next()) // { value: ads, done: false }
console.log(iteratorObj.next()) // { value: undefined, done: true }
上面是一个迭代器接口,我们看到在这个迭代器构建函数返回了一个对象,这个对象有一个方法,next,这个方法返回了传入的一个数据结构某一个迭代值。这个迭代的规则就是数组下标自增1。返回的对象有value和done两个属性。
那么下面我们来自定义一下对象的迭代规则。
const obj = {
name: '莫一直',
1: 2,
2: 3
}
Object.prototype.method = function () {
console.log('这是原型中的方法!')
}
// 自定义对象的迭代器
Object.prototype[Symbol.iterator] = function () {
let that = this // 当前对象
let i = 0 // 迭代器的起点
const keys = Reflect.ownKeys(that) // 对象的键组成的数组
// 也可以写成 Object.keys(self).concat(Object.getOwnPropertySymbols(self))
return {
next: function () {
console.log('遍历到的对象', keys[i], i)
if (i >= keys.length) {
return {
value: undefined,
done: true
}
} else {
return {
value: that[keys[i++]], // 这里有一个写法 i++ 和++i的区别 i++是就读取值,再自增1
done: false
}
}
}
}
}
for (let value of obj) {
console.log(value)
}
我们看到,对象可以用我们规定的规则遍历了。我们看到迭代器返回的对象很像Generator函数的结构,所以我们可以结合Generator函数一起使用。
3.Generator函数
什么是Generator函数,怎么用?
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。generator和函数不同的是,generator由
function*定义(注意多出的*号),并且,除了return语句,还可以用yield返回多次。
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
看到这里我有好几个疑问,首先这个yield是什么鬼东西,多次返回是怎么实现的,有什么用?
要回答这个问题,我们就要去了解ES6的更强大、更完善的异步编程方法。
ES6的异步编程方法
异步操作就是一个任务可以不一定要在同一时间段从头执行到尾,到必要的资源没有得到满足时,可以暂停并执行其他任务,等资源得到满足时,再继续执行任务。这样的流程就是异步执行。
我们都知道js是单线程的,所谓单线程,就是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个,一次只能完成一件任务。
那么,我们在需要异步操作的时候,就需要一些方法去实现我们的需求,而不能使任务之间也是单线程的。
1.回调函数
回调函数是我们最常见的异步编程的解决方案。
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。 它的英语名字 callback,直译过来就是"重新调用"。
下面是我开发过程中使用回调函数处理异步任务的实现。
背景:echarts图表实例绑定了一个鼠标移出表格的事件以及点击图表元素的事件,在每次事件触发时,都需要执行我们的业务逻辑,但是事件的绑定函数是写在公共ts文件里的,因此事件绑定方法还要接收一个回调函数,用于每次事件触发时执行。
// utils.ts
// echarts 绑定点击事件
export function bindTooltipClick(chartInstance: echarts.ECharts, callback:(index: number, data: OptionDataItem) => void) {
chartInstance && chartInstance.on('click', params => {
callback(params.dataIndex, params.data)
})
}
// echarts鼠标移出事件
export function bindMouseOutChart(
chartInstance: echarts.ECharts,
callback: () => void
){
chartInstance && chartInstance.on('globalout', () => {
callback()
})
}
// 展示指定元素的tooltip
export function setDefaultSelect(chartInstance: echarts.ECharts, index: number) {
console.log('显示tooltip', index)
chartInstance && chartInstance.dispatchAction({
type: 'showTip',
// 用 index 或 id 或 name 来指定系列。
// 可以使用数组指定多x个系列。
seriesIndex: [0],
// 数据项的 index,如果不指定也可以通过 name 属性根据名称指定数据项
dataIndex: index
})
}
// app.svelte
let selectedIndex = 0
// 绑定点击事件
bindTooltipClick(chartInstance, (index, data) => {
selectedIndex = index
dispatch('tooltipIndexChange', { index: index, data: data })
})
// 绑定图表失去焦点事件
bindMouseOutChart(
chartInstance,
() => setDefaultSelect(chartInstance, selectedIndex)
)
一个有趣的问题是,为什么 Node.js 约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是 null)?原因是执行分成两段,在这两段之间抛出的错误,程序无法捕捉,只能当作参数,传入第二段。
2.Promise
回调函数本身并没有什么问题,但是从结构上看,通过嵌入的方式会很容易造成层层嵌套的场景,就是我们平时说的回调函数噩梦噩梦(callback hell),例如下面要处理多个文件:
fs.readFile(fileA, function (err, data) {
fs.readFile(fileB, function (err, data) { // ...
})
})
Promise就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的横向加载,改成纵向加载。采用Promise,连续读取多个文件,写法如下。
var readFile = require('fs-readfile-promise')
readFile(fileA)
.then(function (data) {
console.log(data.toString())
})
.then(function () {
return readFile(fileB)
})
.then(function (data) {
console.log(data.toString())
})
.catch(function (err) {
console.log(err)
})
上面代码中,我使用了 fs-readfile-promise 模块,它的作用就是返回一个 Promise 版本的 readFile 函数。Promise 提供 then 方法加载回调函数,catch方法捕捉执行过程中抛出的错误。
可以看到,Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
那么,有没有更好的写法呢?
3.协程
传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下。
第一步,协程A开始执行。
第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
第三步,(一段时间后)协程B交还执行权。
第四步,协程A恢复执行。
上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
举例来说,读取文件的协程写法如下。
function asnycJob() { // ...其他代码
var f = yield readFile(fileA); // ...其他代码
}
上面代码的函数 asyncJob 是一个协程,它的奥妙就在其中的 yield 命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。
协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。
4.Generator函数
在ES6里面新增了一个函数Generator,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
只有在Generator函数内部才能使用yield关键词
下面看看如何使用 Generator 函数,执行一个真实的异步任务。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了 yield 命令。
执行这段代码的方法如下。
var g = gen();
var result = g.next();
result.value.then(
function(data){
return data.json();
}).then(function(data){
g.next(data);
});
上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用 next 方法(第二行),执行异步任务的第一阶段。由于 Fetch 模块返回的是一个 Promise 对象,因此要用 then 方法调用下一个next 方法。
可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
4.Generator函数与迭代器的结合
让我们回顾一下上面我们自定义的迭代器,我们发现在返回的时候,有很多逻辑判断。但是我们观察这里的返回可以发现,和Generator函数中yield返回的结构是一样的{ value, done},所以我们可以结合Generator来实现迭代器。
// 自定义对象的迭代器
Object.prototype[Symbol.iterator] = function* () {
const keys = Reflect.ownKeys(this) // 对象的键组成的数组
// 使用yield返回
for (let key of keys) {
yield this[key]
}
}
for (let value of obj) {
console.log(value)
}
整个代码就豁然开朗了。