JavaScript 中的类数组对象:特性解析与转换之道

90 阅读5分钟

JavaScript 中的类数组对象:特性解析与转换之道

mountain-7011121_1280.webp

在 JavaScript ,我们常常会遇到一类特殊的数据结构 —— 类数组对象。它们看似与数组有着相似的外表,却又有着本质的区别。

一、类数组对象的定义与常见类型

类数组对象是指那些具有类似数组的结构,但并非真正数组的数据集合。

这类对象通常具备以下特征:

  1. 拥有数字索引的属性

  2. 表示元素个数的 length 属性。

  3. 不像真正的数组那样,继承自 Array.prototype,其缺乏数组的诸多数组原生方法,如 pushpopslice 等。

    若要使用数组上的方法可以通过原型链借用或者之间将类数组转为数组

    // 借用数组的 forEach 方法
    Array.prototype.forEach.call(arrayLike, item => {
      // 处理逻辑
    });
    

常见的类数组对象

  1. arguments 对象

它在函数内部发挥着重要作用,包含了传递给函数的所有参数。当我们定义一个函数时,无论传入多少个参数,都可以通过 arguments 对象来访问它们。

function add() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}
console.log(add(1, 2, 3));
  1. NodeList 对象

    当我们使用 document.querySelectorAll() 方法在 DOM 中查找元素时,该方法返回的就是一个 NodeList 对象。

    ES6 规范后浏览器为 NodeList 原生添加了 forEach 方法

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <title>NodeList</title>
      </head>
      <body>
        <div class="container">
          <p>111</p>
          <p>222</p>
          <p>333</p>
        </div>
        <script>
          const pNodes = document.querySelectorAll("p");
          // 判断pNodes 是什么
          console.log(Object.prototype.toString.call(pNodes)) //[object NodeList]
          let str = ''
          pNodes.forEach((item) => {
            str += item.textContent + '|'
          });
          console.log(str); // 111|222|333|
        </script>
      </body>
    </html>
    
    
  2. HTMLCollection 对象

    document.getElementsByClassName() 等方法返回。它同样具有类数组的结构,存储着符合条件的 HTML 元素集合 。

    最新浏览器(chrom没有)为 HTMLCollection 添加了 forEach 方法

        <div class="container">
          <p class="page">111</p>
          <p class="page">222</p>
          <p class="page">333</p>
        </div>
        <script>
          const pNodes = document.getElementsByTagName("page");
          // 判断pNodes 是什么
          console.log(Object.prototype.toString.call(pNodes)); //[object NodeList]
        </script>
    

二、类数组与数组的区别

  • 构造方式

    数组是通过 Array 构造函数创建的,例如 const arr = new Array(); 或者使用数组字面量 const arr = [];

    而类数组对象并非如此,它们往往是在特定的函数调用或 DOM 操作中自然产生的,其内部结构和创建机制与数组有着本质区别。

  • 方法缺失

    由于类数组对象不继承 Array.prototype 上的方法,这使得它们在功能上大打折扣。数组的 map()filter()reduce()等方法,类数组对象都无法直接使用。例如,对于一个 NodeList 对象,我们不能直接调用 map 方法来处理其中的每个 DOM 节点,这在一定程度上限制了我们对类数组对象的操作。

  • 行为差异

    尽管类数组对象拥有 length 属性和数字索引,但在遍历和操作时,它们的行为与数组有所不同。使用 for...in 循环遍历类数组对象的属性时,可能会包含非数值键。

    例如:

    const arrayLike = {
      0: 'a',
      1: 'b',
      2: 'c',
      length: 3,
      foo: 'bar'  // 非数值键
    };
    for(let key in arrayLike){
        console.log(key); // 0  1  2  length foo
    }
    

    而数组在使用 for...of 循环遍历元素时,是按照索引顺序依次获取元素,不会出现这种情况。这种行为上的差异,要求我们在处理类数组对象时,必须格外小心,避免因不当操作导致的错误。

三、将类数组转换为数组的方法

在实际编程中,我们常常需要将类数组对象转换成真正的数组,以便充分利用数组的各种原生方法,提高代码的可读性和效率。以下是几种常见且实用的转换方式。

  1. 使用 Array.from() 方法

    Array.from() 方法是 ES6 引入的新特性,它专门用于将类数组对象或可迭代对象转换为真正的数组。使用起来非常简单,只需将类数组对象作为参数传入即可。例如:

    const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
    const arr = Array.from(arrayLike);
    
    console.log(arr); // \['a', 'b', 'c']
    

    Array.from() 方法还可以接受第二个参数,用于对转换后的数组元素进行映射处理。例如:

    const arrayLike = {0: 1, 1: 2, 2: 3, length: 3};
    const arr = Array.from(arrayLike, num => num * 2);
    
    
    console.log(arr); // [2, 4, 6]
    
  2. 使用扩展运算符

    扩展运算符(...)也是 ES6 的重要特性之一,它同样可以将类数组对象转换为数组。通过在类数组对象前添加扩展运算符,能够快速地将其展开并转换为数组。例如:

    const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
    const arr = [...arrayLike];
    
    console.log(arr); // ['a', 'b', 'c']
    

    这种方式简洁明了,在很多场景下都非常实用,尤其是在需要将类数组对象与其他数组进行合并操作时,扩展运算符的优势更加明显。

  3. 使用 slice() 方法

    在 ES6 之前,我们常用 Array.prototype.slice.call() 方法来将类数组对象转换为数组。slice() 方法原本用于从数组中提取部分元素,返回一个新的数组。当我们通过 call 方法改变 slice() 方法的 this 指向,将类数组对象作为 this 传入时,它就能把类数组对象转换为真正的数组。例如:

    const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
    const arr = Array.prototype.slice.call(arrayLike);
    
    
    console.log(arr); // \['a', 'b', 'c']
    

    虽然这种方式相对 ES6 的新特性略显繁琐,但在一些不支持新特性的环境中,仍然具有重要的应用价值。