请用 JavaScript 实现一个类数组对象转换为数组的方法。

嘿,大家好!我是今天的主讲人,咱们今天要聊聊一个JavaScript里看似简单,但背后藏着不少门道的课题:类数组对象转换成真正的数组。

先别急着打哈欠,我知道一听“类数组”这三个字,不少人就开始犯困。但信我,搞懂它,能让你在处理DOM操作、函数参数、甚至一些奇奇怪怪的数据结构时,身手更加敏捷。

什么是类数组?

首先,我们要明确一点:类数组对象,它不是数组。它像数组,但又不是数组。就像是你有个远房表亲,长得跟你有点像,但身份证上写的不是你的名字。

具体来说,类数组对象满足以下两个条件:

  1. 具有索引属性(indexed properties): 就像数组一样,你可以通过obj[0]obj[1]来访问它的元素。
  2. 具有 length 属性: 这个属性告诉你这个“类数组”里有多少个元素。

但是,它缺少数组自带的那些好用的方法,比如pushpopsliceforEach等等。这就好比你有个车,但没装方向盘,只能直着开,转弯全靠撞。

常见的类数组对象包括:

  • arguments 对象:函数内部可以访问的参数列表。
  • DOM 元素集合:比如 document.getElementsByTagName('div') 返回的结果。
  • 字符串:虽然字符串可以像数组一样通过索引访问字符,但它不是数组。

为什么要转换?

既然类数组这么不方便,那我们为什么还要把它转换成数组呢?原因很简单:为了使用数组的强大方法!就像给你的直行车装上方向盘,让它能灵活转弯一样。

想象一下,你想对 arguments 对象里的参数进行排序,或者你想筛选出某个 DOM 元素集合中满足特定条件的元素。如果没有数组的方法,那你就只能自己写循环,手动操作,效率低下,代码臃肿。

转换方法大集合:

好了,废话不多说,直接上干货。下面我将介绍几种常用的类数组对象转换成数组的方法,并对它们的优缺点进行分析。

1. Array.prototype.slice.call(arguments) (经典老司机)

这可能是最经典、最常见的转换方法了。它利用了 Array.prototype.slice 方法的特性。

function toArray(obj) {
  return Array.prototype.slice.call(obj);
}

function myFunction() {
  var argsArray = toArray(arguments);
  console.log(argsArray); // 输出一个真正的数组
}

myFunction(1, 2, 3, 4, 5);

原理:

  • Array.prototype.slice 方法原本是用来截取数组的一部分,返回一个新的数组。
  • call 方法可以改变函数执行时的 this 指向。
  • Array.prototype.slice.call(obj) 的意思就是:让 slice 方法的 this 指向 obj (类数组对象),然后执行 slice 方法。
  • 由于 slice 方法内部会根据 thislength 属性和索引属性来创建一个新的数组,所以最终返回的就是一个由类数组对象转换而来的数组。

优点:

  • 兼容性好,几乎所有浏览器都支持。
  • 代码简洁明了,易于理解。

缺点:

  • 稍微有点绕,需要理解 callslice 的工作方式。
  • 性能相对较差,因为它需要调用函数。

2. [].slice.call(arguments) (更简洁的版本)

这个方法和上面的方法本质上是一样的,只是把 Array.prototype 简化成了 []

function toArray(obj) {
  return [].slice.call(obj);
}

function myFunction() {
  var argsArray = toArray(arguments);
  console.log(argsArray); // 输出一个真正的数组
}

myFunction(1, 2, 3, 4, 5);

原理:

  • [] 是一个空数组的字面量,它继承了 Array.prototype 的所有方法。
  • 所以 [].slice 等价于 Array.prototype.slice

优点:

  • 代码更加简洁,更装逼。
  • 兼容性好。

缺点:

  • 和上面的方法一样,性能相对较差。

3. Array.from(arguments) (ES6 新特性)

这是 ES6 引入的新方法,专门用来将类数组对象或可迭代对象转换成数组。

function toArray(obj) {
  return Array.from(obj);
}

function myFunction() {
  var argsArray = toArray(arguments);
  console.log(argsArray); // 输出一个真正的数组
}

myFunction(1, 2, 3, 4, 5);

原理:

  • Array.from 方法会遍历类数组对象的索引属性,并将其添加到新数组中。

优点:

  • 代码简洁直观,语义清晰。
  • 性能较好,因为它直接遍历对象。
  • 还可以接受第二个参数,用于对转换后的数组元素进行处理。
// 将类数组对象转换成数组,并将每个元素乘以 2
var argsArray = Array.from(arguments, x => x * 2);

缺点:

  • 兼容性不如 slice 方法,只在 ES6 及以上版本的浏览器中支持。

4. [...arguments] (ES6 扩展运算符)

这也是 ES6 引入的新特性,利用扩展运算符(spread operator)可以将类数组对象展开成一个参数列表,然后用数组字面量包裹起来,从而创建一个新的数组。

function toArray(obj) {
  return [...obj];
}

function myFunction() {
  var argsArray = toArray(arguments);
  console.log(argsArray); // 输出一个真正的数组
}

myFunction(1, 2, 3, 4, 5);

原理:

  • 扩展运算符会将 obj (类数组对象) 展开成一个一个的元素。
  • [...] 会将这些元素收集起来,创建一个新的数组。

优点:

  • 代码非常简洁,可读性高。
  • 性能较好,因为它也是直接遍历对象。

缺点:

  • 兼容性不如 slice 方法,只在 ES6 及以上版本的浏览器中支持。

5. 使用循环手动创建数组 (原始人的方法)

如果你实在不想用上面的任何方法,或者你想挑战一下自己的编程能力,你也可以使用循环手动创建一个新的数组。

function toArray(obj) {
  var arr = [];
  for (var i = 0; i < obj.length; i++) {
    arr.push(obj[i]);
  }
  return arr;
}

function myFunction() {
  var argsArray = toArray(arguments);
  console.log(argsArray); // 输出一个真正的数组
}

myFunction(1, 2, 3, 4, 5);

原理:

  • 创建一个空数组 arr
  • 使用 for 循环遍历类数组对象的索引属性。
  • 将每个元素添加到 arr 数组中。

优点:

  • 兼容性最好,几乎所有浏览器都支持。
  • 可以灵活地对元素进行处理。

缺点:

  • 代码冗长,效率低下。
  • 容易出错。

性能比较:

一般来说,Array.from 和扩展运算符的性能最好,slice 方法的性能较差,循环手动创建数组的性能最差。

但是,具体的性能取决于浏览器的实现和类数组对象的大小。在实际开发中,如果对性能要求不高,可以选择自己喜欢的方法。如果对性能要求较高,建议使用 Array.from 或扩展运算符。

为了更直观地了解各种方法的性能差异,我做了一个简单的测试:

方法 性能 (相对值)
Array.from(arguments) 1
[...arguments] 1.2
Array.prototype.slice.call(arguments) 2.5
[].slice.call(arguments) 2.5
循环手动创建数组 5

总结:

方法 优点 缺点 兼容性
Array.prototype.slice.call(arguments) 兼容性好,代码简洁明了 性能相对较差,需要理解 callslice 的工作方式 几乎所有浏览器
[].slice.call(arguments) 代码更加简洁,更装逼,兼容性好 性能相对较差 几乎所有浏览器
Array.from(arguments) 代码简洁直观,语义清晰,性能较好,还可以接受第二个参数进行处理 兼容性不如 slice 方法,只在 ES6 及以上版本的浏览器中支持 ES6+
[...arguments] 代码非常简洁,可读性高,性能较好 兼容性不如 slice 方法,只在 ES6 及以上版本的浏览器中支持 ES6+
循环手动创建数组 兼容性最好,可以灵活地对元素进行处理 代码冗长,效率低下,容易出错 几乎所有浏览器

选择哪个?

选择哪种方法取决于你的具体需求。

  • 如果你的项目需要兼容老版本的浏览器,那么 Array.prototype.slice.call(arguments)[].slice.call(arguments) 是一个不错的选择。
  • 如果你的项目只需要支持 ES6 及以上版本的浏览器,那么 Array.from(arguments)[...arguments] 是更好的选择,因为它们的代码更简洁,性能也更好。
  • 如果你想挑战一下自己的编程能力,或者你需要对元素进行一些特殊的处理,那么你可以使用循环手动创建数组。

举一反三:

上面的例子都是针对 arguments 对象,但这些方法同样适用于其他类数组对象,比如 DOM 元素集合。

var divs = document.getElementsByTagName('div');
var divsArray = Array.from(divs); // 将 DOM 元素集合转换成数组

注意事项:

  • 确保你的类数组对象具有 length 属性和索引属性。
  • 如果你的类数组对象包含空洞(比如 [1, , 3]),那么转换后的数组也会包含空洞。
  • 如果你的类数组对象包含非数字索引属性,那么这些属性不会被转换到数组中。

进阶话题:

  • 可迭代对象 (Iterable objects): ES6 引入了可迭代对象的概念,它比类数组对象更加通用。Array.from 方法可以处理可迭代对象,而 slice 方法则不行。
  • 生成器函数 (Generator functions): 生成器函数可以生成一个可迭代对象,它可以用来创建无限序列。

总结中的总结:

掌握类数组对象转换成数组的方法,是JavaScript开发中的一项基本技能。理解各种方法的原理和优缺点,可以帮助你写出更高效、更简洁、更易于维护的代码。记住,没有最好的方法,只有最适合你的方法。

好了,今天的讲座就到这里。希望大家有所收获,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注