资讯专栏INFORMATION COLUMN

深入理解JavaScript类数组

Towers / 1626人阅读

摘要:但是,我们可以借用类数组方法不难看出,此时的在调用数组原型方法时,返回值已经转化成数组了。很多时候,深入看看源代码也会让你对这个理解的更透彻。的前端乐园原文链接深入理解类数组

起因

写这篇博客的起因,是我在知乎上回答一个问题时,说自己在学前端时把《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

相关文章

  • JS程序

    摘要:设计模式是以面向对象编程为基础的,的面向对象编程和传统的的面向对象编程有些差别,这让我一开始接触的时候感到十分痛苦,但是这只能靠自己慢慢积累慢慢思考。想继续了解设计模式必须要先搞懂面向对象编程,否则只会让你自己更痛苦。 JavaScript 中的构造函数 学习总结。知识只有分享才有存在的意义。 是时候替换你的 for 循环大法了~ 《小分享》JavaScript中数组的那些迭代方法~ ...

    melody_lql 评论0 收藏0
  • 深入理解JavaScript(三):获取数组中的最大值方法(this,apply)

    摘要:三个方法的作用,都是改变的指向,只是用法稍微有些区别什么是既不指向函数自身,也不指函数的词法作用域。它在函数定义的时候是确定不了的在函数被调用时才发生的绑定,也就是说具体指向什么,取决于你是怎么调用的函数。 1.排序法 思路:给数组先排序(由大到小排序),第一项就是最大值 let arr = [1,5,6,7,9,20,40,2,3]; let max1 = arr.sort(func...

    canopus4u 评论0 收藏0
  • [ JS 基础 ] JS 中 instanceof 运算符深入解析 (2)

    摘要:在高级的技巧中会用来创建作用域安全的构造函数。运算符希望左操作数是一个对象,右操作数表示对象的类。中对象的类似通过初始化它们的构造函数来定义的。为了理解运算符是如何工作的,必须首先理解原型链原型链可作为的继承机制。 在js高级的技巧中会用instanceof来创建作用域安全的构造函数。instanceof运算符希望左操作数是一个对象,右操作数表示对象的类。如果左侧的对象是右侧类的...

    刘厚水 评论0 收藏0
  • MongoDB指南---3、MongoDB基础知识-数据

    摘要:如将构造函数作为函数进行调用即不包括的方式,返回的是日期的字符串表示,而非日期对象。如果不注意这一点,没有始终使用日期构造函数,将得到一堆混乱的日期对象和日期的字符串。关于日期类的完整解释,以及构造函数的参数格式,参见规范节。 上一篇文章:MongoDB指南---2、MongoDB基础知识-文档、集合、数据库、客户端下一篇文章:MongoDB指南---4、MongoDB基础知识-使用M...

    aervon 评论0 收藏0
  • MongoDB指南---3、MongoDB基础知识-数据

    摘要:如将构造函数作为函数进行调用即不包括的方式,返回的是日期的字符串表示,而非日期对象。如果不注意这一点,没有始终使用日期构造函数,将得到一堆混乱的日期对象和日期的字符串。关于日期类的完整解释,以及构造函数的参数格式,参见规范节。 上一篇文章:MongoDB指南---2、MongoDB基础知识-文档、集合、数据库、客户端下一篇文章:MongoDB指南---4、MongoDB基础知识-使用M...

    tunny 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<