MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript类数组对象的代码优化建议

2022-03-184.5k 阅读

JavaScript 类数组对象的概念

在 JavaScript 中,类数组对象(array - like object)并不是真正意义上的数组,但它们在行为上有一些相似之处。类数组对象拥有一个 length 属性,并且可以通过索引来访问其元素,就像数组一样。然而,它们并不具备数组所拥有的完整方法集,例如 mapfilterreduce 等数组原型上的方法。常见的类数组对象有函数内部的 arguments 对象、DOM 方法返回的结果(如 document.querySelectorAll 返回的 NodeList)等。

类数组对象的特点

  1. 拥有 length 属性:这个属性表示类数组对象中元素的数量。例如 arguments 对象,它会根据传入函数的参数个数来确定 length 的值。
function test() {
    console.log(arguments.length);
}
test(1, 2, 3); // 输出 3
  1. 可通过索引访问元素:类数组对象可以像数组一样通过索引(从 0 开始的整数)来访问其内部的元素。以 arguments 为例:
function test() {
    console.log(arguments[0]);
}
test(10); // 输出 10
  1. 非数组类型:尽管类数组对象可以通过索引和 length 属性模拟数组的部分行为,但它们的类型并非 Array。可以通过 Array.isArray 方法来验证:
function test() {
    console.log(Array.isArray(arguments));
}
test(); // 输出 false
  1. 缺少数组原型方法:由于类数组对象不是真正的数组,它们没有继承自 Array.prototype 的一系列便捷方法,如 mapfilter 等。如果直接在类数组对象上调用这些方法,会导致 TypeError
function test() {
    try {
        arguments.map((arg) => arg * 2);
    } catch (error) {
        console.error(error.message);
    }
}
test(1, 2, 3); 
// 输出:arguments.map is not a function

类数组对象在实际开发中的应用场景

函数参数处理

在 JavaScript 函数内部,arguments 是一个类数组对象,它包含了调用函数时传入的所有参数。这在需要处理不定数量参数的函数中非常有用。例如,实现一个简单的求和函数:

function sum() {
    let total = 0;
    for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}
console.log(sum(1, 2, 3)); // 输出 6

DOM 操作

当使用 document.querySelectorAll 等 DOM 操作方法时,返回的 NodeList 也是类数组对象。例如,要给页面上所有带有 class="example" 的元素添加点击事件:

const elements = document.querySelectorAll('.example');
for (let i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', function () {
        console.log('Element clicked');
    });
}

类数组对象的代码优化建议

转换为真正的数组

由于类数组对象缺少数组原型上的方法,将其转换为真正的数组可以方便地使用数组的各种便捷方法。有几种常见的转换方法。

  1. 使用 Array.fromArray.from 方法可以将类数组对象转换为真正的数组。它接受一个类数组对象作为参数,并可以接受一个映射函数作为第二个参数,类似于数组的 map 方法。
function test() {
    const arr = Array.from(arguments, (arg) => arg * 2);
    console.log(arr);
}
test(1, 2, 3); 
// 输出:[2, 4, 6]

对于 NodeList 同样适用:

const elements = document.querySelectorAll('.example');
const elementArray = Array.from(elements);
elementArray.forEach((element) => {
    element.style.color = 'red';
});
  1. 使用扩展运算符 ...:在 ES6 中,可以使用扩展运算符将类数组对象转换为数组。
function test() {
    const arr = [...arguments];
    console.log(arr);
}
test(1, 2, 3); 
// 输出:[1, 2, 3]

对于 NodeList

const elements = document.querySelectorAll('.example');
const elementArray = [...elements];
elementArray.map((element) => {
    return element.textContent;
});
  1. 选择合适的转换方法Array.from 和扩展运算符 ... 都能实现类数组对象到数组的转换,但在某些情况下,两者有细微差别。Array.from 更适用于需要对元素进行映射转换的场景,因为它可以接受第二个映射函数参数。而扩展运算符 ... 在简单的转换场景下更为简洁,并且性能在某些浏览器中可能略好。例如,对于一个只需要简单转换的 arguments 对象,使用扩展运算符可能更合适:
function test() {
    // 使用扩展运算符转换
    const arr1 = [...arguments];
    // 使用 Array.from 转换
    const arr2 = Array.from(arguments);
    // 性能测试(简化版,实际性能测试需更复杂的基准测试工具)
    const start1 = Date.now();
    for (let i = 0; i < 100000; i++) {
        [...arguments];
    }
    const end1 = Date.now();
    const start2 = Date.now();
    for (let i = 0; i < 100000; i++) {
        Array.from(arguments);
    }
    const end2 = Date.now();
    console.log(`扩展运算符耗时: ${end1 - start1}`);
    console.log(`Array.from 耗时: ${end2 - start2}`);
}
test(1, 2, 3); 

在上述性能测试中,一般情况下扩展运算符会比 Array.from 快一些,但如果需要对元素进行映射处理,Array.from 则更为方便。

直接使用类数组对象时的优化

  1. 减少循环中的属性查找:当直接对类数组对象进行循环操作时,尽量减少在循环内部对 length 属性的查找。因为每次访问 length 属性都需要进行一次属性查找,这在性能敏感的场景下可能会有一定影响。可以将 length 属性值缓存到一个变量中。
function sum() {
    let total = 0;
    const len = arguments.length;
    for (let i = 0; i < len; i++) {
        total += arguments[i];
    }
    return total;
}
  1. 使用 for...of 循环(部分类数组对象支持):虽然不是所有类数组对象都支持 for...of 循环,但像 NodeList 等是支持的。for...of 循环语法更简洁,并且在某些情况下性能也不错。
const elements = document.querySelectorAll('.example');
for (const element of elements) {
    element.classList.add('active');
}
  1. 避免不必要的索引访问:如果只是对类数组对象的元素进行遍历处理,而不需要获取元素的索引,尽量使用 for...of 循环或数组转换后使用 forEach 等方法,避免使用传统的 for 循环并通过索引访问元素。因为通过索引访问元素在某些情况下可能会引入额外的性能开销,特别是在类数组对象元素较多时。例如,对于一个 NodeList,如果只是想给每个元素添加一个文本内容:
const elements = document.querySelectorAll('.example');
// 避免这种方式
for (let i = 0; i < elements.length; i++) {
    elements[i].textContent = 'Example';
}
// 使用这种方式
for (const element of elements) {
    element.textContent = 'Example';
}
  1. 利用 Array.prototype 方法的 callapply:虽然类数组对象本身没有数组原型上的方法,但可以通过 callapply 方法借用这些方法。例如,要对 arguments 对象进行 map 操作:
function test() {
    const result = Array.prototype.map.call(arguments, (arg) => arg * 2);
    console.log(result);
}
test(1, 2, 3); 
// 输出:[2, 4, 6]

这里通过 Array.prototype.map.callmap 方法应用到了 arguments 对象上。同样,可以使用 filterreduce 等方法。不过这种方式代码相对复杂,并且在现代 JavaScript 中,将类数组对象转换为真正的数组通常是更优的选择。

内存管理方面的优化

  1. 及时释放不再使用的类数组对象引用:在处理完类数组对象后,如果不再需要,应及时释放对它们的引用,以便 JavaScript 垃圾回收机制能够回收相关内存。例如,在处理完 NodeList 后:
let elements = document.querySelectorAll('.example');
// 处理逻辑
for (const element of elements) {
    element.style.display = 'none';
}
// 处理完后释放引用
elements = null;
  1. 避免过度创建类数组对象:如果在循环或频繁调用的函数中不断创建类数组对象,可能会导致内存占用过高。尽量复用已有的类数组对象或减少不必要的创建。例如,在一个循环中如果每次都调用 document.querySelectorAll 来获取 NodeList,可以将获取操作移到循环外部:
// 不好的做法
for (let i = 0; i < 10; i++) {
    const elements = document.querySelectorAll('.example');
    // 处理 elements
}
// 好的做法
const elements = document.querySelectorAll('.example');
for (let i = 0; i < 10; i++) {
    // 处理 elements
}

代码可读性和可维护性优化

  1. 使用描述性变量名:当处理类数组对象时,为变量选择描述性强的名字。例如,对于 document.querySelectorAll 返回的 NodeList,不要使用诸如 list 这种模糊的变量名,而是使用像 buttonElementsinputElements 这种能清楚表明其用途的变量名。
// 不好的做法
const list = document.querySelectorAll('button');
// 好的做法
const buttonElements = document.querySelectorAll('button');
  1. 封装操作类数组对象的逻辑:如果对类数组对象的操作逻辑比较复杂,可以将其封装成一个函数。这样不仅可以提高代码的复用性,还能使代码结构更加清晰。例如,对于处理 arguments 对象进行复杂计算的逻辑:
function processArguments() {
    let sum = 0;
    let product = 1;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
        product *= arguments[i];
    }
    return { sum, product };
}
function test() {
    const result = processArguments(...arguments);
    console.log(result);
}
test(1, 2, 3); 
// 输出:{sum: 6, product: 6}
  1. 添加注释:对于复杂的类数组对象操作,添加注释可以帮助其他开发人员(包括未来的自己)理解代码的意图。例如,在对 NodeList 进行复杂的过滤和操作时:
// 获取所有带有 data - type="user" 的元素
const userElements = document.querySelectorAll('[data - type="user"]');
// 过滤掉隐藏的元素
const visibleUserElements = [];
for (let i = 0; i < userElements.length; i++) {
    if (window.getComputedStyle(userElements[i]).display!== 'none') {
        visibleUserElements.push(userElements[i]);
    }
}
// 对可见元素进行操作
visibleUserElements.forEach((element) => {
    element.classList.add('highlight');
});

在上述代码中,通过注释清晰地说明了每一步的操作意图。

性能优化综合案例

假设我们有一个需求,要从页面上获取所有 input 元素,过滤掉 type="hidden" 的元素,然后给剩下的元素添加一个输入事件监听器,当输入内容时,将输入的值打印到控制台。

  1. 未优化版本
const inputs = document.querySelectorAll('input');
for (let i = 0; i < inputs.length; i++) {
    if (inputs[i].type!== 'hidden') {
        inputs[i].addEventListener('input', function () {
            console.log(this.value);
        });
    }
}
  1. 优化版本
// 1. 使用 Array.from 转换为数组
const inputArray = Array.from(document.querySelectorAll('input'));
// 2. 使用 filter 方法过滤掉 hidden 类型的 input
const visibleInputs = inputArray.filter((input) => input.type!== 'hidden');
// 3. 使用 forEach 方法添加事件监听器
visibleInputs.forEach((input) => {
    input.addEventListener('input', function () {
        console.log(this.value);
    });
});

在优化版本中,首先使用 Array.fromNodeList 转换为数组,这样可以方便地使用 filterforEach 方法,使代码更加简洁和易读。同时,在性能上,相比于未优化版本,减少了在循环内部进行属性查找和条件判断的次数,提高了执行效率。

通过以上这些针对 JavaScript 类数组对象的代码优化建议,可以使代码在性能、内存管理、可读性和可维护性等方面都得到显著提升,从而提高整个项目的质量。无论是在小型项目还是大型复杂应用中,合理处理类数组对象都是编写高质量 JavaScript 代码的重要一环。在实际开发中,应根据具体的需求和场景,灵活运用这些优化方法,以达到最佳的开发效果。