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

JavaScript数组元素读写的代码优化技巧

2021-04-067.5k 阅读

JavaScript 数组元素读写基础

在 JavaScript 中,数组是一种非常常用的数据结构。数组允许我们在一个变量中存储多个值,并且可以通过索引来读写这些值。

数组的创建与基本读写

我们可以通过多种方式创建数组。最常见的方式是使用数组字面量:

let numbers = [1, 2, 3, 4, 5];

通过索引来读取数组元素,索引从 0 开始。例如,要读取上述数组中的第一个元素:

let firstNumber = numbers[0];
console.log(firstNumber); // 输出: 1

写入操作也很简单,通过指定索引和新的值即可:

numbers[2] = 10;
console.log(numbers); // 输出: [1, 2, 10, 4, 5]

多维数组读写

JavaScript 支持多维数组,也就是数组中包含数组。例如,创建一个二维数组:

let matrix = [
    [1, 2],
    [3, 4]
];

读取多维数组的元素需要使用多个索引。要读取 matrix 中的 4,代码如下:

let value = matrix[1][1];
console.log(value); // 输出: 4

写入操作同样需要指定多层索引:

matrix[0][0] = 100;
console.log(matrix); // 输出: [[100, 2], [3, 4]]

优化数组元素读取的技巧

缓存数组长度

在循环中读取数组元素时,每次获取数组长度都会有一定的性能开销。尤其是在循环体执行次数较多的情况下,这种开销会累积。我们可以将数组长度缓存起来,避免每次都从数组中获取长度。

let longArray = new Array(100000).fill(0);
// 未缓存长度
let sum1 = 0;
for (let i = 0; i < longArray.length; i++) {
    sum1 += longArray[i];
}
// 缓存长度
let sum2 = 0;
let length = longArray.length;
for (let i = 0; i < length; i++) {
    sum2 += longArray[i];
}

在上述代码中,sum2 的计算方式由于缓存了数组长度,在性能上会优于 sum1 的计算方式。尤其是当 longArray 长度很大且循环体复杂时,这种性能差异会更加明显。

使用 for...of 循环(适用于简单读取场景)

for...of 循环是 ES6 引入的新特性,它可以更简洁地遍历数组并获取元素值。对于只需要读取数组元素值而不需要索引的场景,for...of 循环在性能和代码可读性上都有优势。

let fruits = ['apple', 'banana', 'cherry'];
// 使用 for...of 循环
for (let fruit of fruits) {
    console.log(fruit);
}

上述代码直接输出数组中的每个水果名称。如果使用传统的 for 循环,代码会稍显复杂:

for (let i = 0; i < fruits.length; i++) {
    console.log(fruits[i]);
}

虽然在简单场景下性能差异可能不明显,但在复杂逻辑和大量数据的情况下,for...of 循环在代码维护和性能上都有一定优势。

避免不必要的索引计算

在某些情况下,我们可能会在读取数组元素时进行复杂的索引计算。如果这些计算结果是固定的或者可以提前计算的,应该提前计算好,避免在每次读取时重复计算。

let data = new Array(1000).fill(0);
// 不必要的索引计算
for (let i = 0; i < 100; i++) {
    let index = i * 2 + 5;
    let value = data[index];
    console.log(value);
}
// 提前计算索引
let indices = [];
for (let i = 0; i < 100; i++) {
    indices.push(i * 2 + 5);
}
for (let i = 0; i < 100; i++) {
    let value = data[indices[i]];
    console.log(value);
}

在第一个循环中,每次都要重新计算 index。而在第二个循环中,提前计算好了所有需要的索引,避免了重复计算,在性能上会有提升。

利用 mapfilter 等数组方法(在合适场景下)

JavaScript 数组提供了一系列便捷的方法,如 mapfilter 等。这些方法在处理数组元素读取时,在某些场景下可以提高代码的可读性和性能。

let numbers = [1, 2, 3, 4, 5];
// 使用 map 方法将每个元素翻倍
let doubledNumbers = numbers.map((number) => number * 2);
console.log(doubledNumbers); // 输出: [2, 4, 6, 8, 10]

map 方法遍历数组并对每个元素执行给定的函数,返回一个新的数组。这种方式比手动使用 for 循环更加简洁,并且在现代 JavaScript 引擎中,这些方法经过了优化,性能表现也不错。

优化数组元素写入的技巧

批量写入而非逐元素写入

在需要对数组进行大量写入操作时,逐元素写入会产生较多的性能开销。如果可能,应该尽量批量写入。

let largeArray = new Array(100000).fill(0);
// 逐元素写入
let start1 = Date.now();
for (let i = 0; i < 10000; i++) {
    largeArray[i] = i * 2;
}
let end1 = Date.now();
console.log(`逐元素写入时间: ${end1 - start1} ms`);
// 批量写入
let newData = [];
for (let i = 0; i < 10000; i++) {
    newData.push(i * 2);
}
let start2 = Date.now();
largeArray.splice(0, 10000, ...newData);
let end2 = Date.now();
console.log(`批量写入时间: ${end2 - start2} ms`);

在上述代码中,通过 splice 方法进行批量写入,在性能上优于逐元素写入。这是因为 splice 方法在内部进行了优化,一次性处理多个元素的写入,减少了多次操作数组结构带来的开销。

避免频繁改变数组长度(动态增长)

每次改变数组长度(如通过 pushpopunshiftshift 等方法)时,JavaScript 引擎可能需要重新分配内存空间。频繁地改变数组长度会导致性能下降。

let dynamicArray = [];
// 频繁 push 操作
let start1 = Date.now();
for (let i = 0; i < 10000; i++) {
    dynamicArray.push(i);
}
let end1 = Date.now();
console.log(`频繁 push 时间: ${end1 - start1} ms`);
// 预分配空间
let preAllocatedArray = new Array(10000);
let start2 = Date.now();
for (let i = 0; i < 10000; i++) {
    preAllocatedArray[i] = i;
}
let end2 = Date.now();
console.log(`预分配空间时间: ${end2 - start2} ms`);

可以看到,预分配好空间后再进行写入操作,性能要优于频繁地通过 push 操作动态增长数组。如果在编写代码时能够提前预估数组的大致长度,应该尽量预分配空间。

优化多维数组写入

对于多维数组,写入操作的优化同样重要。由于多维数组涉及多层索引,合理的写入顺序和方式可以提高性能。

let twoDArray = new Array(100).fill(0).map(() => new Array(100).fill(0));
// 按行写入
let start1 = Date.now();
for (let i = 0; i < 100; i++) {
    for (let j = 0; j < 100; j++) {
        twoDArray[i][j] = i + j;
    }
}
let end1 = Date.now();
console.log(`按行写入时间: ${end1 - start1} ms`);
// 按列写入
let start2 = Date.now();
for (let j = 0; j < 100; j++) {
    for (let i = 0; i < 100; i++) {
        twoDArray[i][j] = i + j;
    }
}
let end2 = Date.now();
console.log(`按列写入时间: ${end2 - start2} ms`);

在上述代码中,按行写入通常会比按列写入性能更好。这是因为在内存中,数组是按行存储的,按行写入可以更好地利用缓存机制,减少内存访问的开销。

使用 TypedArray(适用于数值型数组)

TypedArray 是 JavaScript 提供的一种更高效的数值型数组类型,如 Int8ArrayUint16Array 等。它们在内存使用和读写性能上都优于普通数组。

// 创建一个普通数组并写入数据
let normalArray = [];
let start1 = Date.now();
for (let i = 0; i < 100000; i++) {
    normalArray.push(i);
}
let end1 = Date.now();
// 创建一个 TypedArray 并写入数据
let typedArray = new Uint32Array(100000);
let start2 = Date.now();
for (let i = 0; i < 100000; i++) {
    typedArray[i] = i;
}
let end2 = Date.now();
console.log(`普通数组写入时间: ${end1 - start1} ms`);
console.log(`TypedArray 写入时间: ${end2 - start2} ms`);

可以看到,TypedArray 的写入速度明显快于普通数组。这是因为 TypedArray 在内存中是连续存储的,并且数据类型固定,JavaScript 引擎可以进行更有效的优化。

其他与数组读写优化相关的因素

内存管理与垃圾回收

数组在内存中占用一定的空间,当数组不再被使用时,JavaScript 的垃圾回收机制会回收这些内存。但是,如果在数组读写过程中频繁创建和销毁数组,会增加垃圾回收的压力,从而影响性能。

function createAndDestroyArrays() {
    for (let i = 0; i < 10000; i++) {
        let tempArray = new Array(100).fill(0);
        // 对 tempArray 进行一些读写操作
        tempArray[0] = i;
        // 这里 tempArray 不再使用,等待垃圾回收
    }
}

在上述函数中,每次循环都创建一个新的数组 tempArray,这些数组在使用后等待垃圾回收。频繁这样操作会导致垃圾回收频繁进行,降低程序性能。可以通过复用数组等方式来减少垃圾回收的压力。

数组与对象的选择

在某些情况下,我们需要在数组和对象之间做出选择。虽然数组适用于存储有序的数据集合,但对象在某些场景下可能更合适,尤其是当数据需要通过键值对来访问而不是索引时。

// 使用数组存储学生成绩
let studentScoresArray = [85, 90, 78];
// 使用对象存储学生成绩
let studentScoresObject = {
    'Alice': 85,
    'Bob': 90,
    'Charlie': 78
};

如果我们需要根据学生名字来查找成绩,使用对象会更加方便和高效。而如果我们只关心成绩的顺序和索引访问,数组可能是更好的选择。在进行数组读写优化时,要考虑是否使用数组是最合适的数据结构。

函数调用开销与数组读写

在数组读写过程中,如果涉及到函数调用,尤其是在循环中调用函数,会产生一定的性能开销。尽量减少在数组读写循环中不必要的函数调用。

function expensiveFunction() {
    // 复杂的计算逻辑
    let result = 0;
    for (let i = 0; i < 1000; i++) {
        result += i;
    }
    return result;
}
let numbers = [1, 2, 3, 4, 5];
// 循环中调用函数
let start1 = Date.now();
for (let i = 0; i < numbers.length; i++) {
    let value = numbers[i] * expensiveFunction();
    console.log(value);
}
let end1 = Date.now();
// 提前计算函数结果
let result = expensiveFunction();
let start2 = Date.now();
for (let i = 0; i < numbers.length; i++) {
    let value = numbers[i] * result;
    console.log(value);
}
let end2 = Date.now();
console.log(`循环中调用函数时间: ${end1 - start1} ms`);
console.log(`提前计算函数结果时间: ${end2 - start2} ms`);

在上述代码中,提前计算函数结果可以避免在循环中多次调用 expensiveFunction,从而提高性能。

性能测试与分析

使用 console.time()console.timeEnd()

在 JavaScript 中,我们可以使用 console.time()console.timeEnd() 来简单地测量代码块的执行时间,从而比较不同数组读写优化方式的性能。

let numbers = new Array(100000).fill(0);
// 测试缓存数组长度的性能
console.time('cachedLength');
let length = numbers.length;
for (let i = 0; i < length; i++) {
    numbers[i] = i * 2;
}
console.timeEnd('cachedLength');
// 测试未缓存数组长度的性能
console.time('uncachedLength');
for (let i = 0; i < numbers.length; i++) {
    numbers[i] = i * 2;
}
console.timeEnd('uncachedLength');

通过这种方式,我们可以直观地看到缓存数组长度和未缓存数组长度两种方式在执行时间上的差异,从而评估优化效果。

使用 performance.now()

performance.now() 提供了更精确的时间测量,它返回一个高精度的时间戳,单位为毫秒。

let start = performance.now();
// 数组读写操作
let numbers = new Array(100000).fill(0);
for (let i = 0; i < numbers.length; i++) {
    numbers[i] = i * 2;
}
let end = performance.now();
console.log(`操作时间: ${end - start} ms`);

performance.now() 的精度比 console.time()console.timeEnd() 更高,更适合进行精细的性能测试。

使用工具进行性能分析

除了上述简单的时间测量方法,还可以使用浏览器的开发者工具(如 Chrome DevTools)或 Node.js 的 node --prof 等工具进行更全面的性能分析。

在 Chrome DevTools 中,可以使用 Performance 面板录制代码运行的性能数据。通过分析这些数据,可以了解到函数调用次数、执行时间、内存使用等详细信息,从而找出数组读写过程中的性能瓶颈。

在 Node.js 中,使用 node --prof 命令可以生成性能分析报告。例如:

node --prof yourScript.js

然后使用 node --prof-process 工具来处理生成的报告文件,以获取更直观的性能分析结果。

通过这些性能测试和分析工具,我们可以更好地评估数组读写优化技巧的实际效果,并针对性地进行进一步优化。

不同运行环境下的优化差异

浏览器环境

在浏览器环境中,JavaScript 的执行会受到多种因素的影响,如页面渲染、内存限制等。浏览器的 JavaScript 引擎会针对这些特点进行优化。

例如,在处理大量数组读写操作时,要注意不要阻塞主线程,以免影响页面的渲染和交互。可以使用 requestIdleCallbackWeb Workers 来将数组操作放在后台线程执行。

// 使用 requestIdleCallback 进行数组操作
function processArray() {
    let numbers = new Array(100000).fill(0);
    for (let i = 0; i < numbers.length; i++) {
        numbers[i] = i * 2;
    }
}
requestIdleCallback(processArray);

requestIdleCallback 会在浏览器空闲时执行回调函数,避免阻塞主线程。而 Web Workers 则可以创建一个独立的线程来执行 JavaScript 代码,与主线程并行运行,适合处理大量计算的数组读写任务。

Node.js 环境

Node.js 作为服务器端 JavaScript 运行环境,其内存管理和性能优化与浏览器环境有所不同。Node.js 更注重内存的高效利用和长时间运行的稳定性。

在 Node.js 中,对于数组读写优化,可以充分利用其单线程事件驱动的特点。例如,在处理文件 I/O 时,如果需要将文件内容读取到数组中并进行处理,可以使用流的方式,避免一次性将大量数据读入内存。

const fs = require('fs');
const readline = require('readline');
let dataArray = [];
const rl = readline.createInterface({
    input: fs.createReadStream('largeFile.txt'),
    crlfDelay: Infinity
});
rl.on('line', (line) => {
    dataArray.push(line);
    // 对数组进行一些处理
});
rl.on('close', () => {
    console.log('文件读取完毕,数组处理完成');
});

通过流的方式逐行读取文件内容并写入数组,可以有效减少内存占用,提高性能。

不同 JavaScript 引擎的优化策略

不同的 JavaScript 引擎(如 V8、SpiderMonkey、ChakraCore 等)对数组读写优化有不同的策略。例如,V8 引擎在处理数组时,会根据数组的使用模式进行优化。如果数组元素类型固定,V8 会采用更高效的存储和访问方式。

// V8 引擎可能优化的数组使用模式
let fixedTypeArray = new Array(1000).fill(0);
for (let i = 0; i < fixedTypeArray.length; i++) {
    fixedTypeArray[i] = i * 2;
}

了解不同 JavaScript 引擎的优化策略,可以帮助我们在编写代码时更好地利用这些特性,提高数组读写的性能。同时,随着 JavaScript 引擎的不断发展和优化,我们也需要关注最新的技术动态,及时调整优化策略。

优化实践案例

案例一:数据处理应用中的数组优化

假设我们正在开发一个数据处理应用,需要从文件中读取大量的数值数据,进行一些计算后再写入到另一个文件。

const fs = require('fs');
const readline = require('readline');
let inputData = [];
const rl = readline.createInterface({
    input: fs.createReadStream('input.txt'),
    crlfDelay: Infinity
});
rl.on('line', (line) => {
    inputData.push(parseFloat(line));
});
rl.on('close', () => {
    // 优化前:逐元素计算和写入
    let start1 = performance.now();
    let outputData1 = [];
    for (let i = 0; i < inputData.length; i++) {
        let result = inputData[i] * 2 + 5;
        outputData1.push(result);
    }
    let end1 = performance.now();
    // 优化后:批量计算和写入
    let start2 = performance.now();
    let outputData2 = inputData.map((value) => value * 2 + 5);
    let end2 = performance.now();
    console.log(`优化前时间: ${end1 - start1} ms`);
    console.log(`优化后时间: ${end2 - start2} ms`);
    // 将结果写入文件
    fs.writeFileSync('output1.txt', outputData1.join('\n'));
    fs.writeFileSync('output2.txt', outputData2.join('\n'));
});

在这个案例中,通过使用 map 方法进行批量计算和写入,性能得到了显著提升。同时,在读取文件数据时,利用流的方式避免了一次性加载大量数据到内存,优化了整体性能。

案例二:游戏开发中的数组优化

在一个 2D 游戏开发中,我们需要管理大量的游戏对象,这些对象的位置等信息存储在数组中。

// 游戏对象数组
let gameObjects = [];
// 预分配空间
for (let i = 0; i < 1000; i++) {
    gameObjects.push({x: 0, y: 0});
}
// 优化前:频繁改变数组长度
let start1 = performance.now();
for (let i = 0; i < 100; i++) {
    let newObject = {x: Math.random(), y: Math.random()};
    gameObjects.push(newObject);
    gameObjects.shift();
}
let end1 = performance.now();
// 优化后:避免频繁改变数组长度
let start2 = performance.now();
for (let i = 0; i < 100; i++) {
    let index = i % 1000;
    gameObjects[index].x = Math.random();
    gameObjects[index].y = Math.random();
}
let end2 = performance.now();
console.log(`优化前时间: ${end1 - start1} ms`);
console.log(`优化后时间: ${end2 - start2} ms`);

在这个案例中,优化前频繁使用 pushshift 方法改变数组长度,导致性能下降。优化后通过复用数组元素,避免了频繁改变数组长度,提高了性能。这在游戏开发中对于保持游戏的流畅性非常重要。

案例三:Web 应用前端数据展示的数组优化

在一个 Web 应用的前端,我们需要从服务器获取数据并展示在表格中。数据以数组形式返回,并且可能需要进行一些处理后再展示。

// 模拟从服务器获取的数据
let serverData = new Array(1000).fill(0).map((_, i) => ({id: i, value: Math.random()}));
// 优化前:在渲染函数中处理数组
function renderTable1(data) {
    let tableHTML = '<table><thead><tr><th>ID</th><th>Value</th></tr></thead><tbody>';
    for (let i = 0; i < data.length; i++) {
        let row = data[i];
        let processedValue = row.value.toFixed(2);
        tableHTML += `<tr><td>${row.id}</td><td>${processedValue}</td></tr>`;
    }
    tableHTML += '</tbody></table>';
    return tableHTML;
}
let start1 = performance.now();
let html1 = renderTable1(serverData);
let end1 = performance.now();
// 优化后:提前处理数组
function processData(data) {
    return data.map((row) => ({id: row.id, value: row.value.toFixed(2)}));
}
function renderTable2(data) {
    let tableHTML = '<table><thead><tr><th>ID</th><th>Value</th></tr></thead><tbody>';
    for (let i = 0; i < data.length; i++) {
        let row = data[i];
        tableHTML += `<tr><td>${row.id}</td><td>${row.value}</td></tr>`;
    }
    tableHTML += '</tbody></table>';
    return tableHTML;
}
let start2 = performance.now();
let processedData = processData(serverData);
let html2 = renderTable2(processedData);
let end2 = performance.now();
console.log(`优化前渲染时间: ${end1 - start1} ms`);
console.log(`优化后渲染时间: ${end2 - start2} ms`);

在这个案例中,优化前在渲染函数中对每个数据进行处理,导致渲染时间较长。优化后提前对数组进行处理,减少了渲染函数中的计算量,提高了渲染性能,从而提升了用户体验。

通过这些实际案例,我们可以看到在不同的应用场景中,通过合理运用数组读写优化技巧,可以显著提高程序的性能和用户体验。在实际开发中,要根据具体的业务需求和场景,选择合适的优化方法,并不断进行性能测试和调整,以达到最佳的性能效果。