一、若一个对象拥有迭代行为,比如可以应用for..of的对象,便是一个可迭代(Iterable)对象。
1、一些内置对象,如Array或Map拥有默认的迭代行为,而其他类型(比如Object)则没有。
二、为了实现可迭代,一个对象必须实现@@iterator方法,这意味着这个对象(或其原型链中的任意一个对象)必须具有一个带Symbol.iterator键(key)的属性。
三、可以多次迭代一个迭代器,或者只迭代一次。
1、只迭代一次的Iterables(如Generators)通常从它们的@@iterator方法中返回它本身。
2、可以多次迭代的方法必须在每次调用@@iterator时返回一个新的迭代器。
Symbol.iterator
一、通过自己创建一个对象,我们就可以轻松地掌握可迭代的概念。
二、例如,我们有一个对象,它并不是数组,但是看上去很适合使用for..of循环。
【示例1】比如一个range对象,它代表了一个数字区间:
let range = {
from: 1,
to: 5
};
// 我们希望 for..of 这样运行:
// for(let num of range) ... num=1,2,3,4,5
1、为了让range对象可迭代(也就是让for..of可以运行)我们需要为对象添加一个名为Symbol.iterator的方法(一个专门用于使对象可迭代的内置 symbol)。
(1)当for..of循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个迭代器(iterator)—— 一个有next方法的对象。
(2)从此开始,for..of仅适用于这个被返回的对象。
(3)当for..of循环希望取得下一个数值,它就调用这个对象的next()方法。
(4)next()方法返回的结果的格式必须是{done: Boolean, value: any},当done=true时,表示迭代结束,否则value是下一个值。
2、这是带有注释的range的完整实现:
let range = {
from: 1,
to: 5
};
// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function() {
// 它返回迭代器对象(iterator object):
// 2. 接下来,for..of 仅与此迭代器一起工作,要求它提供下一个值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// 现在它可以运行了!
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}
console.log(range)
三、请注意可迭代对象的核心功能:关注点分离。
- range自身没有next()方法。
- 相反,是通过调用rangeSymbol.iterator创建了另一个对象,即所谓的“迭代器”对象,并且它的next会为迭代生成值。
1、因此,迭代器对象和与其进行迭代的对象是分开的。
四、从技术上说,我们可以将它们合并,并使用range自身作为迭代器来简化代码。
【示例1】
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}
1、现在rangeSymbol.iterator返回的是range对象自身:它包括了必需的next()方法,并通过this.current记忆了当前的迭代进程。这样更短,对吗?是的。有时这样也可以。
2、但缺点是,现在不可能同时在对象上运行两个for..of循环了:它们将共享迭代状态,因为只有一个迭代器,即对象本身。但是两个并行的for..of是很罕见的,即使在异步情况下。
四、Symbol.iterator
1、技术上来说,可迭代对象必须实现Symbol.iterator方法。
(1)objSymbol.iterator的结果被称为迭代器(iterator)。由它处理进一步的迭代过程。
(2)一个迭代器必须有next()方法,它返回一个{done: Boolean, value: any}对象,这里done:true表明迭代结束,否则value就是下一个值。
2、Symbol.iterator方法会被for..of自动调用,但我们也可以直接调用它。
无穷迭代器(iterator)
一、无穷迭代器也是可能的。
【示例1】将range设置为range.to = Infinity,这时range则成为了无穷迭代器。
【示例2】或者我们可以创建一个可迭代对象,它生成一个无穷伪随机数序列。也是可能的。
1、next没有什么限制,它可以返回越来越多的值,这是正常的。
2、当然,迭代这种对象的for..of循环将不会停止。但是我们可以通过使用break来停止它。
自定义的可迭代对象
一、可以像这样实现自己的可迭代对象
var myIterable = {
*[Symbol.iterator]() {
yield 1
yield 2
yield 3
}
}
for (let value of myIterable) {
console.log(value)
}
// 1
// 2
// 3
// 或者
[...myIterable] // [1, 2, 3]
内置可迭代对象
一、String、Array、TypedArray、Map和Set都是内置可迭代对象,因为它们的原型对象都拥有一个Symbol.iterator方法。
字符串是可迭代的
一、数组和字符串是使用最广泛的内建可迭代对象。
二、对于一个字符串,for..of遍历它的每个字符:
for (let char of "test") {
// 触发 4 次,每个字符一次
alert( char ); // t, then e, then s, then t
}
1、字符串迭代器能够识别代理对(surrogate pair)(译注:代理对也就是指 UTF-16 的扩展字符)
let str = '𝒳😂';
for (let char of str) {
alert( char ); // 𝒳,然后是 😂
}
用于可迭代对象的语法
一、一些语句和表达式专用于可迭代对象,例如for…of循环,展开语法,yield*和解构赋值
// for...of循环
for (let value of ['a', 'b', 'c']) {
console.log(value)
}
// 'a'
// 'b'
// 'c'
// 展开语法
[...'abc'] // ['a', 'b', 'c']
// yield*
function* gen() {
yield* ['a', 'b', 'c']
}
gen().next() // { value: 'a', done: false }
// 解构赋值
[a, b, c] = new Set(['a', 'b', 'c'])
a // 'a'
可迭代(iterable)和类数组(array-like)
一、有两个看起来很相似,但又有很大不同的正式术语。请你确保正确地掌握它们,以免造成混淆。
- Iterable 如上所述,是实现了Symbol.iterator方法的对象(for..of对它们有效)。
- Array-like是有索引和length属性的对象,所以它们看起来很像数组。
二、当我们将 JavaScript 用于编写在浏览器或其他环境中的实际任务时,我们可能会遇到可迭代对象或类数组对象,或两者兼有。
1、字符串既是可迭代的(for..of对它们有效),又是类数组的(它们有数值索引和length属性)。
2、但是一个可迭代对象也许不是类数组对象。反之亦然,类数组对象可能不可迭代。
【示例1】上面例子中的range是可迭代的,但并非类数组对象,因为它没有索引属性,也没有length属性。
【示例2】下面这个对象则是类数组的,但是不可迭代:
let arrayLike = { // 有索引和 length 属性 => 类数组对象
0: "Hello",
1: "World",
length: 2
};
// Error (no Symbol.iterator)
for (let item of arrayLike) {}
三、可迭代对象和类数组对象通常都不是数组,它们没有push和pop等方法。
类数组
见类型数组:https://www.yuque.com/tqpuuk/yrrefz/eorzxp
可迭代对象 / 类数组对象 转换的几种方式
一、如果是可迭代对象或类数组对象,我们想像数组那样操作它。例如,我们想使用数组方法操作range,应该如何实现呢?
扩展运算符(ES6 )
aSuncat-20210707:我在浏览器测试的时候报错了:Uncaught TypeError: arrayLike is not iterable
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
var arr1 = [...arrayLike]; // ['a','b','c']
Array.from (推荐)
一、有一个全局方法Array.from可以接受一个可迭代或类数组的值,并从中获取一个“真正的”数组。然后我们就可以对其调用数组方法了。
| ☆-1【示例】类数组```javascript let arrayLike = { 0: “Hello”, 1: “World”, length: 2 };
// Array.from方法接受对象,检查它是一个可迭代对象或类数组对象,然后创建一个新数组,并将该对象的所有元素复制到这个新数组 let arr = Array.from(arrayLike); // [‘hello’, ‘world’] alert(arr.pop()); // World(pop 方法有效)
|
| --- |
| ☆-1【示例】如果是可迭代对象,也是同样:```javascript
// 假设 range 来自上文的例子中
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (数组的 toString 转化方法生效)
| | —- |
语法
一、Array.from的完整语法允许我们提供一个可选的“映射(mapping)”函数:
Array.from(obj[, mapFn, thisArg])
- 可选的第二个参数mapFn可以是一个函数,该函数会在对象中的元素被添加到数组前,被应用于每个元素
- thisArg允许我们为该函数设置this。 | ☆-1【示例】```javascript // 假设 range 来自上文例子中
// 求每个数的平方 let arr = Array.from(range, num => num * num);
alert(arr); // 1,4,9,16,25
|
| --- |
| 【示例】用Array.from将一个字符串转换为单个字符的数组:```javascript
let str = '𝒳😂';
// 将 str 拆分为字符数组
let chars = Array.from(str); // ['𝒳', '😂']
console.log(str.split('')); // ['\uD835', '\uDCB3', '\uD83D', '\uDE02']
alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2
2、与str.split方法不同,它依赖于字符串的可迭代特性。因此,就像for..of一样,可以正确地处理代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。)
3、技术上来讲,它和下面这段代码做的是相同的事:```javascript
let str = ‘𝒳😂’;
let chars = []; // Array.from 内部执行相同的循环 for (let char of str) { chars.push(char); }
alert(chars);
(1)但Array.from精简很多。 |
| --- |
| 项目【示例】创建项递增的数组```javascript
const arr = Array.from({ length: 100 }, (_, i) => i +1 );
console.log(arr); // [1, 2, 3, 4, 5, ..., 100]
| | —- |
应用
一、将字符串转为数组,然后返回字符串的长度。
| 【示例】```javascript function countSymbols(string) { return Array.from(string).length; }
|
| --- |
二、我们甚至可以基于Array.from创建代理感知(surrogate-aware)的slice方法(译注:也就是能够处理 UTF-16 扩展字符的slice方法):
| 【示例】```javascript
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
let str = '𝒳😂𩷶';
alert( slice(str, 1, 3) ); // 😂𩷶
// 原生方法不支持识别代理对(译注:UTF-16 扩展字符)
alert( str.slice(1, 3) ); // 乱码(两个不同 UTF-16 扩展字符碎片拼接的结果)
| | —- |
Array.prototype.slice.call()(ES5)
一、Array.prototype.slice.call(arguments)能将具有length属性的对象转成数组,除了IE下的节点集合(因为ie下的dom对象是以com对象的形式实现的,js对象与com对象不能进行转换)
function test(a,b,c,d) {
var arg = Array.prototype.slice.call(arguments, 1);
console.log(arg);
}
test("a","b","c","d"); // ['b', 'c', 'd']
二、Array.prototype.slice.call()
- 第一个参数是context(就是上下文的意思),用来替换对象函数中的this
- 第二个参数是传递给对象函数的参数