循环、迭代、遍历、枚举

循环:由计算机底层提供的一种多次运行同一程序的机能,循环是这种机能的接口。
迭代:建立在遍历循环的基础上,遍历循环的每一部分叫做一次迭代。
遍历:针对一组数据而进行的按顺序抽取的行为。例如说,有一个数组[1, 2, 3, 4],现在按照数组的顺序依次抽取1, 2, 3, 4。这样的行为称为遍历,而每次抽取的步骤称为迭代,特别注意:遍历是有序性的。
枚举:概率学上的名称,在一个集合当中,无序的抽取其中成员的过程叫做枚举。比如说:在一个班级内,老师按照花名册进行点名,每当念到一个学生的名字,学生都会站起来。这个时候存在两种概念:

  1. 如果老师是按照学号的顺序来抽取学生,那么就是一个有序的过程,是个遍历的过程。
  2. 如果老师按照花名册上的顺序抽取学生,而学生被抽取到的顺序与学生所在班级的座位进行对比,此时就是一个无序的过程,也是枚举的过程。

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...inObject.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...inObject.keys()方法挺相似,那么相似之处在哪呢?区别又在哪呢?
相同之处:其实本质上for...inObject.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循环提供的顺序相同。
image.png

探讨迭代

Note:
for...offor...in它是底层给我们开发层面抛出的API,目的是为了能够实现循环迭代、枚举对象功能。既然是循环,那么就存在终止循环的方式,所以可以利用continue、break去关闭循环。而像
forEach这种遍历方法就不能够去使用continue、break的关键字,因为遍历是针对数据的,数据要保持遍历的完整性。

我们上面说过for...in是针对于对象的,而for...of是针对可迭代对象的。什么是可迭代对象呢?可迭代对象例如:Array、Map、Set、String、TypedArray、arguments、nodeList等,但是需要注意,可迭代对象必须是有序的对象。
也就是说,for...of目的就是为了统一可迭代对象的循环迭代方式。
比如说,我们现在尝试用for...of去循环迭代ArrayObject,我们看看会发生什么事情?
实际上从例子中我们可以看到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进行循环迭代。
比如说,我们看ObjectArrayprototype属性,在Array.prototype上确实定义了Symbol.iterator属性,而Object.prototype上并没有定义Symbol.iterator属性,所以普通的对象就不能够被for...of进行迭代。
特别注意:为什么Symbol.iterator这个属性要被Symbol数据类型包装呢?在设计Symbol.iterator的时候需要确保它的唯一性,也就是说这个属性不能够被外界覆盖。因为在底层执行for...of的时候,需要调用Symbol.iterator,如果Symbol.iterator被覆盖的话将会导致程序的失败,所以要确保它的唯一性。
特别注意:从浏览器显示上来看,Symbol.iterator属性本质上是一个函数。
image.png
**ES6**实现**for...of**循环迭代:
我们如何在ES6中实现for...of循环迭代的过程呢?

  1. 首先我们需要用到Generator生成器函数,生成器函数是干什么的呢?生成器调用之后返回一个生成器对象,并且它符合可迭代协议和迭代器协议。换句话说,就是生成器函数调用之后返回一个可迭代对象。
  2. 可迭代对象的形式是什么样子的呢?首先可迭代对象存在一个next()接口,这个接口主要承担着迭代的执行,也就是说每次调用一次next()方法就会执行一次迭代的过程。而next()每一次执行都会返回一个对象,这个对象中存在value/done属性,其中value属性对应着yield产出的值,done属性对应着整个迭代过程是否结束。
  3. 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();

循环、迭代、遍历、枚举_第1张图片
特别特别注意:
明白整体的迭代过程之后,我们再细致的分析一下具体的代码执行流程:

  1. 首先调用generator([1,2,3])生成器函数,生成器函数执行返回生成器对象,这个生成器对象符合迭代协议,也可以称为迭代对象。特别注意:生成器函数调用时,生成器函数内部并不会立即执行,而是当next()接口执行时,生成器函数内部才会执行。调用生成器函数,只是会返回一个生成器对象,并且这个对象符合迭代协议。
const iterator = generator([1, 2, 3]);
console.log(iterator); 

循环、迭代、遍历、枚举_第2张图片

  1. 非常重要的一步:我们可以看到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 }
  1. 特别重要的一步:yield关键字的作用是:
    1. 产出值,比如例子中的yield iteratorObject[i],此时yield将产出的值iteratorObject[i]对应next()方法返回的对象value属性值。
    2. 暂停,当next()方法调用时,进行迭代的步骤,此时generator函数内部开始执行程序,遇到yield关键字时,程序将会暂停,暂停的同时yield将值进行产出。当下一个next()方法执行的时候,程序会从暂停的位置重新恢复执行。也就是说,相当于yield是暂停迭代,next()接口重新恢复迭代。
  2. 整体迭代流程完成。

**ES5**实现**for...of**循环迭代:
熟悉for...of循环迭代的过程,我们现在用ES5实现起来也比较简单了。
注意实现的基础还是Symbol.iterator属性,我们知道for...of循环迭代的时候,底层会去调用Symbol.itertator。我们此时只要实现Symbol.iterator方法即可。
注意我们实现的细节:

  1. 实现的思路很简单,也就是上述for...of迭代的流程,实现next()接口,产出值,next()返回对象,对象属性中存在value、done属性。
  2. 注意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
}

你可能感兴趣的:(前端集合,前端,javascript,开发语言)