摘要:但是,我们可以借用类数组方法不难看出,此时的在调用数组原型方法时,返回值已经转化成数组了。很多时候,深入看看源代码也会让你对这个理解的更透彻。的前端乐园原文链接深入理解类数组
起因
写这篇博客的起因,是我在知乎上回答一个问题时,说自己在学前端时把《JavaScript高级程序设计》看了好几遍。
于是在评论区中,出现了如下的对话:
天啦噜,这话说的,宝宝感觉到的,是满满的恶意啊。还好自己的JavaScript基础还算不错,没被打脸。(吐槽一句:知乎少部分人真的是恶意度爆表,整天想着打别人的脸。都是搞技术的,和善一点不行吗…………)
不过这个话题也引起了我的注意,问了问身边很多前端同学关于数组与类数组的区别。他们都表示不太熟悉,所以决定写一篇博客,来分享我对数组与类数组的理解。
什么是类数组类数组的定义,有如下两条:
具有:指向对象元素的数字索引下标以及 length 属性告诉我们对象的元素个数
不具有:诸如 push 、 forEach 以及 indexOf 等数组对象具有的方法Q
这儿有三个典型的JavaScript类数组例子。
1. DOM方法
// 获取所有div let arrayLike = document.querySelectorAll("div") console.log(Object.prototype.toString.call(arrayLike)) // [object NodeList] console.log(arrayLike.length) // 127 console.log(arrayLike[0]) // console.log(Array.isArray(arrayLike)) // false arrayLike.push("push") // Uncaught TypeError: arrayLike.push is not a function(…)
是的,这个arrayLike的 NodeList,有length,也能用数组下标访问,但是使用Array.isArray测试时,却告诉我们它不是数组。直接使用push方法时,当然也会报错。
但是,我们可以借用类数组方法:
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
不难看出,此时的arrayLike在调用数组原型方法时,返回值已经转化成数组了。也能正常使用数组的方法。
2. 类数组对象
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
这个例子也很好理解。一个对象,加入了length属性,再用Array的原型方法处理一下,摇身一变成为了真的数组。
3. 类数组函数
这个应该算是最好玩,也是最迷惑人的类数组对象了。
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(arrayLikeFunc2.length) // 2 let arrFunc2 = Array.prototype.slice.call(arrayLikeFunc2, 0) console.log(arrFunc2, arrFunc2.length) // ([undefined × 2], 2)
可以看出,函数也有length属性,其值等于函数要接收的参数。
注:不适用于ES6的rest参数。具体原因和表现这儿就不再阐述了,不属于本文讨论范围。可参见 《rest参数 - ECMAScript 6 入门》。另外arguments在ES6中,被rest参数代替了,所以这儿不作为例子。
而length属性大于0时,如果转为数组,则数组里的值会是undefined。个数等于函数length的长度。
类数组的实现原理类数组的实现原理,主要有以下两点:
第一点是JavaScript的“万物皆对象”概念。
第二点则是JavaScript支持的“鸭子类型”。
首先,从第一点开始解释。
万物皆对象万物皆对象具体解释如下:
在JavaScript中,“一切皆对象”,数组和函数本质上都是对象,就连三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。
而另外一个要点则是,所有对象都继承于Object。所以都能调用对象的方法,比如使用点和方括号访问属性。
比如说,这样的:
let func = function() {} console.log(func instanceof Object) // true func[0] = "I"m a func" console.log(func[0]) // "I"m a func"鸭子类型
万物皆对象具体解释如下:
如果它走起来像鸭子,而且叫起来像鸭子,那么它就是鸭子。
比如说上面举的类数组例子,虽然他们是对象/函数,但是只要有length属性和对应的数字下标,那么他们就是数组。
但是,在这儿,还是有些迷糊的。为什么使用call/apply借用数组方法就能处理这些类数组呢?
探秘V8一开始,我也对这个犯迷糊啊。直到我去Github上,看到了谷歌V8引擎处理数组的源代码。
地址在这儿:v8/array.js
作为讲述,我们在这里引用push的源代码(方便讲述,删除部分。slice的比较长,但是原理一致):
// 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; }
是的,整个push函数,并没有涉及是否是数组的问题。只关心了length。而因为其对象的特性,所以可以使用方括号来设置属性。
这也是万物皆类型和鸭子类型最生动的体现。
总结JavaScript中的类数组的特殊性,是由其“万物皆类型”和“鸭子类型”决定的,而浏览器引擎底层的实现,更是佐证了这一点。
而先前说我的那位同学,因为只是知道类数组的几种表现和用法,并且想通过apply来打我脸,证明我根本没有仔细看书。这种行为不仅不友善,而且学习效率也不高。
因为,知其然而不知其所以然是不可取的。特别是发现很多这种例子,就得学会归纳总结。(感谢winter老师的演讲:一个前端的自我修养,教会我很多东西。)。
很多时候,深入看看源代码也会让你对这个理解的更透彻。将来就算是蹦出一百种类数组,也能知道是怎么回事儿。
最后,还是开头那句话:“都是搞技术的,和善一点不行吗?有问题就好好交流,不要总想着打别人脸啊…………”
最后附上本人博客地址和原文链接,希望能与各位多多交流。
Lxxyx的前端乐园
原文链接:深入理解JavaScript类数组
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/79397.html
摘要:设计模式是以面向对象编程为基础的,的面向对象编程和传统的的面向对象编程有些差别,这让我一开始接触的时候感到十分痛苦,但是这只能靠自己慢慢积累慢慢思考。想继续了解设计模式必须要先搞懂面向对象编程,否则只会让你自己更痛苦。 JavaScript 中的构造函数 学习总结。知识只有分享才有存在的意义。 是时候替换你的 for 循环大法了~ 《小分享》JavaScript中数组的那些迭代方法~ ...
摘要:三个方法的作用,都是改变的指向,只是用法稍微有些区别什么是既不指向函数自身,也不指函数的词法作用域。它在函数定义的时候是确定不了的在函数被调用时才发生的绑定,也就是说具体指向什么,取决于你是怎么调用的函数。 1.排序法 思路:给数组先排序(由大到小排序),第一项就是最大值 let arr = [1,5,6,7,9,20,40,2,3]; let max1 = arr.sort(func...
摘要:在高级的技巧中会用来创建作用域安全的构造函数。运算符希望左操作数是一个对象,右操作数表示对象的类。中对象的类似通过初始化它们的构造函数来定义的。为了理解运算符是如何工作的,必须首先理解原型链原型链可作为的继承机制。 在js高级的技巧中会用instanceof来创建作用域安全的构造函数。instanceof运算符希望左操作数是一个对象,右操作数表示对象的类。如果左侧的对象是右侧类的...
摘要:如将构造函数作为函数进行调用即不包括的方式,返回的是日期的字符串表示,而非日期对象。如果不注意这一点,没有始终使用日期构造函数,将得到一堆混乱的日期对象和日期的字符串。关于日期类的完整解释,以及构造函数的参数格式,参见规范节。 上一篇文章:MongoDB指南---2、MongoDB基础知识-文档、集合、数据库、客户端下一篇文章:MongoDB指南---4、MongoDB基础知识-使用M...
摘要:如将构造函数作为函数进行调用即不包括的方式,返回的是日期的字符串表示,而非日期对象。如果不注意这一点,没有始终使用日期构造函数,将得到一堆混乱的日期对象和日期的字符串。关于日期类的完整解释,以及构造函数的参数格式,参见规范节。 上一篇文章:MongoDB指南---2、MongoDB基础知识-文档、集合、数据库、客户端下一篇文章:MongoDB指南---4、MongoDB基础知识-使用M...
阅读 2573·2021-10-19 11:41
阅读 2412·2021-09-01 10:32
阅读 3375·2019-08-29 15:21
阅读 1751·2019-08-29 12:20
阅读 1158·2019-08-29 12:13
阅读 596·2019-08-26 12:24
阅读 2517·2019-08-26 10:26
阅读 824·2019-08-23 18:40