2.3 - 箭头函数、解构赋值、扩展运算符
JavaScript ES6 核心特性深度解析
箭头函数、解构赋值、扩展运算符 —— 从原理到实战
目录
一、箭头函数 (Arrow Function)
1.1 基本语法与使用
语法演进
// 传统函数表达式
const add1 = function(a, b) {
return a + b;
};
// 箭头函数 - 完整形式
const add2 = (a, b) => {
return a + b;
};
// 箭头函数 - 简写(单一表达式,自动return)
const add3 = (a, b) => a + b;
// 单参数可省略括号
const square = x => x * x;
// 无参数必须保留空括号
const getRandom = () => Math.random();
// 返回对象字面量需要用括号包裹
const makeUser = (name, age) => ({ name, age });
语法糖本质
箭头函数的简洁语法背后是 JavaScript 引擎的语法糖转换:
// 源代码
const multiply = (a, b) => a * b;
// 引擎实际执行逻辑(简化理解)
const multiply = function(a, b) {
// 关键:词法作用域绑定 this
const _this = this; // 捕获外层 this
return function(a, b) {
return a * b;
}.call(_this, a, b);
};
1.2 this 绑定机制深度剖析
核心原理:词法作用域 (Lexical Scoping)
箭头函数没有自己的 this,它会捕获定义时所在上下文的 this 值。
// 示例 1: 对比传统函数与箭头函数
const obj = {
name: 'MyObject',
// 传统方法
regularMethod: function() {
console.log('Regular:', this.name); // this 指向 obj
setTimeout(function() {
console.log('Callback Regular:', this.name); // this 指向 window/global
}, 100);
},
// 箭头函数方法
arrowMethod: function() {
console.log('Arrow Parent:', this.name); // this 指向 obj
setTimeout(() => {
console.log('Callback Arrow:', this.name); // this 仍指向 obj (词法捕获)
}, 100);
}
};
obj.regularMethod();
// 输出: Regular: MyObject
// Callback Regular: undefined
obj.arrowMethod();
// 输出: Arrow Parent: MyObject
// Callback Arrow: MyObject
原理图解
┌─────────────────────────────────────────┐
│ 全局作用域 (Global Scope) │
│ this = window/global │
│ │
│ ┌───────────────────────────────────┐ │
│ │ obj 对象 │ │
│ │ │ │
│ │ regularMethod() { │ │
│ │ this = obj ✓ │ │
│ │ │ │
│ │ setTimeout(function() { │ │
│ │ this = window ✗ │ │
│ │ }) │ │
│ │ } │ │
│ │ │ │
│ │ arrowMethod() { │ │
│ │ this = obj ✓ │ │
│ │ │ │
│ │ setTimeout(() => { │ │
│ │ this = obj ✓ (词法继承) │ │
│ │ }) │ │
│ │ } │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
真实工作场景
// React 类组件中的事件处理
class TodoList extends React.Component {
state = { todos: [] };
// 方案1: 箭头函数自动绑定 this
addTodo = (text) => {
this.setState({
todos: [...this.state.todos, text]
});
};
// 方案2: 传统方法需要手动绑定
addTodoTraditional(text) {
this.setState({
todos: [...this.state.todos, text]
});
}
componentDidMount() {
// 箭头函数:无需 bind
fetchTodos().then(this.addTodo);
// 传统函数:需要 bind
fetchTodos().then(this.addTodoTraditional.bind(this));
}
}
1.3 箭头函数与普通函数的本质区别
对比表
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
this 绑定 | 动态,调用时决定 | 词法,定义时捕获 |
arguments 对象 | ✅ 有 | ❌ 无 |
new 构造调用 | ✅ 可以 | ❌ 不可以 |
prototype 属性 | ✅ 有 | ❌ 无 |
super 关键字 | ✅ 可用 | ❌ 不可用 |
| 方法简写 | 支持 | 支持 |
| Generator | ✅ 可以 | ❌ 不可以 |
深度对比
// 1. arguments 对象
function regularFunc() {
console.log(arguments); // [1, 2, 3]
}
regularFunc(1, 2, 3);
const arrowFunc = () => {
console.log(arguments); // ReferenceError: arguments is not defined
};
// 解决方案:使用剩余参数
const arrowWithRest = (...args) => {
console.log(args); // [1, 2, 3]
};
arrowWithRest(1, 2, 3);
// 2. 构造函数
function Person(name) {
this.name = name;
}
const p1 = new Person('Alice'); // ✅ 正常
const PersonArrow = (name) => {
this.name = name;
};
const p2 = new PersonArrow('Bob'); // ❌ TypeError: PersonArrow is not a constructor
// 3. prototype
console.log(Person.prototype); // { constructor: ƒ }
console.log(PersonArrow.prototype); // undefined
// 4. call/apply/bind 无效
const obj1 = { value: 1 };
const obj2 = { value: 2 };
const regular = function() { return this.value; };
console.log(regular.call(obj1)); // 1
console.log(regular.call(obj2)); // 2
const arrow = () => this.value;
console.log(arrow.call(obj1)); // undefined (this 已固定)
console.log(arrow.call(obj2)); // undefined
1.4 使用场景与最佳实践
✅ 适合使用箭头函数的场景
// 1. 数组方法回调
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const sum = numbers.reduce((acc, n) => acc + n, 0);
const evens = numbers.filter(n => n % 2 === 0);
// 2. Promise 链
fetch('/api/user')
.then(res => res.json())
.then(data => data.user)
.then(user => console.log(user.name))
.catch(err => console.error(err));
// 3. 定时器/事件监听
class Counter {
count = 0;
start() {
setInterval(() => {
this.count++; // this 指向 Counter 实例
console.log(this.count);
}, 1000);
}
}
// 4. 函数式编程
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const result = pipe(add10, multiply2)(5); // (5+10)*2 = 30
❌ 不适合使用箭头函数的场景
// 1. 对象方法(需要动态 this)
const calculator = {
value: 0,
// ❌ 错误:this 指向外层作用域
addArrow: (n) => {
this.value += n; // this !== calculator
},
// ✅ 正确
add(n) {
this.value += n;
}
};
// 2. 需要 arguments 的函数
// ❌ 错误
const sum = () => {
return Array.from(arguments).reduce((a, b) => a + b);
};
// ✅ 正确
function sum() {
return Array.from(arguments).reduce((a, b) => a + b);
}
// 或使用剩余参数
const sum2 = (...nums) => nums.reduce((a, b) => a + b);
// 3. 原型方法
// ❌ 错误
Person.prototype.sayHello = () => {
console.log(`Hello, ${this.name}`); // this 不是实例
};
// ✅ 正确
Person.prototype.sayHello = function() {
console.log(`Hello, ${this.name}`);
};
// 4. 动态上下文(事件处理器)
// ❌ 错误
button.addEventListener('click', () => {
this.classList.toggle('active'); // this 不是 button
});
// ✅ 正确
button.addEventListener('click', function() {
this.classList.toggle('active');
});
1.5 常见陷阱与面试题
陷阱 1: 对象方法中的箭头函数
const obj = {
name: 'Object',
// 陷阱:看似是方法,实际是属性
getName: () => {
console.log(this.name); // this 指向定义时的外层作用域
}
};
obj.getName(); // undefined (严格模式) 或 window.name (非严格模式)
// 正确理解:等价于
const obj = {
name: 'Object'
};
obj.getName = () => {
console.log(this.name); // this 在对象外部定义
};
陷阱 2: 构造函数中的箭头函数
function Person(name) {
this.name = name;
// ✅ 实例方法(每个实例独立)
this.sayHello = () => {
console.log(`Hello, ${this.name}`);
};
}
const p1 = new Person('Alice');
const p2 = new Person('Bob');
p1.sayHello(); // Hello, Alice
p2.sayHello(); // Hello, Bob
// 注意:每个实例都创建新的函数
console.log(p1.sayHello === p2.sayHello); // false
// 对比:原型方法共享
Person.prototype.greet = function() {
console.log(`Hi, ${this.name}`);
};
console.log(p1.greet === p2.greet); // true
面试题 1: 经典 this 指向
const obj = {
a: 1,
foo: function() {
console.log(this.a);
},
bar: () => {
console.log(this.a);
}
};
obj.foo(); // 输出: 1 (this = obj)
obj.bar(); // 输出: undefined (this = 定义时的外层,通常是 window)
const foo = obj.foo;
const bar = obj.bar;
foo(); // 输出: undefined (this = window)
bar(); // 输出: undefined (this 已固定为定义时的外层)
面试题 2: 箭头函数嵌套
const obj = {
name: 'Outer',
getNameGetter: function() {
// 外层是普通函数,this = obj
return () => {
// 箭头函数捕获外层 this
console.log(this.name);
};
}
};
const getter = obj.getNameGetter();
getter(); // 输出: Outer
// 改变调用方式
const getNameGetter = obj.getNameGetter;
const getter2 = getNameGetter(); // 此时外层 this = window
getter2(); // 输出: undefined
面试题 3: React 中的实际应用
class TodoItem extends React.Component {
// 方案1: 构造函数 bind
constructor(props) {
super(props);
this.handleClick1 = this.handleClick1.bind(this);
}
handleClick1() {
console.log(this.props.id);
}
// 方案2: 箭头函数类属性(推荐)
handleClick2 = () => {
console.log(this.props.id);
};
// 方案3: 内联箭头函数(不推荐,每次 render 创建新函数)
render() {
return (
<div>
<button onClick={this.handleClick1}>方案1</button>
<button onClick={this.handleClick2}>方案2</button>
<button onClick={() => this.handleClick1()}>方案3</button>
</div>
);
}
}
// 性能对比:
// 方案1和2: 函数引用不变,子组件可用 React.memo 优化
// 方案3: 每次 render 创建新函数,导致子组件无意义 re-render
二、解构赋值 (Destructuring)
2.1 数组解构原理
底层机制: Iterator 接口
数组解构本质是调用目标的 Iterator 接口 (Symbol.iterator)。
// 解构语法
const [a, b, c] = [1, 2, 3];
// 等价于(简化理解)
const _arr = [1, 2, 3];
const _iterator = _arr[Symbol.iterator]();
const a = _iterator.next().value; // 1
const b = _iterator.next().value; // 2
const c = _iterator.next().value; // 3
基本语法
// 1. 基础解构
const [a, b] = [1, 2];
console.log(a, b); // 1 2
// 2. 跳过元素
const [first, , third] = [1, 2, 3];
console.log(first, third); // 1 3
// 3. 剩余元素
const [head, ...tail] = [1, 2, 3, 4];
console.log(head); // 1
console.log(tail); // [2, 3, 4]
// 4. 嵌套解构
const [a, [b, c]] = [1, [2, 3]];
console.log(a, b, c); // 1 2 3
// 5. 默认值
const [x = 1, y = 2] = [10];
console.log(x, y); // 10 2
Iterator 协议验证
// 任何实现了 Iterator 接口的对象都可以解构
const str = 'hello';
const [h, e, l1, l2, o] = str;
console.log(h, e, l1, l2, o); // h e l l o
// Set 也可以解构
const set = new Set([1, 2, 3]);
const [s1, s2, s3] = set;
console.log(s1, s2, s3); // 1 2 3
// Generator 函数也可以
function* gen() {
yield 1;
yield 2;
yield 3;
}
const [g1, g2, g3] = gen();
console.log(g1, g2, g3); // 1 2 3
// 普通对象不可以(没有 Iterator)
const [err] = { a: 1 }; // TypeError: object is not iterable
2.2 对象解构原理
底层机制: 属性访问
对象解构本质是 属性查找和赋值。
// 解构语法
const { name, age } = { name: 'Alice', age: 25 };
// 等价于
const _obj = { name: 'Alice', age: 25 };
const name = _obj.name;
const age = _obj.age;
基本语法
// 1. 基础解构
const { name, age } = { name: 'Alice', age: 25 };
console.log(name, age); // Alice 25
// 2. 重命名
const { name: userName, age: userAge } = { name: 'Bob', age: 30 };
console.log(userName, userAge); // Bob 30
// 3. 默认值
const { x = 10, y = 20 } = { x: 5 };
console.log(x, y); // 5 20
// 4. 嵌套解构
const { user: { name, address: { city } } } = {
user: {
name: 'Charlie',
address: { city: 'Beijing' }
}
};
console.log(name, city); // Charlie Beijing
// 5. 剩余属性
const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 };
console.log(a, b, rest); // 1 2 { c: 3, d: 4 }
原型链查找
const parent = { inherited: 'yes' };
const child = Object.create(parent);
child.own = 'mine';
const { own, inherited } = child;
console.log(own, inherited); // mine yes (会查找原型链)
2.3 默认值机制
触发条件: undefined
// 只有严格等于 undefined 才使用默认值
const [a = 1] = [undefined];
console.log(a); // 1
const [b = 1] = [null];
console.log(b); // null (不触发默认值)
const [c = 1] = [0];
console.log(c); // 0
const [d = 1] = [false];
console.log(d); // false
const [e = 1] = [''];
console.log(e); // ''
默认值表达式惰性求值
let count = 0;
function getDefault() {
count++;
return 'default';
}
// 案例1: 有值,不调用函数
const [a = getDefault()] = ['value'];
console.log(a); // 'value'
console.log(count); // 0 (函数未调用)
// 案例2: 无值,调用函数
const [b = getDefault()] = [];
console.log(b); // 'default'
console.log(count); // 1 (函数被调用)
对象解构默认值陷阱
// 陷阱:重命名 + 默认值语法
const { name: userName = 'Anonymous' } = {};
console.log(userName); // Anonymous
console.log(name); // ReferenceError: name is not defined
// 理解:
// 1. name 是属性名
// 2. userName 是变量名
// 3. 'Anonymous' 是默认值
2.4 高级应用场景
场景 1: 函数参数解构
// 传统写法
function createUser(options) {
const name = options.name || 'Anonymous';
const age = options.age || 18;
const role = options.role || 'user';
// ...
}
// 解构写法(简洁 + 自文档)
function createUser({
name = 'Anonymous',
age = 18,
role = 'user'
} = {}) {
console.log(name, age, role);
}
createUser({ name: 'Alice', age: 25 }); // Alice 25 user
createUser(); // Anonymous 18 user (注意最后的 = {})
场景 2: 返回多个值
// 传统:返回对象
function getStats(arr) {
return {
min: Math.min(...arr),
max: Math.max(...arr),
avg: arr.reduce((a, b) => a + b) / arr.length
};
}
const stats = getStats([1, 2, 3, 4, 5]);
console.log(stats.min, stats.max, stats.avg);
// 解构:更简洁
function getStats(arr) {
return {
min: Math.min(...arr),
max: Math.max(...arr),
avg: arr.reduce((a, b) => a + b) / arr.length
};
}
const { min, max, avg } = getStats([1, 2, 3, 4, 5]);
console.log(min, max, avg);
场景 3: 交换变量
// 传统写法
let a = 1, b = 2;
let temp = a;
a = b;
b = temp;
// 解构写法
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
// 原理(简化)
let x = 1, y = 2;
let _temp = [y, x]; // [2, 1]
x = _temp[0];
y = _temp[1];
场景 4: 提取 JSON 数据
const response = {
code: 200,
data: {
user: {
id: 1,
name: 'Alice',
email: 'alice@example.com'
},
token: 'xxx'
},
message: 'success'
};
// 深度解构
const {
code,
data: {
user: { id, name, email },
token
},
message
} = response;
console.log(id, name, email, token); // 1 Alice alice@example.com xxx
场景 5: React Hooks
// useState 解构
const [count, setCount] = useState(0);
// useReducer 解构
const [state, dispatch] = useReducer(reducer, initialState);
// 自定义 Hook
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(setUser).finally(() => setLoading(false));
}, [userId]);
return { user, loading };
}
// 使用时解构
const { user, loading } = useUser(1);
2.5 性能与边界情况
性能考量
// 解构不影响性能(编译时转换)
const { a, b, c } = obj; // 与下面等价
const a = obj.a;
const b = obj.b;
const c = obj.c;
// 但嵌套解构可能导致多次属性访问
const {
user: {
profile: {
address: {
city
}
}
}
} = data;
// 等价于
const city = data.user.profile.address.city; // 4 次属性访问
边界情况
// 1. null/undefined 解构报错
const { a } = null; // TypeError: Cannot destructure property 'a' of 'null'
const [b] = undefined; // TypeError: undefined is not iterable
// 安全解构
const { a } = obj || {};
const [b] = arr || [];
// 2. 解构不存在的属性
const { nonexistent } = { foo: 'bar' };
console.log(nonexistent); // undefined (不报错)
// 3. 计算属性名解构
const key = 'dynamicKey';
const { [key]: value } = { dynamicKey: 'value' };
console.log(value); // 'value'
// 4. 数组越界
const [a, b, c, d] = [1, 2];
console.log(a, b, c, d); // 1 2 undefined undefined
三、扩展运算符 (Spread/Rest)
3.1 展开运算符原理
底层机制
// 数组展开:调用 Iterator 接口
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
// 等价于
const arr2 = [];
for (const item of arr1) {
arr2.push(item);
}
arr2.push(4, 5);
// 对象展开:调用 Object.assign(浅拷贝)
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };
// 类似于(但不完全等价)
const obj2 = Object.assign({}, obj1, { c: 3 });
数组展开应用
// 1. 合并数组
const arr1 = [1, 2];
const arr2 = [3, 4];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4]
// 传统方法对比
const combined2 = arr1.concat(arr2);
// 2. 数组克隆(浅拷贝)
const original = [1, 2, 3];
const cloned = [...original];
cloned[0] = 999;
console.log(original); // [1, 2, 3] (不受影响)
// 3. 类数组转数组
const divs = document.querySelectorAll('div');
const divsArray = [...divs];
// 4. 字符串转数组(正确处理 Unicode)
const str = '𝒳𝒴𝒵';
console.log([...str]); // ['𝒳', '𝒴', '𝒵']
console.log(str.split('')); // 错误输出(会拆分代理对)
// 5. 传递函数参数
const numbers = [1, 5, 3, 9, 2];
console.log(Math.max(...numbers)); // 9
// 传统方法
console.log(Math.max.apply(null, numbers));
对象展开应用
// 1. 对象合并
const defaults = { theme: 'light', lang: 'en' };
const userSettings = { theme: 'dark' };
const finalSettings = { ...defaults, ...userSettings };
console.log(finalSettings); // { theme: 'dark', lang: 'en' }
// 注意:后面的覆盖前面的
const obj = { ...{ a: 1 }, ...{ a: 2 } };
console.log(obj.a); // 2
// 2. 添加/修改属性
const user = { name: 'Alice', age: 25 };
const updatedUser = { ...user, age: 26, city: 'Beijing' };
console.log(updatedUser); // { name: 'Alice', age: 26, city: 'Beijing' }
// 3. 条件属性
const includeEmail = true;
const user = {
name: 'Bob',
...(includeEmail && { email: 'bob@example.com' })
};
console.log(user); // { name: 'Bob', email: 'bob@example.com' }
// 4. 移除属性
const { password, ...publicUser } = {
id: 1,
name: 'Alice',
password: 'secret'
};
console.log(publicUser); // { id: 1, name: 'Alice' }
3.2 剩余参数原理
Rest 语法
// 函数剩余参数
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// 等价于(使用 arguments)
function sum() {
const numbers = Array.from(arguments);
return numbers.reduce((a, b) => a + b, 0);
}
// 数组剩余元素
const [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [2, 3, 4]
// 对象剩余属性
const { a, b, ...others } = { a: 1, b: 2, c: 3, d: 4 };
console.log(a, b); // 1 2
console.log(others); // { c: 3, d: 4 }
Rest vs Arguments
// arguments 问题
function traditional() {
console.log(arguments); // 类数组对象
console.log(Array.isArray(arguments)); // false
// 无法使用数组方法
// arguments.map(...) // TypeError
// 箭头函数无 arguments
const inner = () => {
console.log(arguments); // ReferenceError
};
}
// Rest 参数优势
function modern(...args) {
console.log(args); // 真数组
console.log(Array.isArray(args)); // true
// 可以使用数组方法
args.map(x => x * 2);
// 箭头函数也可用
const inner = (...innerArgs) => {
console.log(innerArgs);
};
}
// Rest 参数必须是最后一个参数
function valid(a, b, ...rest) {} // ✅
function invalid(...rest, a, b) {} // ❌ SyntaxError
3.3 深拷贝与浅拷贝
浅拷贝陷阱
// 扩展运算符只做浅拷贝
const original = {
name: 'Alice',
address: { city: 'Beijing' }
};
const shallow = { ...original };
// 修改嵌套对象
shallow.address.city = 'Shanghai';
console.log(original.address.city); // 'Shanghai' (被影响了!)
console.log(original === shallow); // false
console.log(original.address === shallow.address); // true (共享引用)
深拷贝方案对比
const data = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'coding'],
address: {
city: 'Beijing',
district: { name: 'Chaoyang' }
}
};
// 1. JSON 方法(最简单,有局限)
const copy1 = JSON.parse(JSON.stringify(data));
// 问题:丢失函数、undefined、Symbol、循环引用
// 2. 递归手写
function deepClone(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj); // 处理循环引用
const clone = Array.isArray(obj) ? [] : {};
cache.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], cache);
}
}
return clone;
}
const copy2 = deepClone(data);
// 3. 现代浏览器: structuredClone
const copy3 = structuredClone(data);
// 优点:原生、性能好、支持循环引用、Date、RegExp、Map、Set 等
// 缺点:不支持函数、Symbol
// 4. 库方案
// import _ from 'lodash';
// const copy4 = _.cloneDeep(data);
实战:不可变更新
// React/Redux 中的状态更新
const state = {
user: {
name: 'Alice',
profile: {
age: 25,
city: 'Beijing'
}
},
settings: { theme: 'light' }
};
// ❌ 错误:直接修改(会导致引用不变,组件不更新)
state.user.profile.age = 26;
// ✅ 正确:不可变更新
const newState = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
age: 26
}
}
};
// 使用 Immer 库简化
// import produce from 'immer';
// const newState = produce(state, draft => {
// draft.user.profile.age = 26;
// });
3.4 实战应用场景
场景 1: 数组去重
// 方案1: Set + 展开运算符
const arr = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]
// 方案2: 对象去重(复杂对象)
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }
];
const uniqueUsers = [
...new Map(users.map(u => [u.id, u])).values()
];
console.log(uniqueUsers);
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
场景 2: 条件渲染属性
// React 组件
function Button({
onClick,
disabled = false,
loading = false,
type = 'button'
}) {
return (
<button
type={type}
onClick={onClick}
{...(disabled && { disabled: true })}
{...(loading && { 'aria-busy': true })}
className={`btn ${loading ? 'btn-loading' : ''}`}
>
{loading ? 'Loading...' : 'Submit'}
</button>
);
}
场景 3: 合并配置
// 默认配置
const defaultConfig = {
timeout: 5000,
headers: {
'Content-Type': 'application/json'
},
retries: 3
};
// 用户配置
const userConfig = {
timeout: 10000,
headers: {
'Authorization': 'Bearer token'
}
};
// 合并(注意:嵌套对象需要深度合并)
const config = {
...defaultConfig,
...userConfig,
headers: {
...defaultConfig.headers,
...userConfig.headers
}
};
console.log(config);
// {
// timeout: 10000,
// headers: {
// 'Content-Type': 'application/json',
// 'Authorization': 'Bearer token'
// },
// retries: 3
// }
场景 4: 函数柯里化
// 使用剩余参数实现通用柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, [...args, ...nextArgs]);
};
}
};
}
// 使用示例
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
场景 5: 数组分组
// 按属性分组
function groupBy(arr, key) {
return arr.reduce((groups, item) => {
const group = item[key];
return {
...groups,
[group]: [...(groups[group] || []), item]
};
}, {});
}
const users = [
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
{ name: 'Charlie', role: 'admin' }
];
const grouped = groupBy(users, 'role');
console.log(grouped);
// {
// admin: [{ name: 'Alice', role: 'admin' }, { name: 'Charlie', role: 'admin' }],
// user: [{ name: 'Bob', role: 'user' }]
// }
3.5 性能考量
性能对比
// 测试数据
const arr = Array.from({ length: 10000 }, (_, i) => i);
// 方案1: 扩展运算符
console.time('spread');
const copy1 = [...arr];
console.timeEnd('spread'); // ~0.1ms
// 方案2: Array.from
console.time('from');
const copy2 = Array.from(arr);
console.timeEnd('from'); // ~0.15ms
// 方案3: slice
console.time('slice');
const copy3 = arr.slice();
console.timeEnd('slice'); // ~0.05ms
// 方案4: concat
console.time('concat');
const copy4 = [].concat(arr);
console.timeEnd('concat'); // ~0.05ms
// 结论:小数据量差异不大,大数据量 slice/concat 略快
避免过度使用
// ❌ 避免:循环中频繁创建新对象
const result = [];
for (let i = 0; i < 1000; i++) {
result.push({ ...obj, id: i }); // 每次都创建新对象
}
// ✅ 优化:只在必要时创建
const result = [];
for (let i = 0; i < 1000; i++) {
if (needsNewObject) {
result.push({ ...obj, id: i });
} else {
result.push(obj);
}
}
// ❌ 避免:不必要的展开
function process(...args) {
return [...args]; // 多余,args 本身已是数组
}
// ✅ 直接返回
function process(...args) {
return args;
}
四、综合实战案例
案例 1: 实现不可变数据更新工具
// 类似 Immer 的简化版
class ImmerLite {
produce(baseState, recipe) {
const draft = this.createDraft(baseState);
recipe(draft);
return this.finalize(draft);
}
createDraft(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const draft = Array.isArray(obj) ? [...obj] : { ...obj };
for (const key in draft) {
draft[key] = this.createDraft(draft[key]);
}
return draft;
}
finalize(draft) {
return draft;
}
}
// 使用示例
const immer = new ImmerLite();
const state = {
user: { name: 'Alice', age: 25 },
todos: [{ id: 1, text: 'Learn', done: false }]
};
const newState = immer.produce(state, draft => {
draft.user.age = 26;
draft.todos[0].done = true;
draft.todos.push({ id: 2, text: 'Practice', done: false });
});
console.log(state.user.age); // 25 (原对象不变)
console.log(newState.user.age); // 26
案例 2: 函数组合工具
// compose 和 pipe 实现
const compose = (...fns) => x =>
fns.reduceRight((v, f) => f(v), x);
const pipe = (...fns) => x =>
fns.reduce((v, f) => f(v), x);
// 使用示例
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;
const composed = compose(subtract5, multiply2, add10);
console.log(composed(5)); // (5+10)*2-5 = 25
const piped = pipe(add10, multiply2, subtract5);
console.log(piped(5)); // (5+10)*2-5 = 25
// 实战:数据处理管道
const processUser = pipe(
user => ({ ...user, name: user.name.trim() }),
user => ({ ...user, age: parseInt(user.age) }),
user => ({ ...user, createdAt: new Date() })
);
const rawUser = { name: ' Alice ', age: '25' };
const processedUser = processUser(rawUser);
console.log(processedUser);
// { name: 'Alice', age: 25, createdAt: 2024-... }
案例 3: Redux-style Reducer
// Action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const UPDATE_USER = 'UPDATE_USER';
// Initial state
const initialState = {
user: { name: '', email: '' },
todos: []
};
// Reducer (使用扩展运算符保证不可变性)
function rootReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: action.payload,
done: false
}
]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, done: !todo.done }
: todo
)
};
case UPDATE_USER:
return {
...state,
user: {
...state.user,
...action.payload
}
};
default:
return state;
}
}
// 使用
let state = initialState;
state = rootReducer(state, {
type: ADD_TODO,
payload: 'Learn Redux'
});
state = rootReducer(state, {
type: UPDATE_USER,
payload: { name: 'Alice', email: 'alice@example.com' }
});
console.log(state);
案例 4: API 请求封装
// 通用请求函数
async function request(url, options = {}) {
// 默认配置
const defaults = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
timeout: 5000
};
// 合并配置
const config = {
...defaults,
...options,
headers: {
...defaults.headers,
...options.headers
}
};
// 解构配置
const { timeout, ...fetchOptions } = config;
// 超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
// 使用示例
async function getUserData(userId, options) {
const {
includeProfile = true,
includePosts = false,
...restOptions
} = options || {};
const [user, profile, posts] = await Promise.all([
request(`/api/users/${userId}`, restOptions),
includeProfile
? request(`/api/users/${userId}/profile`, restOptions)
: null,
includePosts
? request(`/api/users/${userId}/posts`, restOptions)
: null
]);
return {
...user,
...(profile && { profile }),
...(posts && { posts })
};
}
五、面试高频题解析
题目 1: 箭头函数的 this
// 问题:输出是什么?
const obj = {
name: 'Object',
regularFunc: function() {
console.log('1:', this.name);
setTimeout(function() {
console.log('2:', this.name);
}, 100);
setTimeout(() => {
console.log('3:', this.name);
}, 100);
},
arrowFunc: () => {
console.log('4:', this.name);
}
};
obj.regularFunc();
obj.arrowFunc();
// 答案:
// 1: Object (regularFunc 的 this 是 obj)
// 4: undefined (arrowFunc 的 this 是定义时的外层,通常是 window)
// 2: undefined (setTimeout 回调的 this 是 window)
// 3: Object (箭头函数捕获 regularFunc 的 this,即 obj)
题目 2: 解构赋值默认值
// 问题:输出是什么?
const { a = 10, b = 5 } = { a: 3 };
console.log(a, b); // ?
const { x: { y = 10 } = {} } = { x: { y: 3 } };
console.log(y); // ?
const { z = 10 } = { z: undefined };
console.log(z); // ?
const { w = 10 } = { w: null };
console.log(w); // ?
// 答案:
// 3 5 (a 有值用 3,b 无值用默认 5)
// 3 (y 的值是 3)
// 10 (undefined 触发默认值)
// null (null 不触发默认值)
题目 3: 扩展运算符浅拷贝
// 问题:修改 copy 会影响 original 吗?
const original = {
name: 'Alice',
hobbies: ['reading', 'coding'],
address: { city: 'Beijing' }
};
const copy = { ...original };
copy.name = 'Bob';
copy.hobbies.push('gaming');
copy.address.city = 'Shanghai';
console.log(original.name); // ?
console.log(original.hobbies); // ?
console.log(original.address.city); // ?
// 答案:
// Alice (基本类型不受影响)
// ['reading', 'coding', 'gaming'] (数组是引用,受影响)
// Shanghai (对象是引用,受影响)
题目 4: 数组解构交换
// 问题:不使用临时变量交换 a 和 b
let a = 1, b = 2;
// 答案
[a, b] = [b, a];
console.log(a, b); // 2 1
// 原理
// 1. [b, a] 创建临时数组 [2, 1]
// 2. 解构赋值 a = 2, b = 1
题目 5: Rest 参数与 Arguments
// 问题:以下代码有什么问题?
function sum(...args) {
return arguments.reduce((a, b) => a + b);
}
// 答案:
// 问题:箭头函数没有 arguments
// 解决:
function sum(...args) {
return args.reduce((a, b) => a + b);
}
// 或者
function sum() {
return Array.from(arguments).reduce((a, b) => a + b);
}
题目 6: 对象解构重命名
// 问题:从 user 对象中提取 name 并重命名为 userName
const user = { name: 'Alice', age: 25 };
// 答案
const { name: userName } = user;
console.log(userName); // Alice
console.log(name); // ReferenceError (name 是属性名,不是变量)
题目 7: 数组展开与 concat
// 问题:以下两种方式有区别吗?
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const result1 = [...arr1, ...arr2];
const result2 = arr1.concat(arr2);
// 答案:
// 结果相同,但细微区别:
// 1. 展开运算符更灵活(可以在任意位置插入)
const result3 = [...arr1, 'middle', ...arr2];
// 2. concat 不修改原数组,展开也不修改
// 3. 性能上大数据量 concat 略快
题目 8: 深度解构陷阱
// 问题:以下代码会报错吗?
const obj = {
a: {
b: {
c: 1
}
}
};
const { a: { b: { c, d = 10 } } } = obj;
console.log(c, d); // ?
const { x: { y } } = obj; // ?
// 答案:
// 1 10 (c 存在值为 1,d 不存在用默认值)
// TypeError: Cannot destructure property 'y' of 'undefined' (x 不存在)
题目 9: 实现对象扁平化
// 问题:实现 flatten 函数
const nested = {
a: 1,
b: {
c: 2,
d: {
e: 3
}
}
};
// 目标输出: { a: 1, 'b.c': 2, 'b.d.e': 3 }
// 答案
function flatten(obj, prefix = '', result = {}) {
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null) {
flatten(value, newKey, result);
} else {
result[newKey] = value;
}
}
return result;
}
console.log(flatten(nested));
题目 10: 数组去重(保留顺序)
// 问题:实现数组去重,保留第一次出现的元素
const arr = [1, 2, 2, 3, 1, 4, 3];
// 答案1: Set
const unique1 = [...new Set(arr)];
// 答案2: filter
const unique2 = arr.filter((item, index) => arr.indexOf(item) === index);
// 答案3: reduce
const unique3 = arr.reduce((acc, item) =>
acc.includes(item) ? acc : [...acc, item], []
);
console.log(unique1); // [1, 2, 3, 4]
// 对象数组去重
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }
];
const uniqueUsers = [
...new Map(users.map(u => [u.id, u])).values()
];
总结
核心要点
箭头函数
- this 词法绑定: 定义时捕获,无法改变
- 没有 arguments/prototype/super
- 不能作为构造函数
- 适用场景: 回调、不需要动态 this 的函数
解构赋值
- 数组解构: 基于 Iterator 接口
- 对象解构: 基于属性访问
- 默认值: 只有
undefined触发 - 适用场景: 函数参数、提取数据、变量交换
扩展运算符
- 展开: 基于 Iterator(数组)/属性复制(对象)
- 剩余: 收集多余参数/属性
- 浅拷贝: 只复制第一层
- 适用场景: 数组/对象合并、函数参数、不可变更新
最佳实践
- 箭头函数: 优先用于回调,避免用作对象方法
- 解构: 提高代码可读性,但避免过度嵌套
- 扩展运算符: 简化数组/对象操作,注意浅拷贝陷阱
- 性能: 小数据量差异不大,大数据量考虑传统方法
- 可读性: 简洁优先,但不牺牲清晰度
进阶学习
- TypeScript: 类型标注增强安全性
- 函数式编程: 结合 Ramda/Lodash-fp
- 不可变数据: Immer/Immutable.js
- 性能优化: 大数据量场景的替代方案
祝你在面试和工作中游刃有余! 🚀