深入理解JavaScript类数组

sapworker 8年前
   <p>写这篇博客的起因,是我在知乎上回答一个问题时,说自己在学前端时把《JavaScript高级程序设计》看了好几遍。<br> 于是在评论区中,出现了如下的对话:<br> <img src="https://simg.open-open.com/show/86c4ab33c842c20f9b43818627c359c4.png"></p>    <p>天啦噜,这话说的,宝宝感觉到的,是满满的恶意啊。还好自己的JavaScript基础还算不错,没被打脸。(吐槽一句:知乎少部分人真的是恶意度爆表,整天想着打别人的脸。都是搞技术的,和善一点不行吗…………)</p>    <p>不过这个话题也引起了我的注意,问了问身边很多前端同学关于数组与类数组的区别。他们都表示不太熟悉,所以决定写一篇博客,来分享我对数组与类数组的理解。</p>    <h2>什么是类数组</h2>    <p>类数组的定义,有如下两条:</p>    <ul>     <li>具有:指向对象元素的数字索引下标以及 length 属性告诉我们对象的元素个数</li>     <li>不具有:诸如 push 、 forEach 以及 indexOf 等数组对象具有的方法Q</li>    </ul>    <p>这儿有三个典型的JavaScript类数组例子。</p>    <ol>     <li>DOM方法:</li>    </ol>    <pre>  <code class="language-javascript">// 获取所有div  let arrayLike = document.querySelectorAll('div')    console.log(Object.prototype.toString.call(arrayLike))  // [object NodeList]    console.log(arrayLike.length) // 127    console.log(arrayLike[0])   // <div id="js-pjax-loader-bar" class="pjax-loader-bar"></div>    console.log(Array.isArray(arrayLike)) // false    arrayLike.push('push')   // Uncaught TypeError: arrayLike.push is not a function(…)  </code></pre>    <p>是的,这个arrayLike的 NodeList ,有length,也能用数组下标访问,但是使用Array.isArray测试时,却告诉我们它不是数组。直接使用push方法时,当然也会报错。<br> 但是,我们可以借用类数组方法:</p>    <pre>  <code class="language-javascript">let arr = Array.prototype.slice.call(arrayLike, 0)    console.log(Array.isArray(arr)) // true    arr.push('push something to arr')  console.log(arr[arr.length - 1]) // push something to arr  </code></pre>    <p>不难看出,此时的arrayLike在调用数组原型方法时,返回值已经转化成数组了。也能正常使用数组的方法。</p>    <ol>     <li>类数组对象</li>    </ol>    <pre>  <code class="language-javascript">let arrayLikeObj = {    length: 2,    0: 'This is Array Like Object',    1: true  }    console.log(arrayLikeObj.length) // 2  console.log(arrayLikeObj[0]) // This is Array Like Object  console.log(Array.isArray(arrayLikeObj)) // false    let arrObj = Array.prototype.slice.call(arrayLikeObj, 0)  console.log(Array.isArray(arrObj)) // true  </code></pre>    <p>这个例子也很好理解。一个对象,加入了length属性,再用Array的原型方法处理一下,摇身一变成为了真的数组。</p>    <ol>     <li>类数组函数</li>    </ol>    <p>这个应该算是最好玩,也是最迷惑人的类数组对象了。</p>    <pre>  <code class="language-javascript">let arrayLikeFunc1 = function () {}  console.log(arrayLikeFunc1.length) // 0  let arrFunc1 = Array.prototype.slice.call(arrayLikeFunc1, 0)  console.log(arrFunc1, arrFunc1.length) // ([], 0)    let arrayLikeFunc2 = function (a, b) {}  console.log(arrayLikeFunc1.length) // 2  let arrFunc2 = Array.prototype.slice.call(arrayLikeFunc2, 0)  console.log(arrFunc2, arrFunc2.length) // ([undefined × 2], 2)  </code></pre>    <p>可以看出, <strong>函数也有length属性,其值等于函数要接收的参数。</strong></p>    <p>注:不适用于ES6的rest参数。具体原因和表现这儿就不再阐述了,不属于本文讨论范围。可参见 <a href="/misc/goto?guid=4959672547124201926" rel="nofollow,noindex">《rest参数 - ECMAScript 6 入门》</a> 。另外arguments在ES6中,被rest参数代替了,所以这儿不作为例子。</p>    <p>而length属性大于0时,如果转为数组,则数组里的值会是undefined。个数等于函数length的长度。</p>    <h2>类数组的实现原理</h2>    <p>类数组的实现原理,主要有以下两点:<br> 第一点是JavaScript的“万物皆对象”概念。<br> 第二点则是JavaScript支持的“鸭子类型”。</p>    <p>首先,从第一点开始解释。</p>    <h3>万物皆对象</h3>    <p>万物皆对象具体解释如下:</p>    <p>在JavaScript中,“一切皆对象”,数组和函数本质上都是对象,就连三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。</p>    <p>而另外一个要点则是,所有对象都继承于Object。所以都能调用对象的方法,比如使用点和方括号访问属性。比如说,这样的:</p>    <pre>  <code class="language-javascript">let func = function() {}  console.log(func instanceof Object) // true  func[0] = 'I\'m a func'  console.log(func[0]) // 'I\'m a func'  </code></pre>    <h3>鸭子类型</h3>    <p>万物皆对象具体解释如下:</p>    <p>如果它走起来像鸭子,而且叫起来像鸭子,那么它就是鸭子。</p>    <p>比如说上面举的类数组例子,虽然他们是对象/函数,但是只要有length属性和对应的数字下标,那么他们就是数组。</p>    <p>但是,在这儿,还是有些迷糊的。为什么使用 call/apply 借用数组方法就能处理这些类数组呢?</p>    <h2>探秘V8</h2>    <p>一开始,我也对这个犯迷糊啊。直到我去Github上,看到了谷歌V8引擎处理数组的源代码。<br> 地址在这儿: <a href="/misc/goto?guid=4959672547219530853" rel="nofollow,noindex">v8/array.js</a><br> 作为讲述,我们在这里引用push的源代码(方便讲述,删除部分。slice的比较长,但是原理一致):</p>    <pre>  <code class="language-javascript">// Appends the arguments to the end of the array and returns the new  // length of the array. See ECMA-262, section 15.4.4.7.  function ArrayPush() {    // 获取要处理的数组    var array = TO_OBJECT(this);    // 获取数组长度    var n = TO_LENGTH(array.length);    // 获取函数参数长度    var m = arguments.length;      for (var i = 0; i < m; i++) {      // 将函数参数push进数组      array[i+n] = arguments[i];    }      // 修正数组长度    var new_length = n + m;    array.length = new_length;    // 返回值是数组的长度    return new_length;  }  </code></pre>    <p>是的, 整个push函数,并没有涉及是否是数组的问题。只关心了length。而因为其对象的特性,所以可以使用方括号来设置属性。</p>    <p>这也是万物皆类型和鸭子类型最生动的体现。</p>    <h2>总结</h2>    <p>JavaScript中的类数组的特殊性,是由其“万物皆类型”和“鸭子类型”决定的,而浏览器引擎底层的实现,更是佐证了这一点。<br> 而先前说我的那位同学,因为只是知道类数组的几种表现和用法,并且想通过apply来打我脸,证明我根本没有仔细看书。这种行为不仅不友善,而且学习效率也不高。<br> 因为, <strong>知其然而不知其所以然是不可取的</strong> 。特别是发现很多这种例子,就得学会归纳总结。(感谢winter老师的演讲: <a href="http://www.open-open.com/lib/view/open1462621548943.html">一个前端的自我修养</a> ,教会我很多东西。)。<br> 很多时候,深入看看源代码也会让你对这个理解的更透彻。将来就算是蹦出一百种类数组,也能知道是怎么回事儿。</p>    <p>最后,还是开头那句话:“都是搞技术的,和善一点不行吗?有问题就好好交流,不要总想着打别人脸啊…………”</p>    <p>来自: http://www.lxxyx.win/2016/05/07/深入理解JavaScript类数组/</p>