引
pseudo
英 ['sju:dəʊ] 美 ['su:doʊ]
adj.假的,虚伪的
n.[口]假冒的人,伪君子
pseudo-array
英 [s'ju:dəʊər'eɪ] 美 [s'ju:dəʊər'eɪ]
[计] 伪数组
jQuery 对象是伪数组
两个事实
不管以前知道不知道,至少马上会知道
jQuery 对象是一个伪数组。
const $obj = jQuery(); Array.isArray($obj); // false
jQuery.fn
是jQuery.prototype
的简写jQuery.fn === jQuery.prototype; // true
提出问题
既然 jQuery 对象是伪数组,那 ES6 的 for...of 想用在 jQuery 对象上就不会那么顺利。毕竟 jQuery 还没有按 ES6 重写。
那么,想用 for..of 遍历 jQuery 对象中的 DOM 引用,就自己实现吧——这得从 iterable 和 iterator 开始。
jQuery 在进步,我也是
本文是基于 jQuery 对象不能使用 for ... of
来写的,但其实 jQuery 从 1.12/2.2/3.0 开始已经支持 for ... of
了,详情请阅读:
jQuery 虽然已经实现了对 for ... of
的支持,但是并不影响本文的有效性。只是做实验的时候需要 jQuery 1.12/2.2.0 以下的版本。
Iterable(可迭代对象) 和 Iterator(迭代器)
不以规矩,不成方圆 —— Iteration Protocols
为了使某个对象成为可迭代对象象,它必须实现 @@iterator
方法,也就是说,它得有一个 key 是 Symbol.iterator
的属性。说人话,就是必须得在 jQuery 对象上实现这么个东东:
jQuery.fn[Symbol.iterator] = ...
而这个所谓的 @@iterator
方法,返回的是一个迭代器 (iterator)。迭代器这活也不是随便谁都能干的,它必须得遵循迭代协议。
按照协议,迭代器有一个 next()
方法,而这个 next()
方法每次调用,都返回下一个迭代对象。当然迭代对象也是有标准的,它必须是这么个结构:
{
done: "(boolean),true 表示迭代完成,false 表示还有下一个",
value: "这个才是正主,for...of 迭代出来的值"
}
注意 done
这个小坑,其它语言中通常是用 hasNext()
或者 hasMore()
之类的来判断是否有下一个值,而 javascript 是用 done
来判断,它们的逻辑意思正好相反,所以千万注意不要给错了值。
实现
知道了规矩,实现起来就好办了
jQuery.fn[Symbol.iterator] = function () {
let index = 0;
return {
next: () => ({
done: index >= this.length,
value: this[index++]
})
}
}
测试
for (const div of $("div")) {
console.log("[DEBUG]", div);
}
正确执行,通过……话虽如此,代码写起来好累。如果用 Generator 情况会好得多。
Generator
ES6 的又一新特性,Generator 对象(生成器对象),简直就是为迭代而生的。每个生成器对象都符合上面提到的 Iterable 和 Iterator 两个规矩。换句话说,生成器对象既是一个可迭代对象,又是一个迭代器,而它作为可迭代对象的时候,返回的迭代器就是它自己。
然而生成器对象并不是 new 出来的,而是通过 Generator Function(生成器函数)生成的;生成器函数得自己写,又不能 new Generator()
,那么这个生成器对象从哪里来呢?当然是生成器函数生成的,而且这会用到新语法,以及新的关键字 yield
。
Generator Function(生成器函数)和 yield 关键字
生成器函数的定义与普通函数略有不同,形式上的区别是,它在 function
关键字后加了一个 *
号,就像这样:
function* aGenerator() { ... }
与普通函数相比,生成器函数并不直接执行业务逻辑,而是返回一个生成器对象。
如果不是很明白,来捋一捋:
- 生成器函数返回一个生成器对象
- 生成器对象是一个迭代器
- 生成器对象也是一个可迭代对象,每次迭代返回自己(这句知晓即可)
- 迭代器有一个
next()
方法用来返回迭代值(以及判断是否完成迭代)
捋清楚了,生成器函数干的,其实主浊上面最后一条描述的事情:描述每次迭代返回的值,以及是否完成迭代。这是与普通 function
完全不同的语法,它是怎么做到的呢?凭空说起来太吃力,先上代码
function* aGenerator() { yield 1; yield 2; yield 3;}
每次 yield
,就相当于一次通过 next()
返回值,也就上面提到的迭代对象的 value
属性。那么 done
属性如何确定呢?如果从生成器函数返回,就 done 了。这有两种情况,一种是自然执行完所有语法,函数结束返回;另一种是在函数体中调用 return
返回。
新问题:眼看例中 3 个 yield
语句排在一起,不是“啪啪啪”一下子就搞完了?最后就 next()
出来一个 3
了事?
非也,yield
返回值与 return
不同。yield
反回值(也就是 next()
)之后会将代码暂停在那个位置,等下一次 next()
的时候,继续执行,到下一个 yield
再暂停……如此直到显示或隐匿的 return
。
改进的 jQuery.fn[Symbol.iterator]
jQuery.fn[Symbol.iterator] = function* () { for (let i = 0; i < this.length; i++) { yield this[i]; }}
比上一个实现简单了不少吧?!别急,还有更简单的
巧妙的实现
更简单的实现
除了可以用 yield
返回值之外,还可以用 yield *
返回可迭代对象。这时,控制权会暂时交给这个可迭代对象,由它接替实现 next()
,直到 done
,再由当前生成器函数中的下一个 yield
接手继续。形象一点的理解——这个过程有点像树型结构的深度遍历。
因为原生数组也是可迭代对象,所以可以取个巧,把 jQuery 这个伪数组变成真数组,有两个方法
- 使用
jQuery.fn.toArray()
方法(推荐),这是 jQuery 自己提供的把伪数组转换为真数组的方法 使用
Array.prototype.slice
来提取
jQuery.fn[Symbol.iterator] = function* () { yield* this.toArray(); // 或 // yield* [].slice.call(this);}
不过,还可以再简单一些……
最简单的实现
上面说了一通,用了 N 种方法,无非是讲解 ES6 的新特性而已。要为 jQuery 实现 for...of 遍历,最简单的方法其实是拿来主义:
jQuery.fn[Symbol.iterator] = [][Symbol.iterator];
或者用语义更明确一些的写法:
jQuery.fn[Symbol.iterator] = Array.prototype[Symbol.iterator];
最后的提醒
jQuery 对象是一个伪数组,它的每一个元素都是一个 DOM(或原对象)而不是被封装的 jQuery 对象,所以,用 for..of
遍历的时候,和用 jQuery.fn.each()
遍历一样,如果想继续在每个元素上使用 jQuery 的特性,就要记得用 jQuery()
包装。
// for...offor (const span of $("span")) { var $span = $(span);}// jQuey.fn.each$("span").each(function() { const $span = $(this);});
本文以扩展 jQuery 的迭代器能力为例讲了迭代器的几种实现方法,在自己实现或扩展可迭代对象的时候还是很有用的。
更新说明
2021-07-04 更新
- 补充了对 jQuery 版本选择 的说明(较新版本的 jQuery 已经支持
for ... of
) - 更新示例代码,全部使用 ES6+ 语法,特别是避免使用
var
- 改善部分描述文字,使之更清楚、准确
- 修订部分错误(有两处代码都把
for ... of
写成了for ... in
,😂)