thief系列之三:从实现链式调用中看类数组对象与级联

@youngwind 2016-04-24 10:34:56发表于 youngwind/blog JQuery

问题

让我们回顾一下之前我们已经完成的功能。

  1. 获取dom元素
  2. 添加类
  3. 删除类

但是,还存在这样的问题。如图
2016-04-24 6 27 15

跟jquery对比我们就会发现目前我们的代码还有以下问题:

  1. 选择器返回的是一个对象,而jquery返回的是一个数组
  2. 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])
  }

结果如图所示
2016-04-24 7 19 22

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;
    }
  };

效果图
2016-04-24 7 18 38