循环:由计算机底层提供的一种多次运行同一程序的机能,循环是这种机能的接口。
迭代:建立在遍历循环的基础上,遍历循环的每一部分叫做一次迭代。
遍历:针对一组数据而进行的按顺序抽取的行为。例如说,有一个数组[1, 2, 3, 4]
,现在按照数组的顺序依次抽取1, 2, 3, 4
。这样的行为称为遍历,而每次抽取的步骤称为迭代,特别注意:遍历是有序性的。
枚举:概率学上的名称,在一个集合当中,无序的抽取其中成员的过程叫做枚举。比如说:在一个班级内,老师按照花名册进行点名,每当念到一个学生的名字,学生都会站起来。这个时候存在两种概念:
- 如果老师是按照学号的顺序来抽取学生,那么就是一个有序的过程,是个遍历的过程。
- 如果老师按照花名册上的顺序抽取学生,而学生被抽取到的顺序与学生所在班级的座位进行对比,此时就是一个无序的过程,也是枚举的过程。
forEach
方法中的一些疑问
循环的一些特性
下面这个例子非常简单,也就是通过for
循环输出数组中所有的元素而已。但是我们仔细来看,for
循环到底做了一些什么事情呢?
我们上面对循环做出结论,循环是计算机底层提供的一种多次运行同一程序的机能。for
循环作为循环的一种方式,那么它本质上功能就是多次执行同一程序。那么在下面的例子中,for
循环多次执行的程序是什么呢?实际上是console.log(arr[i])
,console.log()
方法将数组元素输出到控制台中,实际上获取数组元素并不是for
循环、console.log()
的能力,而是arr[i]
的能力。
也就是说什么呢?循环本质上就是多次执行同一程序的机能接口。
const arr = [1, 2, 3, 4, 5];
for(var i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
特别注意:循环可以关闭终止吗?实际上是可以的,我们可以使用continue、break
关键字对循环进行不同的处理,将达到关闭循环的目的。
forEach 方法的问题展现
上面我们说到循环是可以终止关闭的,但是为什么在forEach
方法中就不能够使用continue、break
关键字呢?
特别注意:
因为forEach
方法是遍历方法,遍历是针对于什么?遍历针对于数据,遍历的过程必须完整,如果不完整的话就不是遍历。所以continue、break
这些关键字用在遍历方法中就是违背遍历逻辑的。
[1,2,3].forEach(item => {
if (item === 2) {
break;
}
});
有序性 和 无序性
什么是有序性?什么是无序性?
我们现在考虑一个问题:下面两个数组的数据意义是相同的吗?实际上这两个数组的数据意义并不相同,因为我们要考虑数组元素的顺序问题。例如:[1, 2, 3]
数组元素对应的下标分别是:0, 1, 2
,而[2, 1, 3]
数组元素对应的下标分别是:0, 1, 2
。正是因为数组元素对应的下标不同,所以导致数据结构不同,两个看似拥有相同元素的数组,其实本质上数据意义是不同的。
特别注意:这就说明数组是有序性的,有序的列表是可以进行迭代的。
Note:
字符串也是有序的列表:
比如说'abcd', 'bacd'
两个字符串,从数据意义角度上来说,它们两个是不同的。因为从表面上来本来就不是相同的字符串。正是因为字符串是有序的列表,所以有时候字符串能够用数组的方法,两者在某些方面是类似的。
[1, 2, 3]
[2, 1, 3]
那么我们再来思考一个问题:下面两个对象的数据意义是相同的吗?实际上这两个对象的数据意义是相同的,因为对象是无序的。对象中的a、b
只是表示着数据的属性,而这些属性并不存在顺序的说法。也就是说属性a
在属性b
前面,还是属性b
在属性a
前面都不会影响到数据意义。
特别注意:这就说明对象是无序性的,无序的列表是不可以进行迭代的。
{
a:1,
b:2
}
{
b:2,
a:1
}
Note:
为什么有序的列表可以迭代?而无序的列表不可以迭代呢?
因为我们上面说,迭代其实是遍历的每一部分。遍历是针对一组数组进行的有序抽取行为,并且要保证遍历的完整性。其实针对于迭代来说,迭代也是要必须按照顺序的。比如生活中程序版本的迭代,版本从1.0迭代到2.0,而不能从2.0迭代到1.0。所以迭代也要按照顺序进行。
类数组Array-like
是有序的?还是无序的?
现在我们需要思考的是类数组,那么类数组是有序的,还是无序的呢?
特别注意:类数组是干什么的呢?类数组目的是什么?
类数组本身是给DOM
使用的,我们能够发现通过get
系列方法获取到的DOM
集合是HTMLCollect
的形式,而通过query
系列方法获取到的DOM
集合是NodeList
。
也正是因为类似HTMLCollect
集合的原因:HTMLCollect
集合中要存在带有顺序的DOM
元素,因为DOM
元素在DOM
树中是存在顺序的。而HTMLCollect
集合中不仅仅要存储这些带有顺序的元素,还需要存储一些方法,属性之类的,所以类数组存在的目的就是为了能够让这些有序、无序的东西存放在一个集合中。
const obj = {
0:1,
1:2,
2:3,
3:4,
length: 4
}
// HTMLCollect
{
li: HTMLElement,
li: HTMLElement,
li: HTMLElement,
methods: {
method1,
method2,
method3
},
length: 3
}
特别注意:虽然类数组模拟数组的有序性,但是只是在形式上模拟数组的有序性。类数组本质上还是一个对象Object
,是无序性的。所以类数组不能够遍历、迭代。
探讨对象枚举
如果说,现在我有一个对象obj
,我想把obj
对象的属性值打印出来?我现在该怎么办呢?
const obj = {
a:1,
b:2,
c:3,
length: 3
}
那么用for
循环方式可以做的到吗?看似是没有办法做到的,因为此时变量i
并不对应着obj
对象中的属性名,所以获取不到相应的属性值。
for(var i = 0; i < obj.length; i++) {
console.log(obj[i]); // ???
}
此时我们可以通过Object.keys()
方法获取一个对象的属性集合。注意:Object.keys()
方法会返回一个由给定对象自身可枚举属性组成的数组。
为什么keys
数组中还包含'length'
属性呢?因为Object.keys()
返回的是一个给定对象自身可枚举属性组成的数组,而'length'
也是属于obj
对象的自身可枚举属性,所以自然能够获取的到。
const keys = Object.keys(obj);
console.log(keys); // ['a', 'b', 'c', 'length']
如果说我不想让'length'
属性能够获取到,也就是说将length
设置为不可枚举属性,可以做的到吗?实际上是可以做到的,我们可以通过Object.defineProperties()
方法进行定义属性,例如下面的例子:
我们将length
属性手动设置为不可枚举、不可配置、不可修改(当然,你也可以不用手动设置,因为defineProperties()
方法默认为false
),此时再利用Object.keys()
方法去获取对象属性集合时,就不能够枚举到length
属性。
const obj = {};
Object.defineProperties(obj, {
a: {
value:1,
writable: true,
enumerable: true,
configurable: true
},
b: {
value: 2,
writable: true,
enumerable: true,
configurable: true
},
c: {
value: 3,
writable: true,
enumerable: true,
configurable: true
},
length: {
value: 3,
writable: false,
enumerable: false,
configurable: false
}
});
console.log(Object.keys(obj)); // ['a', 'b', 'c']
那么我现在如果想知道一个对象中有哪些属性是不可枚举的,我该如何做呢?注意:此时我们可以通过Object.getOwnPropertyNames()
获取对象自身所有属性集合。
Object.getOwnPropertyNames()
方法与Object.keys()
方法对比,我们能够发现Object.getOwnPropertyNames()
方法能够获取到对象自身的所有属性,不论是否可枚举。而Object.keys()
只能够获取到对象可枚举属性。
const obj = {};
Object.defineProperties(obj, {
a: {
value:1,
writable: true,
enumerable: true,
configurable: true
},
b: {
value: 2,
writable: true,
enumerable: true,
configurable: true
},
c: {
value: 3,
writable: true,
enumerable: true,
configurable: true
},
length: {
value: 3,
writable: false,
enumerable: false,
configurable: false
}
});
console.log(Object.keys(obj)); // ['a', 'b', 'c']
console.log(Object.getOwnPropertyNames(obj)); // ['a', 'b', 'c', 'length']
如果说让我们写一个函数,只获取到对象自身不可枚举属性集合,我们又该如何封装呢?首先我们知道Object.getOwnPropertyNames()
方法能够获取对象自身的所有属性(不论是否可枚举),其次Object.keys()
能够获取到对象自身可枚举属性。所以我们可以通过过滤的方式,将所有属性集合中的可枚举属性过滤掉,那么就能够获取到所有不可枚举的属性了。
Object.prototype.getOwnPropertyNonEnumberable = function () {
// 保存当前this对象
var _this = this,
result = [];
// 获取自身可枚举属性
const enumerableKeys = Object.keys(_this);
// 获取自身所有属性
const allKeys = Object.getOwnPropertyNames(_this);
// 过滤
result = allKeys.filter(item => {
const index = enumerableKeys.indexOf(item);
// 如果当前属性名存在可枚举属性集合中,我们就过滤掉
return index == -1 ? true : false;
});
return result;
}
除了用Object.keys()
方法之外,我们还有哪些方式能够枚举对象属性呢?实际上我们还可以通过for..in
循环来枚举对象属性。注意:对象是无序的,所以不能说成遍历对象,而是枚举对象。比如说:
我们可以看到下面例子中,通过for...in
循环能够枚举对象的属性名,然后我们通过属性名去获取对应的属性值。for...in
也是枚举对象的一种方式。
const obj = {
a:1,
b:2,
c:3,
}
for(var key in obj) {
console.log(key, obj[key]); // a, 1 b, 2 c, 3
}
那么for...in
能够处理不可枚举的属性吗?从结果上来看,for...in
是没有办法获取到不可枚举的属性的,所以从一些方面上来看,for...in
与Object.keys()
方法还是挺类似的。
const obj = {};
Object.defineProperties(obj, {
a: {
value:1,
writable: true,
enumerable: true,
configurable: true
},
b: {
value: 2,
writable: true,
enumerable: true,
configurable: true
},
c: {
value: 3,
writable: true,
enumerable: true,
configurable: true
},
length: {
value: 3,
writable: false,
enumerable: false,
configurable: false
}
});
for(var key in obj) {
console.log(key); // a b c
}
既然for...in
与Object.keys()
方法挺相似,那么相似之处在哪呢?区别又在哪呢?
相同之处:其实本质上for...in
与Object.keys()
非常相似,除了返回值形式的不同。前者是返回属性名或者属性值,而后者返回的是属性名集合的数组形式。但是二者本质上都是处理对象的枚举问题,而且都没有处理不可枚举属性的能力。
不同之处在于:
for...in
其实利用的是in
运算符,in
运算符可以判断指定对象或其原型链中是否存在某个属性,例如a in obj
。也就是说for...in
循环有能力枚举到对象继承的属性。
Object.keys()
方法返回的数组中,只包含对象自身的可枚举属性。
Object.prototype.a = 100;
var obj = {};
Object.keys(obj); // []
for(var key in obj) {
console.log(key); // a
}
特别特别特别注意:我们之前说过,对象是无序的,也就是说对象内部属性并不存在顺序之说。那么我们枚举对象,为什么for...in
循环和Object.keys()
方法返回的对象属性顺序是按照我们定义时的顺序显示的呢?既然对象是无序的,那么枚举的时候自然也是无序的吖?
在MDN
文档上指出:for...in
枚举的顺序,按照现代ECMAScript
规范的遍历顺序,已经很好的定义和实现。首先它会按照所有非负整数键(那些可以是数组索引的键)将首先按值升序遍历,然后按属性创建的升序时间顺序遍历其它字符串键。
const obj = {
'2': 1,
'1': 3,
'3': 1
}
for(var key in obj) {
console.log(key); // 1 2 3
}
Object.keys()
方法枚举的顺序,MDN
文档上也明确指出:Object.keys()
返回数组的顺序与for...in
循环提供的顺序相同。
探讨迭代
Note:
for...of
和for...in
它是底层给我们开发层面抛出的API
,目的是为了能够实现循环迭代、枚举对象功能。既然是循环,那么就存在终止循环的方式,所以可以利用continue、break
去关闭循环。而像
forEach
这种遍历方法就不能够去使用continue、break
的关键字,因为遍历是针对数据的,数据要保持遍历的完整性。
我们上面说过for...in
是针对于对象的,而for...of
是针对可迭代对象的。什么是可迭代对象呢?可迭代对象例如:Array、Map、Set、String、TypedArray、arguments、nodeList
等,但是需要注意,可迭代对象必须是有序的对象。
也就是说,for...of
目的就是为了统一可迭代对象的循环迭代方式。
比如说,我们现在尝试用for...of
去循环迭代Array
和Object
,我们看看会发生什么事情?
实际上从例子中我们可以看到Array
是可以通过for...of
完成循环迭代的,但是Object
将会抛出异常,错误提示:obj
不是可迭代的。
为什么说对象是不可以迭代的呢?因为我们上面说过,对象是无序的,而for...of
是针对于可迭代对象,既然是可迭代对象,那它必须是有序的,迭代是要按照顺序执行的。
const arr = [1, 2, 3];
for(var item of arr) {
console.log(item); // 1 2 3
}
const obj = {
a:1,
b:2,
c:3
}
for(var [key, value] of obj) {
console.log(key, value); // Uncaught TypeErorr: obj is not iterable.
}
既然for...of
针对于可迭代对象,那么for...of
遍历循环可迭代对象时做了一些什么事情呢?
特别注意:我们先明确一个事情:如何判断对象是否能够被for...of
进行循环迭代呢?其实本质上要看对象自身或者原型上是否存在Symbol.iterator
属性。为什么要看是否存在Symbol.iterator
属性呢?因为Symbol.iterator
能够为每一个对象定义默认的迭代器,在for...of
循环遍历的时候,底层会自动去调用Symbol.iterator
。
换句话说,如果你想通过for...of
去迭代遍历某个对象,那么你就要看这个对象自身或者原型上是否存在Symbol.iterator
属性,如果存在就可以使用for...of
进行循环迭代,如果不存在的话,那么就不能够使用for...of
进行循环迭代。
比如说,我们看Object
和Array
的prototype
属性,在Array.prototype
上确实定义了Symbol.iterator
属性,而Object.prototype
上并没有定义Symbol.iterator
属性,所以普通的对象就不能够被for...of
进行迭代。
特别注意:为什么Symbol.iterator
这个属性要被Symbol
数据类型包装呢?在设计Symbol.iterator
的时候需要确保它的唯一性,也就是说这个属性不能够被外界覆盖。因为在底层执行for...of
的时候,需要调用Symbol.iterator
,如果Symbol.iterator
被覆盖的话将会导致程序的失败,所以要确保它的唯一性。
特别注意:从浏览器显示上来看,Symbol.iterator
属性本质上是一个函数。
**ES6**
实现**for...of**
循环迭代:
我们如何在ES6
中实现for...of
循环迭代的过程呢?
- 首先我们需要用到
Generator
生成器函数,生成器函数是干什么的呢?生成器调用之后返回一个生成器对象,并且它符合可迭代协议和迭代器协议。换句话说,就是生成器函数调用之后返回一个可迭代对象。 - 可迭代对象的形式是什么样子的呢?首先可迭代对象存在一个
next()
接口,这个接口主要承担着迭代的执行,也就是说每次调用一次next()
方法就会执行一次迭代的过程。而next()
每一次执行都会返回一个对象,这个对象中存在value/done
属性,其中value
属性对应着yield
产出的值,done
属性对应着整个迭代过程是否结束。 yield
又是什么意思呢?yield
其实是产出的意思。那么yield
产出的值是什么呢?比如:yield iteratorObject[i]
,此时yield
产出的值就是iteratorObject[i]
的值。那么yield
产出的值,对应next()
方法返回的对象其中的属性value
值。
function * generator(iteratorObject) {
for(var i = 0; i < iteratorObject.length; i++) {
yield iteratorObject(i);
}
}
const iterator = generator([1, 2, 3]);
iterator.next();
iterator.next();
iterator.next();
iterator.next();
特别特别注意:
明白整体的迭代过程之后,我们再细致的分析一下具体的代码执行流程:
- 首先调用
generator([1,2,3])
生成器函数,生成器函数执行返回生成器对象,这个生成器对象符合迭代协议,也可以称为迭代对象。特别注意:生成器函数调用时,生成器函数内部并不会立即执行,而是当next()
接口执行时,生成器函数内部才会执行。调用生成器函数,只是会返回一个生成器对象,并且这个对象符合迭代协议。
const iterator = generator([1, 2, 3]);
console.log(iterator);
- 非常重要的一步:我们可以看到
iterator
迭代对象在[[prototype]]
属性上存在next()
方法,这个next()
方法将会驱动迭代的执行,也就是说调用一次next()
,就会执行一次迭代过程。next()
接口调用之后,将会返回一个对象,这个对象内部存在value/done
属性。其中value
属性表示:yield
关键字产出的值;done
属性表示:整个迭代流程是否结束。
function* generator(iteratorObject) {
for (var i = 0; i < iteratorObject.length; i++) {
yield iteratorObject[i];
}
}
const iterator = generator([1, 2, 3]);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: true }
console.log(iterator.next()); // { value: undefined, done: true }
- 特别重要的一步:
yield
关键字的作用是:- 产出值,比如例子中的
yield iteratorObject[i]
,此时yield
将产出的值iteratorObject[i]
对应next()
方法返回的对象value
属性值。 - 暂停,当
next()
方法调用时,进行迭代的步骤,此时generator
函数内部开始执行程序,遇到yield
关键字时,程序将会暂停,暂停的同时yield
将值进行产出。当下一个next()
方法执行的时候,程序会从暂停的位置重新恢复执行。也就是说,相当于yield
是暂停迭代,next()
接口重新恢复迭代。
- 产出值,比如例子中的
- 整体迭代流程完成。
**ES5**
实现**for...of**
循环迭代:
熟悉for...of
循环迭代的过程,我们现在用ES5
实现起来也比较简单了。
注意实现的基础还是Symbol.iterator
属性,我们知道for...of
循环迭代的时候,底层会去调用Symbol.itertator
。我们此时只要实现Symbol.iterator
方法即可。
注意我们实现的细节:
- 实现的思路很简单,也就是上述
for...of
迭代的流程,实现next()
接口,产出值,next()
返回对象,对象属性中存在value、done
属性。 - 注意
index
变量是私有变量,注意next()
接口是闭包函数。
function generator(iteratorObject) {
// 私有变量index
let index = 0;
// next()方法,迭代接口,闭包函数
function next() {
return index < iteratorObject.length ? { value: iteratorObject[index++], done: false}
: { value: undefined, done: true }
}
return {
next
}
}
const iterator = generator([1, 2, 3]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
迭代对象:
迭代对象这种说法有点歧义,为什么呢?因为我们说过for...of
迭代的是可迭代对象,而可迭代对象都是有序列表。而对象是无序的,所以理论上对象是没有办法直接被for...of
进行循环迭代的。但是我们有没有方法能够让for...of
进行循环迭代对象呢?
实际上是可以做到的,虽然对象是无序的,是不可迭代的。但是如果我们给对象手动添加Symbol.iterator
接口的话,那么for...of
在执行的时候,就会去对象上寻找Symbol.iterator
接口,这样就能够实现for...of
循环迭代对象。
例如下面的例子:
整体逻辑实现的思路并不是很难,本质上都是和上面例子的流程都是一个道理,在这里我们就不多介绍了。我们要注意一个很重要的问题:
特别注意:仔细对比obj
数据结构与for...of
迭代出来的结果,你会发现迭代结果顺序和obj
数据结构顺序不同。这是为什么呢?这和我们之前说的迭代怎么不一样呢?迭代不是按照顺序来的吗?我们如果按照迭代的理论,那么得到的结果应该是'2' 1 , '1' 2, '3' 3
。
但是问题并不出在迭代上,而是出在对象本身。我们知道对象本身是无序的,我们迭代的顺序是依据Object.keys()
方法返回的数组顺序**。什么意思呢?也就是说Object.keys()
会返回数组,数组中存储的是对象自身可枚举属性键名,数组中的顺序与for...in
循环枚举对象的顺序相同。这个顺序,之前我们在MDN
看过,其实就是ECMAScript
规定:**首先它会按照所有非负整数键(那些可以是数组索引的键)将首先按值升序遍历,然后按属性创建的升序时间顺序遍历其它字符串键。
那么这说明什么问题呢?这就说明对象虽然是通过for...of
进行迭代的(理论上应该是有序的),但是依旧不能够保证与对象定义时属性的顺序一致(实际上依旧是无序的)。
Note:
不要单纯的认为for...of
循环迭代是有序的。然后你就用for...of
去迭代普通对象,实际上你得到的结果可能与你预期的不同。因为对象是无序的,所以你并不能依靠迭代去保证迭代出来的属性顺序。如果说你确实要保证对象属性迭代的顺序,你就将迭代产出的值按照对象定义时的顺序进行产出。
const obj = {
'2':1,
'1':2,
'3':3
}
// 实现Symbol.iterator接口
Object.prototype[Symbol.iterator] = function() {
// 保存this指向
var _this = this;
// 获取对象自身可枚举键值
var keys = Object.keys(_this); // 作为迭代的顺序来使用,此时属性顺序已经发生改变
// 私有变量index
var index = 0;
// 实现next接口
function next() {
return index < keys.length
? { value: [keys[index], _this[keys[index++]]], done: false}
: { value: undefined, done: true }
}
return {
next
}
}
for(var [key, value] of obj) {
console.log(key, value);
// 1 2
// 2 1
// 3 3
}