未找到匹配的笔记

2.3 - 箭头函数、解构赋值、扩展运算符

JavaScript面试

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(数组)/属性复制(对象)
  • 剩余: 收集多余参数/属性
  • 浅拷贝: 只复制第一层
  • 适用场景: 数组/对象合并、函数参数、不可变更新

最佳实践

  1. 箭头函数: 优先用于回调,避免用作对象方法
  2. 解构: 提高代码可读性,但避免过度嵌套
  3. 扩展运算符: 简化数组/对象操作,注意浅拷贝陷阱
  4. 性能: 小数据量差异不大,大数据量考虑传统方法
  5. 可读性: 简洁优先,但不牺牲清晰度

进阶学习

  • TypeScript: 类型标注增强安全性
  • 函数式编程: 结合 Ramda/Lodash-fp
  • 不可变数据: Immer/Immutable.js
  • 性能优化: 大数据量场景的替代方案

祝你在面试和工作中游刃有余! 🚀