skip to content
月与羽

js 中的语法糖

/ 17 min read

…的使用

1. Rest Parameters (剩余参数) - 聚集

... 出现在函数的参数列表中时,它就是“剩余参数”。它的作用是捕获函数调用时传入的所有剩余的、未被命名的参数,并将它们放入一个真正的数组中。

核心规则:

  • 必须是函数的最后一个参数
  • 一个函数定义中只能有一个剩余参数。

在没有 Rest Parameters 的“远古时代”(ES5)

以前,我们需要使用 arguments 对象来获取所有参数。但 arguments 是一个“类数组对象”,而不是真正的数组,使用起来很麻烦(没有 forEach, map 等方法)。

function sumOld() {
// arguments is not a real array, so we have to convert it first
const args = Array.prototype.slice.call(arguments);
let total = 0;
for (let i = 0; i < args.length; i++) {
total += args[i];
}
return total;
}
console.log(sumOld(1, 2, 3)); // 6

使用 Rest Parameters (现代 JS)

代码变得极其简洁和直观。

// ...numbers 会将所有传入的参数聚集到一个名为 `numbers` 的数组中
function sum(...numbers) {
// `numbers` 是一个真正的数组,可以直接使用数组方法
return numbers.reduce((total, current) => total + current, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
console.log(sum()); // 0

结合普通参数使用:

function logTeam(captain, coach, ...players) {
console.log(`Captain: ${captain}`);
console.log(`Coach: ${coach}`);
console.log(`Players: ${players.join(', ')}`);
}
logTeam("Alice", "Bob", "Charlie", "David", "Eve");
// 输出:
// Captain: Alice
// Coach: Bob
// Players: Charlie, David, Eve

在这里,players 捕获了除了前两个以外的所有剩余参数。


2. Spread Syntax (展开语法) - 展开

... 出现在函数调用、数组字面量或对象字面量中时,它就是“展开语法”。它的作用是将一个可迭代对象(如数组、字符串)或对象“展开”成其独立的组成部分。

用途一:在函数调用中展开数组

这是 Rest Parameters 的反向操作。

const numbers = [10, 5, 25, 15];
// 以前需要使用 .apply()
const max_old = Math.max.apply(null, numbers);
// 现在使用展开语法,非常直观
const max = Math.max(...numbers); // 等价于 Math.max(10, 5, 25, 15)
console.log(max); // 25

用途二:在数组字面量 [] 中展开

这在处理数组时非常有用,尤其是在**不改变原数组(Immutability)**的情况下。

a) 复制数组(浅拷贝)

const original = ['a', 'b', 'c'];
const copy = [...original];
copy.push('d');
console.log(original); // ['a', 'b', 'c'] (原数组未受影响)
console.log(copy); // ['a', 'b', 'c', 'd']

b) 合并数组

const arr1 = [1, 2];
const arr2 = [3, 4];
const combined = [...arr1, 'middle', ...arr2];
console.log(combined); // [1, 2, 'middle', 3, 4]

c) 将字符串转换为字符数组

const str = "hello";
const chars = [...str];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']

用途三:在对象字面量 {} 中展开 (ES2018)

这对于处理对象同样非常强大,是 React 和其他现代框架中状态管理的核心。

a) 复制对象(浅拷贝)

const originalObj = { name: "Alice", age: 30 };
const copyObj = { ...originalObj };
console.log(copyObj); // { name: "Alice", age: 30 }

b) 合并对象

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 }; // 注意,obj2 也有属性 'b'
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // { a: 1, b: 3, c: 4 }
// 规则:后面的同名属性会覆盖前面的。

c) 以不可变的方式更新对象属性

这是它最常见的用途!在不直接修改原对象的情况下,创建一个包含更新值的新对象。

const user = {
id: 101,
name: "Bob",
profile: {
theme: "dark",
notifications: true
}
};
// 更新用户的 name
const updatedUser = { ...user, name: "Charlie" };
console.log(updatedUser); // { id: 101, name: "Charlie", profile: {...} }
console.log(user); // { id: 101, name: "Bob", profile: {...} } (原对象未变)
// 更新嵌套对象的属性
const userWithNewTheme = {
...user,
profile: {
...user.profile, // 先展开老的 profile
theme: "light" // 然后用新值覆盖 theme
}
};
console.log(userWithNewTheme.profile.theme); // "light"
  • 注意:* 无论是数组还是对象,展开语法执行的都是浅拷贝 (Shallow Copy)。这意味着如果对象或数组中包含其他对象(如上面的 profile),那么只复制了对那个嵌套对象的引用,而不是复制嵌套对象本身。

总结

Rest Parameters (剩余参数)Spread Syntax (展开语法)
作用聚集 (Gather)展开 (Spread)
做什么将多个独立的元素收集成一个数组将一个数组/对象展开成独立的元素
用在何处函数的参数定义中,且必须是最后一个。函数调用时、数组字面量[] 中、对象字面量{} 中。
示例function fn(a, ...rest) {}const arr = [...oldArr];
fn(...args);
const obj = {...oldObj};

可选链操作符(Optional Chaining ?.)

可选链操作符 ?. 是 ES2020 引入的语法糖,用于安全地访问嵌套对象属性,即使中间某个引用为 nullundefined 也不会抛出错误。它是现代 JavaScript 中处理不确定数据结构的重要工具。

核心概念

问题场景: 在访问深层嵌套对象时,如果中间任何一层为 nullundefined,就会抛出 TypeError

// 危险的传统写法
const user = null;
console.log(user.profile.name); // ❌ TypeError: Cannot read property 'profile' of null
// 或者后端返回的不完整数据
const response = {
data: {
user: null // 用户未登录
}
};
console.log(response.data.user.profile.name); // ❌ TypeError

解决方案: 可选链操作符提供了优雅的短路机制:

const user = null;
console.log(user?.profile?.name); // ✅ undefined (不报错)
const response = {
data: {
user: null
}
};
console.log(response.data.user?.profile?.name); // ✅ undefined

三种语法形式

1. 属性访问:obj?.prop

const user = {
id: 1,
profile: {
name: 'Alice',
avatar: 'avatar.jpg'
}
};
// 安全访问存在的属性
console.log(user?.profile?.name); // 'Alice'
// 安全访问不存在的属性
console.log(user?.settings?.theme); // undefined
// 处理 null/undefined 对象
const emptyUser = null;
console.log(emptyUser?.profile?.name); // undefined

2. 方括号访问:obj?.[expr]

用于动态属性名或需要计算的属性访问:

const config = {
development: { apiUrl: 'dev-api.com' },
production: { apiUrl: 'api.com' }
};
const env = 'development';
console.log(config?.[env]?.apiUrl); // 'dev-api.com'
// 处理不存在的环境配置
const invalidEnv = 'testing';
console.log(config?.[invalidEnv]?.apiUrl); // undefined
// 与变量结合使用
const user = { name: 'Bob', age: 30 };
const field = 'email'; // 这个属性不存在
console.log(user?.[field]?.toLowerCase()); // undefined

3. 方法调用:obj?.method?.()

安全调用可能不存在的方法:

const api = {
user: {
getName: () => 'Alice',
getProfile: null // 方法不存在或为 null
}
};
// 安全调用存在的方法
console.log(api.user?.getName?.()); // 'Alice'
// 安全调用不存在的方法
console.log(api.user?.getProfile?.()); // undefined
// 处理整个对象都不存在的情况
const emptyApi = null;
console.log(emptyApi?.user?.getName?.()); // undefined

可选链实际应用场景

1. API 响应处理

// 模拟不稳定的 API 响应
const apiResponses = [
{
data: {
user: {
profile: {
name: 'John',
contact: {
email: 'john@email.com'
}
}
}
}
},
{
data: {
user: null // 用户未登录
}
},
{
data: null // 请求失败
},
null // 网络错误
];
// 安全处理所有情况
apiResponses.forEach((response, index) => {
const email = response?.data?.user?.profile?.contact?.email;
console.log(`Response ${index}: ${email || 'No email'}`);
});
// 输出:
// Response 0: john@email.com
// Response 1: No email
// Response 2: No email
// Response 3: No email

2. DOM 操作

// 安全访问 DOM 元素
const element = document.querySelector('#user-panel');
// 传统写法(容易出错)
// if (element && element.querySelector && element.querySelector('.avatar')) {
// element.querySelector('.avatar').src = 'new-avatar.jpg';
// }
// 可选链写法(简洁安全)
const avatar = element?.querySelector?.('.avatar');
if (avatar) {
avatar.src = 'new-avatar.jpg';
}
// 或者更简洁
element?.querySelector?.('.avatar')?.setAttribute?.('src', 'new-avatar.jpg');

3. 事件处理和回调函数

// 配置对象可能包含可选的回调函数
function processData(data, options = {}) {
// 处理数据...
const result = data.map(item => item.value);
// 安全调用可选的回调函数
options?.onSuccess?.(result);
options?.logger?.log?.('Processing completed');
return result;
}
// 使用示例
processData([{value: 1}, {value: 2}], {
onSuccess: (result) => console.log('Success:', result),
logger: console // 有 log 方法
});
processData([{value: 3}], {
// 没有提供回调函数 - 不会报错
});

4. 配置和设置管理

// 应用配置,某些配置项可能不存在
const appConfig = {
theme: {
primary: '#007bff',
// secondary 颜色未配置
},
features: {
darkMode: true,
// notifications 功能未配置
}
};
// 安全获取配置值,提供默认值
const primaryColor = appConfig?.theme?.primary ?? '#000000';
const secondaryColor = appConfig?.theme?.secondary ?? '#6c757d';
const notificationsEnabled = appConfig?.features?.notifications ?? false;
console.log({
primaryColor, // '#007bff'
secondaryColor, // '#6c757d' (默认值)
notificationsEnabled // false (默认值)
});

与其他操作符结合

与空值合并操作符 ?? 结合

const user = {
profile: {
name: null, // 名字为 null
bio: '' // 简介为空字符串
}
};
// 只有 null/undefined 才使用默认值
const name = user?.profile?.name ?? 'Anonymous';
const bio = user?.profile?.bio ?? 'No bio available';
console.log(name); // 'Anonymous'
console.log(bio); // '' (空字符串不会被替换)
// 如果想把空字符串也替换,使用 ||
const bioWithOr = user?.profile?.bio || 'No bio available';
console.log(bioWithOr); // 'No bio available'

与解构赋值结合

const response = {
data: {
user: {
profile: {
name: 'Alice',
contact: {
email: 'alice@email.com'
}
}
}
}
};
// 先用可选链确保路径存在,再解构
const profile = response?.data?.user?.profile;
if (profile) {
const { name, contact: { email } = {} } = profile;
console.log(name, email); // 'Alice', 'alice@email.com'
}

注意事项和限制

1. 不能用于赋值

const obj = {};
// ❌ 错误:不能用可选链进行赋值
// obj?.prop = 'value'; // SyntaxError
// ✅ 正确:先检查再赋值
if (obj) {
obj.prop = 'value';
}

2. 短路求值的性能考虑

const expensiveOperation = () => {
console.log('执行了昂贵的计算');
return { result: 42 };
};
const obj = null;
// 由于 obj 为 null,expensiveOperation 不会被调用
const result = obj?.someMethod?.(expensiveOperation());
// 不会打印 '执行了昂贵的计算'

3. 与 delete 操作符结合

const user = {
profile: {
temporaryData: 'should be removed'
}
};
// ✅ 安全删除
delete user?.profile?.temporaryData;
// 对于不存在的路径,delete 也是安全的
delete user?.settings?.cache; // 不会报错

可选链的优势总结

  1. 安全性:避免 TypeError 异常
  2. 简洁性:减少冗长的 if 检查
  3. 可读性:代码意图更加清晰
  4. 维护性:减少因数据结构变化导致的错误
  5. 性能:短路求值避免不必要的计算

可选链操作符是现代 JavaScript 中处理不确定数据结构的标准做法,特别适用于 API 数据处理、DOM 操作和配置管理等场景。


解构赋值(Destructuring Assignment)

解构赋值是一种方便的语法糖,可以从数组或对象中提取数据,并将这些值分配给变量。它让代码更简洁、易读。

数组解构

数组基本用法

// 传统方式
const fruits = ['apple', 'banana', 'orange'];
const first = fruits[0];
const second = fruits[1];
// 解构方式
const [first, second, third] = fruits;
console.log(first); // 'apple'
console.log(second); // 'banana'
console.log(third); // 'orange'

跳过元素

const colors = ['red', 'green', 'blue', 'yellow'];
// 只要第一个和第三个
const [first, , third] = colors;
console.log(first); // 'red'
console.log(third); // 'blue'

数组默认值

const [a = 1, b = 2, c = 3] = [10, 20];
console.log(a); // 10 (实际值)
console.log(b); // 20 (实际值)
console.log(c); // 3 (默认值)

与剩余参数结合

const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]

对象解构

对象基本用法

// 传统方式
const user = { name: 'Alice', age: 30, city: 'New York' };
const name = user.name;
const age = user.age;
// 解构方式
const { name, age, city } = user;
console.log(name); // 'Alice'
console.log(age); // 30
console.log(city); // 'New York'

重命名变量

const user = { name: 'Bob', age: 25 };
// 将 name 重命名为 username
const { name: username, age: userAge } = user;
console.log(username); // 'Bob'
console.log(userAge); // 25

对象默认值

const settings = { theme: 'dark' };
const { theme, language = 'en', notifications = true } = settings;
console.log(theme); // 'dark'
console.log(language); // 'en' (默认值)
console.log(notifications); // true (默认值)

嵌套解构

const data = {
user: {
profile: {
name: 'Charlie',
preferences: {
theme: 'light',
notifications: true
}
}
}
};
// 深层嵌套解构
const {
user: {
profile: {
name,
preferences: { theme, notifications }
}
}
} = data;
console.log(name); // 'Charlie'
console.log(theme); // 'light'
console.log(notifications); // true

剩余属性 (Rest Properties)

const user = { name: 'David', age: 28, city: 'Boston', country: 'USA' };
const { name, age, ...address } = user;
console.log(name); // 'David'
console.log(age); // 28
console.log(address); // { city: 'Boston', country: 'USA' }

函数参数解构

对象参数解构

// 传统方式
function createUser(options) {
const name = options.name || 'Anonymous';
const age = options.age || 0;
const isActive = options.isActive || false;
// ...
}
// 解构方式
function createUser({ name = 'Anonymous', age = 0, isActive = false }) {
console.log(`User: ${name}, Age: ${age}, Active: ${isActive}`);
}
createUser({ name: 'Eve', age: 22 });
// 输出: User: Eve, Age: 22, Active: false

数组参数解构

function processCoordinates([x, y, z = 0]) {
console.log(`X: ${x}, Y: ${y}, Z: ${z}`);
}
processCoordinates([10, 20]); // X: 10, Y: 20, Z: 0
processCoordinates([5, 15, 25]); // X: 5, Y: 15, Z: 25

实际应用场景

1. 交换变量

let a = 1;
let b = 2;
// 传统方式需要临时变量
// let temp = a;
// a = b;
// b = temp;
// 解构方式,一行搞定
[a, b] = [b, a];
console.log(a, b); // 2, 1

2. 函数返回多个值

function getMinMax(numbers) {
return [Math.min(...numbers), Math.max(...numbers)];
}
const [min, max] = getMinMax([3, 1, 4, 1, 5, 9]);
console.log(min, max); // 1, 9

3. 从 API 响应中提取数据

// 模拟 API 响应
const apiResponse = {
data: {
user: {
id: 123,
profile: {
name: 'John',
email: 'john@example.com'
}
}
},
status: 'success',
message: 'Data retrieved successfully'
};
// 直接提取需要的数据
const {
data: {
user: {
id: userId,
profile: { name: userName, email }
}
},
status
} = apiResponse;
console.log(userId, userName, email, status);
// 输出: 123, 'John', 'john@example.com', 'success'

4. React 中的 props 解构

// 传统方式
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2>
<p>Age: {props.age}</p>
<p>Email: {props.email}</p>
</div>
);
}
// 解构方式
function UserCard({ name, age, email, isOnline = false }) {
return (
<div>
<h2>{name} {isOnline ? '🟢' : '🔴'}</h2>
<p>Age: {age}</p>
<p>Email: {email}</p>
</div>
);
}

解构总结

解构赋值的核心优势:

  1. 简洁性:减少代码量,提高可读性
  2. 默认值:优雅处理缺失的数据
  3. 重命名:避免变量名冲突
  4. 深层提取:直接访问嵌套数据
  5. 函数参数:让函数接口更清晰

解构赋值是现代 JavaScript 开发中不可或缺的语法糖,特别是在处理复杂数据结构和函数参数时。