thief系列之三:从实现链式调用中看类数组对象与级联
youngwind opened this issue · 0 comments
问题
让我们回顾一下之前我们已经完成的功能。
- 获取dom元素
- 添加类
- 删除类
跟jquery对比我们就会发现目前我们的代码还有以下问题:
- 选择器返回的是一个对象,而jquery返回的是一个数组
- jquery支持链式调用,也就是$('h1').addClass('aaa').removeClass('aaa'),但是我们这样用的时候就会报错。
类数组对象
我们先来看第一个问题。
我的代码之前是通过把dom元素存储在this.dom中,this还有addClass、removeClass等方法,this是一个对象。但是,jquery返回的居然是一个数组,而且关键是这个数组还能有addClass这些方法!我觉得这是让我非常疑惑的地方。
我去查看jquery的源码,发现了这个
jQuery.fn = jQuery.prototype = {
// The default length of a jQuery object is 0
length: 0,
splice: arr.splice
};
我很奇怪为什么需要定义一个length属性呢?prototype不是一个对象吗?而且splice方式不是array才有的方法吗?
后来google到这篇文章,让我豁然开朗。http://mao.li/javascript/array-like-objects-in-javascript/
没错,问题的核心就是类数组对象:这是一个对象,但是它具有数组的特性。举个例子。
var a = {
id: 1,
name: "youngwind"
};
for (var i = 0; i < a.length; i++) {
console.log(a[i])
}
这段代码运行的结果是:没有任何输出。
因为a是一个对象,而且它没有length属性,a.length是undefined,所以循环不会被执行。
我们来改造一下这段代码
var a = {
0: 1,
1: "youngwind",
length: 2,
};
for (var i = 0; i < a.length; i++) {
console.log(a[i])
}
执行结果:输出1,youngwind
到目前为止依然没什么大问题。因为对象获取属性的值本来就可以通过[]来获取。
我再改一下代码。
var a = {
0: 1,
1: "youngwind"
};
for (var i = 0; i < a.length; i++) {
console.log(a[i])
}
Array.prototype.push.call(a, 'blabal')
console.log(a);
程序执行结果:循环没有输出。最后输出{0: "blabal", 1: "youngwind", length: 1}
有没有觉得很奇怪?为什么a明明是一个对象,却可以使用数组的push方法?而且调用之后第0项被重置,还多了一个length属性!而且length属性为毛是1啊!我明明有两个属性啊!
犀牛书上7.11章节给出了答案:
这个一个类数组对象,其实在js中,对象和数组的区别并没有看起来那么大
所谓数组背后的实现也不过是key值为非负整数的对象而已。这就不难解释为什么对象可以使用数组的方法了,虽然使用的结果会出现很怪异的情况,就像上面的例子。
ok,我们就到这儿。不去深究更多的数组和对象的细微区别。现在我想知道的是:
如何让一个对象表现得像数组?也就是说,如果让T('selector')返回的对象在浏览器中被解析成数组,而且我通过数组的各种方法去操作这个对象的时候,不会出现怪异的情况。
jquery的源码和刚刚那篇文章给了我们指引。
再改代码
var a = {
0: 1,
1: "youngwind",
length: 2,
splice: [].splice
};
for (var i = 0; i < a.length; i++) {
console.log(a[i])
}
ok,这样我们终于可以返回一个类数组了!这也为后面的链式调用做好了准备。
链式调用
所谓链式调用,也就是《js语言精粹》中提到的级联,每次执行完方法都返回this,这样就可以一个接口一个接口的调用下去了。有了上面的准备,this既是一个拥有各种方法的对象,也是一个可以直接遍历的类数组,所以我们可以直接return this作为每个函数的返回了。下面给出代码。
T.prototype = {
splice: [].splice,
length: 0,
init: function (selector) {
var ele = document.querySelectorAll(selector);
for (var i = 0; i < ele.length; i++) {
this[i] = ele[i];
}
this.length = ele.length;
return this;
},
addClass: function (className) {
for (var i = 0; i < this.length; i++) {
this[i].classList.add(className);
}
return this;
},
removeClass: function (className) {
for (var i = 0; i < this.length; i++) {
this[i].classList.remove(className);
}
return this;
}
};