1. JavaScript数组:从基础操作到高阶应用
数组是JavaScript中最基础也最强大的数据结构之一。作为前端开发者,我几乎每天都要和各种数组操作打交道。让我们从最基础的数组定义开始,逐步深入探讨它的各种特性和应用场景。
1.1 数组的定义与基本操作
数组字面量使用方括号[]定义,这是最直观的创建方式:
javascript复制// 基础数组示例
const numbers = [1, 2, 3, 4, 5];
const mixed = [1, 'text', true, null, {name: 'John'}];
注意:虽然JavaScript允许数组包含不同类型的数据,但在实际开发中,我们通常会保持数组元素类型一致,这有利于代码维护和类型检查。
数组索引从0开始,这是大多数编程语言的惯例。访问不存在的索引会返回undefined:
javascript复制console.log(numbers[0]); // 1
console.log(numbers[10]); // undefined
数组长度是动态的,可以通过length属性获取,也可以直接修改:
javascript复制numbers.length = 3; // 截断数组,现在numbers是[1, 2, 3]
numbers.length = 5; // 扩展数组,新增元素为undefined
1.2 多维数组与复杂结构
JavaScript支持多维数组,实际上就是数组的嵌套:
javascript复制const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
const complexStructure = [
{
id: 1,
items: ['a', 'b']
},
{
id: 2,
items: ['c', 'd']
}
];
访问多维数组元素需要链式索引:
javascript复制console.log(matrix[1][2]); // 6
console.log(complexStructure[0].items[1]); // 'b'
1.3 数组方法详解
JavaScript数组提供了丰富的方法,可以分为几大类:
修改原数组的方法:
javascript复制const arr = [1, 2, 3];
arr.push(4); // 末尾添加,返回新长度
arr.pop(); // 末尾移除,返回被移除元素
arr.unshift(0); // 开头添加,返回新长度
arr.shift(); // 开头移除,返回被移除元素
arr.splice(1, 1, 'a'); // 从索引1开始,删除1个元素,插入'a'
arr.reverse(); // 反转数组
arr.sort(); // 排序
不修改原数组的方法:
javascript复制const newArr = arr.slice(1, 3); // 截取子数组
const str = arr.join('-'); // 合并为字符串
const found = arr.includes(2); // 检查包含
const index = arr.indexOf(2); // 查找索引
ES6新增的强大方法:
javascript复制// 箭头函数简化回调写法
arr.forEach(item => console.log(item));
const doubled = arr.map(item => item * 2);
const evens = arr.filter(item => item % 2 === 0);
const sum = arr.reduce((acc, curr) => acc + curr, 0);
const hasEven = arr.some(item => item % 2 === 0);
const allEven = arr.every(item => item % 2 === 0);
实操心得:现代前端开发中,尽量使用
map、filter、reduce等函数式方法替代传统的for循环,代码更简洁且易于维护。
1.4 数组与字符串的转换
字符串和数组之间的转换是常见操作:
javascript复制// 字符串转数组
const dateStr = '2023-07-15';
const dateArr = dateStr.split('-'); // ['2023', '07', '15']
// 数组转字符串
const newStr = dateArr.join('/'); // '2023/07/15'
更复杂的字符串处理可以结合正则表达式:
javascript复制const sentence = 'Hello world, how are you?';
const words = sentence.split(/\W+/); // ['Hello', 'world', 'how', 'are', 'you', '']
2. 变量、赋值与内存管理
理解JavaScript的变量赋值和内存管理机制对于写出高效、无bug的代码至关重要。
2.1 基本类型与引用类型
JavaScript中的数据类型分为两大类:
基本类型(原始类型):
- Number
- String
- Boolean
- Null
- Undefined
- Symbol (ES6)
- BigInt (ES11)
引用类型:
- Object
- Array
- Function
- Date
- RegExp
- 其他内置对象
2.2 值传递 vs 引用传递
基本类型使用值传递,赋值时会创建值的副本:
javascript复制let a = 10;
let b = a; // b得到a的值副本
a = 20;
console.log(b); // 仍然是10
引用类型使用引用传递,赋值时传递的是内存地址:
javascript复制let arr1 = [1, 2, 3];
let arr2 = arr1; // arr2和arr1指向同一个数组
arr1[0] = 99;
console.log(arr2[0]); // 99,因为指向同一个内存
2.3 深拷贝与浅拷贝
对于引用类型,有时我们需要真正的拷贝而不是引用:
浅拷贝(只拷贝一层):
javascript复制const original = [1, 2, [3, 4]];
const shallowCopy = [...original]; // 或 original.slice()
original[0] = 99; // 不影响拷贝
original[2][0] = 99; // 会影响拷贝,因为第二层是引用
深拷贝(完全独立副本):
javascript复制// 简单方法(有局限性)
const deepCopy = JSON.parse(JSON.stringify(original));
// 复杂对象的深拷贝可能需要递归实现或使用库如lodash的_.cloneDeep
注意事项:JSON方法不能拷贝函数、undefined、循环引用等特殊值,在实际项目中要根据需求选择合适的拷贝方式。
2.4 内存管理
JavaScript使用自动内存管理(垃圾回收),但了解内存机制有助于避免内存泄漏:
- 栈内存:存储基本类型值和引用类型的地址(访问快,大小固定)
- 堆内存:存储引用类型的实际数据(大小动态,访问稍慢)
常见内存泄漏场景:
- 意外的全局变量
- 遗忘的定时器或回调
- DOM引用未清理
- 闭包滥用
javascript复制// 内存泄漏示例
function leak() {
leakedVar = '这是一个全局变量'; // 意外创建全局变量
this.leakedProperty = '实例属性未释放';
}
// 正确的做法
function noLeak() {
const localVar = '局部变量会被回收';
}
3. 循环控制:从基础到高级模式
循环是编程中的基本控制结构,JavaScript提供了多种循环方式,各有适用场景。
3.1 for循环及其变体
基础for循环:
javascript复制for (let i = 0; i < 5; i++) {
console.log(i); // 0, 1, 2, 3, 4
}
for...of循环(ES6):
javascript复制const arr = ['a', 'b', 'c'];
for (const item of arr) {
console.log(item); // 'a', 'b', 'c'
}
// 获取索引
for (const [index, item] of arr.entries()) {
console.log(index, item); // 0 'a', 1 'b', 2 'c'
}
for...in循环(遍历对象属性):
javascript复制const obj = {a: 1, b: 2};
for (const key in obj) {
console.log(key, obj[key]); // 'a' 1, 'b' 2
}
// 数组也可以用for...in但不推荐,因为它会遍历所有可枚举属性
3.2 while与do...while循环
while循环:
javascript复制let i = 0;
while (i < 5) {
console.log(i); // 0, 1, 2, 3, 4
i++;
}
do...while循环:
javascript复制let j = 0;
do {
console.log(j); // 0, 1, 2, 3, 4
j++;
} while (j < 5);
关键区别:do...while至少执行一次,while可能一次都不执行。
3.3 循环控制语句
break与continue:
javascript复制// break跳出整个循环
for (let i = 0; i < 10; i++) {
if (i === 5) break;
console.log(i); // 0, 1, 2, 3, 4
}
// continue跳过当前迭代
for (let i = 0; i < 5; i++) {
if (i === 2) continue;
console.log(i); // 0, 1, 3, 4
}
标签语句(较少使用但有时很有用):
javascript复制outerLoop: for (let i = 0; i < 3; i++) {
innerLoop: for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) break outerLoop;
console.log(i, j);
}
}
// 输出:0 0, 0 1, 0 2, 1 0
3.4 循环性能优化
循环性能在大数据量时尤为重要:
javascript复制// 缓存数组长度
const bigArray = new Array(1000000).fill(0);
// 较差性能
for (let i = 0; i < bigArray.length; i++) {
// 每次循环都访问length属性
}
// 较好性能
for (let i = 0, len = bigArray.length; i < len; i++) {
// 只访问一次length
}
// 最佳性能(倒序循环)
for (let i = bigArray.length - 1; i >= 0; i--) {
// 比较操作更少
}
实测数据:在100万次循环中,优化后的循环可以节省10%-30%的时间。不过在现代JavaScript引擎中,这些差异已经变小,但良好的习惯仍然值得保持。
4. 常见问题与高级技巧
4.1 数组操作中的陷阱
稀疏数组问题:
javascript复制const sparse = [1, , 3]; // 注意中间的"空位"
console.log(sparse.length); // 3
console.log(sparse[1]); // undefined
// 但遍历行为不同
sparse.forEach(item => console.log(item)); // 1, 3 (跳过空位)
数组类型检测:
javascript复制typeof []; // 'object' (不够准确)
Array.isArray([]); // true (推荐方式)
修改数组时的注意事项:
javascript复制const arr = [1, 2, 3];
arr[10] = 10; // 合法,但会创建空位
console.log(arr); // [1, 2, 3, 空属性 × 7, 10]
console.log(arr.length); // 11
4.2 循环中的闭包问题
一个经典问题:
javascript复制for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 输出5个5
}, 100);
}
解决方案:
- 使用let代替var(块级作用域)
javascript复制for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 0,1,2,3,4
}, 100);
}
- IIFE(立即执行函数)
javascript复制for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => {
console.log(j); // 0,1,2,3,4
}, 100);
})(i);
}
4.3 现代JavaScript循环替代方案
数组方法替代循环:
javascript复制// 查找元素
const found = array.find(item => item.id === 123);
// 查找索引
const index = array.findIndex(item => item.id === 123);
// 检查条件
const allValid = array.every(item => item.valid);
const anyValid = array.some(item => item.valid);
Generator函数:
javascript复制function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
for (const value of generateSequence()) {
console.log(value); // 1, 2, 3
}
4.4 性能优化实战
减少循环内部计算:
javascript复制// 不佳
for (let i = 0; i < array.length; i++) {
const result = expensiveCalculation(array[i]);
// ...
}
// 优化
const len = array.length;
for (let i = 0; i < len; i++) {
const item = array[i];
const result = expensiveCalculation(item);
// ...
}
循环展开(在非常注重性能的场景):
javascript复制// 常规循环
for (let i = 0; i < 8; i++) {
process(i);
}
// 展开循环(减少循环控制开销)
process(0); process(1); process(2); process(3);
process(4); process(5); process(6); process(7);
Web Worker并行处理:
javascript复制// 主线程
const worker = new Worker('worker.js');
worker.postMessage(bigArray);
// worker.js
self.onmessage = function(e) {
const array = e.data;
// 处理数组
const result = processArray(array);
self.postMessage(result);
};
在实际项目中,选择正确的循环方式和数组操作方法可以显著提高代码的可读性和性能。经过多年的JavaScript开发,我发现掌握这些基础概念的核心原理比记忆API更重要,因为理解了"为什么"之后,"怎么做"就会变得自然而然。