JavaScript ES6+ 新特性面试题
1. 请简述 ES6 引入的主要新特性
Details
ES6(ECMAScript 2015)是 JavaScript 的一个重要版本,引入了许多新特性,以下是主要的新特性:
1. 箭头函数(Arrow Functions)
箭头函数提供了更简洁的函数语法,并且没有自己的 this 绑定。
// 传统函数
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
// 带函数体的箭头函数
const multiply = (a, b) => {
const result = a * b;
return result;
};2. 模板字符串(Template Literals)
模板字符串允许在字符串中嵌入表达式,使用反引号(`)包围。
const name = "John";
const age = 30;
// 传统字符串拼接
const message = "My name is " + name + " and I am " + age + " years old.";
// 模板字符串
const message = `My name is ${name} and I am ${age} years old.`;3. 解构赋值(Destructuring Assignment)
解构赋值允许从数组或对象中提取值,并赋给变量。
// 数组解构
const [a, b, ...rest] = [1, 2, 3, 4, 5];
console.log(a); // 1
console.log(b); // 2
console.log(rest); // [3, 4, 5]
// 对象解构
const { name, age, ...other } = { name: "John", age: 30, city: "New York" };
console.log(name); // "John"
console.log(age); // 30
console.log(other); // { city: "New York" }4. 扩展运算符(Spread Operator)
扩展运算符允许将数组或对象展开为多个元素。
// 数组扩展
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5]
// 对象扩展
const obj1 = { name: "John" };
const obj2 = { ...obj1, age: 30 };
console.log(obj2); // { name: "John", age: 30 }5. let 和 const 声明
let 和 const 提供了块级作用域的变量声明,替代了 var。
// let 声明(可变)
let count = 1;
count = 2; // 合法
// const 声明(不可变)
const PI = 3.14159;
PI = 3.14; // 错误
// 块级作用域
if (true) {
let blockVar = "block"; // 只在块内可见
const blockConst = "constant"; // 只在块内可见
}
console.log(blockVar); // 错误,blockVar 未定义6. 类(Classes)
ES6 引入了类语法,提供了更清晰的面向对象编程方式。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name}`;
}
static create(name, age) {
return new Person(name, age);
}
}
const person = new Person("John", 30);
console.log(person.greet()); // "Hello, my name is John"
const person2 = Person.create("Jane", 25);
console.log(person2.greet()); // "Hello, my name is Jane"7. 模块(Modules)
ES6 引入了模块系统,允许将代码分割为独立的模块。
// 导出模块
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export default {
multiply: (a, b) => a * b,
divide: (a, b) => a / b
};
// 导入模块
// app.js
import math, { add, subtract } from './math.js';
console.log(add(1, 2)); // 3
console.log(subtract(5, 3)); // 2
console.log(math.multiply(2, 3)); // 68. Promise
Promise 提供了一种处理异步操作的方法,避免了回调地狱。
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { name: "John" };
resolve(data);
}, 1000);
});
};
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));9. 迭代器和生成器(Iterators and Generators)
迭代器和生成器提供了一种更灵活的遍历数据的方式。
// 生成器函数
function* generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 310. 其他新特性
- 默认参数:
function greet(name = "World") { ... } - 剩余参数:
function sum(...numbers) { ... } - 增强的对象字面量:
const name = "John"; const obj = { name, greet() { ... } } - Symbol:一种新的原始数据类型,表示唯一的标识符
- Map 和 Set:新的数据结构
- WeakMap 和 WeakSet:弱引用的数据结构
- Proxy 和 Reflect:用于拦截和自定义对象行为
2. 请解释 ES6 中的箭头函数与传统函数的区别
Details
ES6 引入的箭头函数与传统函数相比,有以下几个主要区别:
1. 语法简洁
箭头函数提供了更简洁的语法,特别是对于单行函数体。
// 传统函数
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;2. this 绑定
箭头函数没有自己的 this 绑定,它会继承外层作用域的 this。
// 传统函数
const obj1 = {
name: "John",
greet: function() {
setTimeout(function() {
console.log(this.name); // undefined,因为 this 指向全局对象
}, 1000);
}
};
// 箭头函数
const obj2 = {
name: "John",
greet: function() {
setTimeout(() => {
console.log(this.name); // "John",继承 greet 方法的 this
}, 1000);
}
};3. 不能作为构造函数
箭头函数不能使用 new 关键字调用,因为它没有 prototype 属性。
const Person = (name) => {
this.name = name;
};
// 错误,箭头函数不能作为构造函数
const person = new Person("John");4. 没有 arguments 对象
箭头函数没有 arguments 对象,但可以使用剩余参数来实现类似功能。
// 传统函数
function sum() {
return Array.from(arguments).reduce((acc, num) => acc + num, 0);
}
// 箭头函数
const sum = (...args) => {
return args.reduce((acc, num) => acc + num, 0);
};5. 没有 prototype 属性
箭头函数没有 prototype 属性,因此不能向其添加方法。
const add = (a, b) => a + b;
console.log(add.prototype); // undefined6. 不能使用 yield 关键字
箭头函数不能作为生成器函数,不能使用 yield 关键字。
// 错误,箭头函数不能使用 yield
const generator = () => {
yield 1;
yield 2;
};总结
| 特性 | 传统函数 | 箭头函数 |
|---|---|---|
| 语法 | 较冗长 | 简洁 |
| this 绑定 | 动态绑定 | 继承外层作用域 |
| 构造函数 | 可以 | 不可以 |
| arguments 对象 | 有 | 无(使用剩余参数) |
| prototype 属性 | 有 | 无 |
| yield 关键字 | 可以 | 不可以 |
应用场景
- 箭头函数:适用于需要保持
this上下文的场景,如回调函数、事件处理函数等。 - 传统函数:适用于需要独立
this绑定的场景,如构造函数、方法等。
3. 请解释 ES6 中的模块系统
Details
ES6 引入了模块系统,允许将代码分割为独立的模块,每个模块可以导出(export)和导入(import)功能。
模块的基本概念
- 模块:一个独立的 JavaScript 文件,包含变量、函数、类等。
- 导出(Export):将模块中的内容暴露给其他模块。
- 导入(Import):从其他模块中引入内容。
导出方式
1. 命名导出(Named Exports)
可以导出多个变量、函数或类,每个都有自己的名称。
// math.js
// 导出单个变量
export const PI = 3.14159;
// 导出函数
export function add(a, b) {
return a + b;
}
// 导出类
export class Calculator {
add(a, b) {
return a + b;
}
}
// 批量导出
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;
export { subtract, multiply };2. 默认导出(Default Export)
每个模块只能有一个默认导出,通常用于导出模块的主要功能。
// math.js
// 默认导出对象
const math = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b
};
export default math;
// 或者直接导出
// export default {
// add: (a, b) => a + b,
// // ...
// };导入方式
1. 导入命名导出
使用花括号 {} 导入命名导出的内容。
// app.js
// 导入单个命名导出
import { add, subtract } from './math.js';
// 导入所有命名导出
import * as math from './math.js';
// 重命名导入
import { add as sum, subtract as minus } from './math.js';
console.log(add(1, 2)); // 3
console.log(math.multiply(2, 3)); // 6
console.log(sum(3, 4)); // 72. 导入默认导出
直接导入默认导出的内容,不需要使用花括号。
// app.js
// 导入默认导出
import math from './math.js';
// 重命名导入
import calculator from './math.js';
console.log(math.add(1, 2)); // 3
console.log(calculator.multiply(2, 3)); // 63. 混合导入
可以同时导入默认导出和命名导出。
// app.js
import math, { PI, add } from './math.js';
console.log(PI); // 3.14159
console.log(add(1, 2)); // 3
console.log(math.multiply(2, 3)); // 6模块的特点
- 自动严格模式:模块默认运行在严格模式下。
- 独立作用域:每个模块都有自己的作用域,变量不会泄漏到全局。
- 静态导入导出:导入和导出在编译时处理,不支持动态导入(ES2020 引入了动态导入)。
- 单例模式:每个模块只被加载一次,多次导入会使用同一个实例。
动态导入
ES2020 引入了动态导入,允许在运行时按需加载模块。
// 动态导入
async function loadModule() {
const math = await import('./math.js');
console.log(math.add(1, 2)); // 3
}
loadModule();模块系统的优势
- 代码组织:将代码分割为独立的模块,提高代码的可维护性。
- 代码复用:可以在多个项目中重用模块。
- 命名空间:避免变量名冲突,每个模块有自己的命名空间。
- tree-shaking:打包工具可以移除未使用的代码,减少最终 bundle 的大小。
与 CommonJS 的区别
| 特性 | ES6 模块 | CommonJS |
|---|---|---|
| 语法 | import/export | require()/module.exports |
| 加载方式 | 静态加载(编译时) | 动态加载(运行时) |
| 作用域 | 模块作用域 | 函数作用域 |
| this 值 | undefined | module.exports |
| 适用环境 | 浏览器和 Node.js | 主要用于 Node.js |
4. 请解释 ES6 中的 Promise
Details
Promise 是 ES6 引入的一种处理异步操作的方法,它代表一个异步操作的最终完成(或失败)及其结果值。
Promise 的基本概念
- Promise:一个对象,表示异步操作的最终状态(完成或失败)。
- 状态:
pending:初始状态,既不是成功,也不是失败。fulfilled:操作成功完成。rejected:操作失败。
- 链式调用:Promise 支持链式调用,使代码更清晰。
Promise 的创建
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = true;
if (success) {
resolve('操作成功');
} else {
reject('操作失败');
}
}, 1000);
});Promise 的使用
promise
.then(result => {
console.log(result); // '操作成功'
return '处理结果';
})
.then(processedResult => {
console.log(processedResult); // '处理结果'
})
.catch(error => {
console.error(error); // '操作失败'
})
.finally(() => {
console.log('无论成功或失败都会执行');
});Promise 的方法
1. Promise.all()
并行执行多个 Promise,当所有 Promise 都完成时返回结果数组。
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results); // [1, 2, 3]
});2. Promise.race()
并行执行多个 Promise,返回第一个完成的 Promise 的结果。
const promise1 = new Promise(resolve => setTimeout(resolve, 100, 'first'));
const promise2 = new Promise(resolve => setTimeout(resolve, 50, 'second'));
Promise.race([promise1, promise2])
.then(result => {
console.log(result); // 'second'
});3. Promise.allSettled()
并行执行多个 Promise,返回所有 Promise 的结果,无论成功或失败。
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject('error');
Promise.allSettled([promise1, promise2])
.then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'error' }
// ]
});4. Promise.any()
并行执行多个 Promise,返回第一个成功的 Promise 的结果。
const promise1 = Promise.reject('error1');
const promise2 = Promise.resolve('success');
const promise3 = Promise.reject('error2');
Promise.any([promise1, promise2, promise3])
.then(result => {
console.log(result); // 'success'
});Promise 的优势
- 避免回调地狱:通过链式调用,使异步代码更清晰。
- 错误处理:集中处理错误,避免在每个回调中处理错误。
- 更好的可读性:代码结构更清晰,易于理解和维护。
- 支持并行执行:可以同时执行多个异步操作,提高性能。
Promise 与 async/await
ES2017 引入了 async/await,使异步代码更像同步代码。
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();5. 请解释 ES6 中的 Map 和 Set
Details
ES6 引入了两种新的数据结构:Map 和 Set。
Map
Map 是一种键值对集合,与对象类似,但有以下区别:
- 键的类型:Map 的键可以是任何类型(包括对象、函数、基本类型),而对象的键只能是字符串或 Symbol。
- 键的顺序:Map 中的键是有序的,按照插入顺序排列,而对象的键在 ES6 之前是无序的。
- 大小:Map 有
size属性,可以直接获取键值对的数量,而对象需要手动计算。 - 迭代:Map 是可迭代的,可以使用
for...of循环遍历,而对象需要通过Object.keys()等方法获取键数组后再遍历。
Map 的基本操作
// 创建 Map
const map = new Map();
// 添加键值对
map.set('name', 'John');
map.set(1, 'one');
map.set(true, 'boolean');
// 获取值
console.log(map.get('name')); // 'John'
// 检查键是否存在
console.log(map.has('name')); // true
// 删除键值对
map.delete('name');
// 清空 Map
map.clear();
// 获取大小
console.log(map.size); // 0
// 遍历 Map
const map2 = new Map([
['name', 'John'],
['age', 30]
]);
// 遍历键
for (const key of map2.keys()) {
console.log(key);
}
// 遍历值
for (const value of map2.values()) {
console.log(value);
}
// 遍历键值对
for (const [key, value] of map2.entries()) {
console.log(`${key}: ${value}`);
}
// 使用 forEach
map2.forEach((value, key) => {
console.log(`${key}: ${value}`);
});Set
Set 是一种值的集合,其中每个值只能出现一次。
- 唯一性:Set 中的值是唯一的,不会重复。
- 值的类型:Set 可以存储任何类型的值(包括对象、函数、基本类型)。
- 值的顺序:Set 中的值是有序的,按照插入顺序排列。
- 大小:Set 有
size属性,可以直接获取值的数量。 - 迭代:Set 是可迭代的,可以使用
for...of循环遍历。
Set 的基本操作
// 创建 Set
const set = new Set();
// 添加值
set.add('apple');
set.add('banana');
set.add('apple'); // 重复值,不会添加
// 检查值是否存在
console.log(set.has('apple')); // true
// 删除值
set.delete('apple');
// 清空 Set
set.clear();
// 获取大小
console.log(set.size); // 0
// 遍历 Set
const set2 = new Set(['apple', 'banana', 'cherry']);
// 使用 for...of
for (const value of set2) {
console.log(value);
}
// 使用 forEach
set2.forEach(value => {
console.log(value);
});
// 转换为数组
const array = [...set2];
console.log(array); // ['apple', 'banana', 'cherry']
// 数组去重
const duplicateArray = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = [...new Set(duplicateArray)];
console.log(uniqueArray); // [1, 2, 3, 4, 5]WeakMap 和 WeakSet
ES6 还引入了 WeakMap 和 WeakSet,它们与 Map 和 Set 的区别在于:
- 弱引用:WeakMap 和 WeakSet 对键或值的引用是弱引用,不会阻止垃圾回收。
- 键的类型:WeakMap 的键只能是对象,WeakSet 只能存储对象。
- 不可迭代:WeakMap 和 WeakSet 不可迭代,没有
size属性,也没有keys()、values()、entries()方法。
WeakMap 的基本操作
const weakMap = new WeakMap();
const obj = {};
// 添加键值对
weakMap.set(obj, 'value');
// 获取值
console.log(weakMap.get(obj)); // 'value'
// 检查键是否存在
console.log(weakMap.has(obj)); // true
// 删除键值对
weakMap.delete(obj);WeakSet 的基本操作
const weakSet = new WeakSet();
const obj1 = {};
const obj2 = {};
// 添加值
weakSet.add(obj1);
weakSet.add(obj2);
// 检查值是否存在
console.log(weakSet.has(obj1)); // true
// 删除值
weakSet.delete(obj1);6. 请解释 ES6+ 中的 async/await
Details
async/await 是 ES2017 引入的特性,它建立在 Promise 之上,提供了一种更简洁、更像同步代码的方式来处理异步操作。
async 函数
- 定义:使用
async关键字声明的函数。 - 返回值:async 函数总是返回一个 Promise。如果函数返回一个值,Promise 会被解析为该值;如果函数抛出异常,Promise 会被拒绝。
await 表达式
- 用法:只能在 async 函数内部使用。
- 作用:暂停 async 函数的执行,等待 Promise 解析或拒绝,然后继续执行 async 函数并返回解析的值。
- 错误处理:可以使用 try-catch 捕获 await 表达式的错误。
基本用法
// 定义 async 函数
async function fetchData() {
try {
// 等待 Promise 解析
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
return data;
} catch (error) {
// 捕获错误
console.error('Error:', error);
throw error;
}
}
// 调用 async 函数
fetchData()
.then(data => console.log('Processed data:', data))
.catch(error => console.error('Error in fetchData:', error));并行执行
async function fetchMultipleData() {
// 并行执行多个异步操作
const [data1, data2] = await Promise.all([
fetch('https://api.example.com/data1').then(res => res.json()),
fetch('https://api.example.com/data2').then(res => res.json())
]);
console.log('Data 1:', data1);
console.log('Data 2:', data2);
return { data1, data2 };
}串行执行
async function fetchSerialData() {
// 串行执行异步操作
const data1 = await fetch('https://api.example.com/data1').then(res => res.json());
console.log('Data 1:', data1);
const data2 = await fetch('https://api.example.com/data2').then(res => res.json());
console.log('Data 2:', data2);
return { data1, data2 };
}async/await 的优势
- 代码可读性:代码结构更清晰,更像同步代码,易于理解和维护。
- 错误处理:可以使用 try-catch 统一处理错误,避免了 Promise 链式调用中的错误处理嵌套。
- 调试方便:可以在 async 函数中设置断点,逐行调试。
- 简化 Promise 链式调用:避免了 Promise 链式调用的嵌套,使代码更扁平。
注意事项
- await 只能在 async 函数中使用:在非 async 函数中使用 await 会导致语法错误。
- async 函数返回 Promise:调用 async 函数时,需要使用 .then() 或 await 来处理返回值。
- 错误处理:必须使用 try-catch 捕获 await 表达式的错误,否则会导致 Promise 被拒绝。
- 并行执行:当多个异步操作之间没有依赖关系时,应使用 Promise.all() 并行执行,以提高性能。
7. 请解释 ES6+ 中的可选链操作符(?.)和空值合并操作符(??)
Details
ES2020 引入了两个新的操作符:可选链操作符(?.)和空值合并操作符(??)。
可选链操作符(?.)
可选链操作符允许我们安全地访问对象的深层属性,而不需要检查每个中间属性是否存在。
基本用法
// 传统方式
const user = {
profile: {
name: 'John'
}
};
// 传统方式:需要检查每个中间属性
const userName = user && user.profile && user.profile.name;
console.log(userName); // 'John'
// 使用可选链操作符
const userName2 = user?.profile?.name;
console.log(userName2); // 'John'
// 如果中间属性不存在,返回 undefined
const userAge = user?.profile?.age;
console.log(userAge); // undefined
// 可选链与函数调用
const user = {
greet: function() {
return 'Hello!';
}
};
// 安全地调用方法
const greeting = user?.greet?.();
console.log(greeting); // 'Hello!'
// 如果方法不存在,返回 undefined
const farewell = user?.farewell?.();
console.log(farewell); // undefined
// 可选链与数组访问
const user = {
hobbies: ['reading', 'coding']
};
// 安全地访问数组元素
const firstHobby = user?.hobbies?.[0];
console.log(firstHobby); // 'reading'
// 如果数组不存在或索引越界,返回 undefined
const thirdHobby = user?.hobbies?.[2];
console.log(thirdHobby); // undefined空值合并操作符(??)
空值合并操作符用于提供默认值,当左侧表达式为 null 或 undefined 时,返回右侧表达式的值。
基本用法
// 传统方式:使用 || 操作符
const name = null || 'Default Name';
console.log(name); // 'Default Name'
// 问题:|| 操作符会将 falsy 值(如 0, '', false)也视为需要使用默认值的情况
const count = 0 || 10;
console.log(count); // 10,这可能不是我们想要的
// 使用空值合并操作符
const name2 = null ?? 'Default Name';
console.log(name2); // 'Default Name'
const count2 = 0 ?? 10;
console.log(count2); // 0,正确保留了 0 值
const emptyString = '' ?? 'Default String';
console.log(emptyString); // '',正确保留了空字符串
const falseValue = false ?? true;
console.log(falseValue); // false,正确保留了 false 值组合使用
可选链操作符和空值合并操作符可以组合使用,以提供更简洁的代码。
const user = {
profile: {
name: 'John'
}
};
// 安全地访问深层属性,并提供默认值
const userName = user?.profile?.name ?? 'Anonymous';
console.log(userName); // 'John'
const userAge = user?.profile?.age ?? 18;
console.log(userAge); // 18
const userEmail = user?.profile?.email ?? 'no-email@example.com';
console.log(userEmail); // 'no-email@example.com'注意事项
- 可选链操作符的返回值:当访问的属性不存在时,可选链操作符返回
undefined,而不是抛出错误。 - 空值合并操作符的操作数:空值合并操作符只对
null和undefined进行判断,对其他 falsy 值(如 0, '', false)会保留原值。 - 优先级:空值合并操作符的优先级低于大多数其他操作符,因此在复杂表达式中可能需要使用括号。
- 兼容性:可选链操作符和空值合并操作符在现代浏览器和 Node.js 14+ 中支持,旧环境需要使用 Babel 等工具进行转译。
8. 请解释 ES6+ 中的 BigInt
Details
BigInt 是 ES2020 引入的一种新的原始数据类型,用于表示任意精度的整数。
基本概念
- BigInt:一种原始数据类型,表示任意精度的整数,可以表示超出 Number 类型范围的整数。
- Number 类型的限制:Number 类型使用 64 位浮点数表示,最大安全整数为
Number.MAX_SAFE_INTEGER(9007199254740991)。 - BigInt 的表示:在数字后面添加
n后缀,或使用BigInt()构造函数。
基本用法
// 使用 n 后缀创建 BigInt
const bigInt1 = 123n;
console.log(typeof bigInt1); // 'bigint'
// 使用 BigInt() 构造函数创建 BigInt
const bigInt2 = BigInt(123);
console.log(bigInt2); // 123n
// 从字符串创建 BigInt
const bigInt3 = BigInt('12345678901234567890');
console.log(bigInt3); // 12345678901234567890n
// 超出 Number 范围的整数
const bigInt4 = 9007199254740991n; // Number.MAX_SAFE_INTEGER
const bigInt5 = 9007199254740992n; // 超出 Number 范围
console.log(bigInt5); // 9007199254740992n运算操作
BigInt 支持大部分与 Number 相同的运算操作,但有一些限制:
// 加法
const sum = 1n + 2n;
console.log(sum); // 3n
// 减法
const difference = 5n - 3n;
console.log(difference); // 2n
// 乘法
const product = 2n * 3n;
console.log(product); // 6n
// 除法(向下取整)
const quotient = 7n / 3n;
console.log(quotient); // 2n
// 取模
const remainder = 7n % 3n;
console.log(remainder); // 1n
// 幂运算
const power = 2n ** 10n;
console.log(power); // 1024n
// 自增/自减
let counter = 1n;
counter++;
console.log(counter); // 2n
counter--;
console.log(counter); // 1n比较操作
// 与 BigInt 比较
console.log(1n < 2n); // true
console.log(2n > 1n); // true
console.log(1n === 1n); // true
// 与 Number 比较(类型不同,=== 会返回 false)
console.log(1n == 1); // true
console.log(1n === 1); // false
// 与 Number 进行大小比较
console.log(1n < 2); // true
console.log(2n > 1); // true注意事项
- 类型转换:BigInt 不能与 Number 直接进行算术运算,需要先转换类型。
// 错误:BigInt 与 Number 不能直接运算
// const result = 1n + 1; // TypeError: Cannot mix BigInt and other types, use explicit conversions
// 正确:先转换类型
const result1 = 1n + BigInt(1); // 2n
const result2 = Number(1n) + 1; // 2不支持的操作:
- BigInt 不能使用一元加号操作符(+)
- BigInt 不能与 Number 一起使用位运算符(除了 >>>)
- BigInt 不能用于 Math 对象的方法
兼容性:BigInt 在现代浏览器和 Node.js 10.4+ 中支持,旧环境需要使用 polyfill。
使用场景:
- 需要处理超出 Number 范围的整数
- 密码学计算
- 高精度金融计算
- 处理大整数 ID
9. 请解释 ES6+ 中的私有字段和方法
Details
ES2022 引入了类的私有字段和方法,允许在类中定义私有成员,只能在类内部访问。
私有字段
私有字段使用 # 前缀定义,只能在类内部访问。
class Person {
// 私有字段
#name;
#age;
constructor(name, age) {
this.#name = name;
this.#age = age;
}
// 公共方法,可以访问私有字段
getName() {
return this.#name;
}
getAge() {
return this.#age;
}
// 私有方法,只能在类内部访问
#validateAge(age) {
return age >= 0 && age <= 120;
}
setAge(age) {
if (this.#validateAge(age)) {
this.#age = age;
} else {
throw new Error('Invalid age');
}
}
}
const person = new Person('John', 30);
// 访问公共方法
console.log(person.getName()); // 'John'
console.log(person.getAge()); // 30
// 设置年龄
person.setAge(31);
console.log(person.getAge()); // 31
// 尝试直接访问私有字段(会报错)
// console.log(person.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class
// 尝试直接调用私有方法(会报错)
// person.#validateAge(30); // SyntaxError: Private field '#validateAge' must be declared in an enclosing class私有方法
私有方法使用 # 前缀定义,只能在类内部调用。
class Calculator {
// 私有方法
#add(a, b) {
return a + b;
}
#subtract(a, b) {
return a - b;
}
// 公共方法,调用私有方法
calculate(operation, a, b) {
switch (operation) {
case 'add':
return this.#add(a, b);
case 'subtract':
return this.#subtract(a, b);
default:
throw new Error('Invalid operation');
}
}
}
const calculator = new Calculator();
// 调用公共方法
console.log(calculator.calculate('add', 1, 2)); // 3
console.log(calculator.calculate('subtract', 5, 3)); // 2
// 尝试直接调用私有方法(会报错)
// console.log(calculator.#add(1, 2)); // SyntaxError: Private field '#add' must be declared in an enclosing class静态私有字段和方法
静态私有字段和方法使用 static # 前缀定义,只能在类的静态方法中访问。
class MathUtils {
// 静态私有字段
static #PI = 3.14159;
// 静态私有方法
static #validateNumber(num) {
return typeof num === 'number' && !isNaN(num);
}
// 静态公共方法,访问静态私有字段和方法
static calculateCircleArea(radius) {
if (this.#validateNumber(radius) && radius > 0) {
return this.#PI * radius * radius;
}
throw new Error('Invalid radius');
}
static calculateCircleCircumference(radius) {
if (this.#validateNumber(radius) && radius > 0) {
return 2 * this.#PI * radius;
}
throw new Error('Invalid radius');
}
}
// 调用静态公共方法
console.log(MathUtils.calculateCircleArea(5)); // 78.53975
console.log(MathUtils.calculateCircleCircumference(5)); // 31.4159
// 尝试直接访问静态私有字段(会报错)
// console.log(MathUtils.#PI); // SyntaxError: Private field '#PI' must be declared in an enclosing class
// 尝试直接调用静态私有方法(会报错)
// console.log(MathUtils.#validateNumber(5)); // SyntaxError: Private field '#validateNumber' must be declared in an enclosing class私有字段和方法的优势
- 封装性:将内部实现细节隐藏,只暴露公共接口,提高代码的可维护性。
- 安全性:防止外部代码修改类的内部状态,避免意外的副作用。
- 代码清晰:明确区分公共接口和私有实现,使代码结构更清晰。
- 避免命名冲突:私有字段和方法不会与外部作用域的变量或方法发生命名冲突。
注意事项
- 语法:私有字段和方法必须使用
#前缀,不能使用private关键字。 - 声明:私有字段必须在类的顶层声明,不能在构造函数或方法中声明。
- 访问:私有字段和方法只能在类内部访问,外部代码无法访问。
- 继承:私有字段和方法不会被继承,子类无法访问父类的私有成员。
- 兼容性:私有字段和方法在现代浏览器和 Node.js 12+ 中支持,旧环境需要使用 Babel 等工具进行转译。
10. 请简述 ES6 之后的主要新特性(ES2016-2023)
Details
ES6(ECMAScript 2015)之后,JavaScript 每年都会发布一个新的版本,引入新的特性和改进。以下是 ES2016 到 2023 的主要新特性:
ES2016(ES7)
- 数组 includes() 方法:检查数组是否包含指定元素。
const array = [1, 2, 3, 4, 5];
console.log(array.includes(3)); // true
console.log(array.includes(6)); // false- 指数操作符()**:用于计算幂。
console.log(2 ** 3); // 8
console.log(3 ** 2); // 9ES2017(ES8)
- async/await:提供了更简洁的方式来处理异步操作。
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error(error);
}
}- Object.values():返回对象所有值的数组。
const obj = { a: 1, b: 2, c: 3 };
console.log(Object.values(obj)); // [1, 2, 3]- Object.entries():返回对象所有键值对的数组。
const obj = { a: 1, b: 2, c: 3 };
console.log(Object.entries(obj)); // [['a', 1], ['b', 2], ['c', 3]]- Object.getOwnPropertyDescriptors():返回对象所有属性的描述符。
const obj = { a: 1 };
console.log(Object.getOwnPropertyDescriptors(obj));
// { a: { value: 1, writable: true, enumerable: true, configurable: true } }- 字符串填充方法:
padStart()和padEnd()。
const str = '5';
console.log(str.padStart(2, '0')); // '05'
console.log(str.padEnd(2, '0')); // '50'- 函数参数列表和调用中的尾逗号:允许函数参数和调用中使用尾逗号。
function foo(a, b, c,) {
// ...
}
foo(1, 2, 3,);ES2018(ES9)
- 异步迭代器和生成器:支持异步迭代。
async function* asyncGenerator() {
yield 1;
yield 2;
yield 3;
}
async function processAsyncGenerator() {
for await (const value of asyncGenerator()) {
console.log(value);
}
}- Promise.finally():无论 Promise 成功或失败,都会执行的回调。
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => console.log('Request completed'));- 对象展开运算符:扩展了对象的展开运算符,支持更多场景。
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3, d: 4 };
console.log(obj2); // { a: 1, b: 2, c: 3, d: 4 }- 正则表达式改进:
- 命名捕获组
- 反向断言
- Unicode 属性转义
ES2019(ES10)
- Array.flat():将嵌套数组扁平化为指定深度。
const nestedArray = [1, [2, [3, [4]]]];
console.log(nestedArray.flat()); // [1, 2, [3, [4]]]
console.log(nestedArray.flat(2)); // [1, 2, 3, [4]]
console.log(nestedArray.flat(Infinity)); // [1, 2, 3, 4]- Array.flatMap():先映射数组,然后扁平化结果。
const array = [1, 2, 3];
console.log(array.flatMap(x => [x, x * 2])); // [1, 2, 2, 4, 3, 6]- String.trimStart() 和 String.trimEnd():去除字符串开头和结尾的空白字符。
const str = ' Hello World ';
console.log(str.trimStart()); // 'Hello World '
console.log(str.trimEnd()); // ' Hello World'- Object.fromEntries():将键值对数组转换为对象。
const entries = [['a', 1], ['b', 2], ['c', 3]];
console.log(Object.fromEntries(entries)); // { a: 1, b: 2, c: 3 }- Symbol.description:获取 Symbol 的描述。
const sym = Symbol('description');
console.log(sym.description); // 'description'- Function.toString():返回函数的完整源代码,包括注释和空格。
ES2020(ES11)
- 可选链操作符(?.):安全地访问对象的深层属性。
const user = { profile: { name: 'John' } };
console.log(user?.profile?.name); // 'John'
console.log(user?.address?.street); // undefined- 空值合并操作符(??):当左侧表达式为
null或undefined时,返回右侧表达式的值。
const name = null ?? 'Default';
console.log(name); // 'Default'
const count = 0 ?? 10;
console.log(count); // 0- BigInt:表示任意精度的整数。
const bigInt = 9007199254740991n;
console.log(bigInt); // 9007199254740991n- 动态导入(import()):在运行时按需加载模块。
async function loadModule() {
const module = await import('./module.js');
module.doSomething();
}- globalThis:全局对象的统一引用。
console.log(globalThis); // 在浏览器中是 window,在 Node.js 中是 globalES2021(ES12)
- 逻辑赋值运算符:
&&=、||=、??=。
// &&=
let a = 1;
a &&= 2;
console.log(a); // 2
// ||=
let b = null;
b ||= 'default';
console.log(b); // 'default'
// ??=
let c = null;
c ??= 'default';
console.log(c); // 'default'- 数字分隔符:使用下划线(_)分隔数字,提高可读性。
const million = 1_000_000;
console.log(million); // 1000000
const pi = 3.141_592_653_59;
console.log(pi); // 3.14159265359- String.replaceAll():替换字符串中所有匹配的子串。
const str = 'Hello World, World!';
console.log(str.replaceAll('World', 'JavaScript')); // 'Hello JavaScript, JavaScript!'- Promise.any():返回第一个成功的 Promise 的结果。
const promise1 = Promise.reject('error1');
const promise2 = Promise.resolve('success');
const promise3 = Promise.reject('error2');
Promise.any([promise1, promise2, promise3])
.then(result => console.log(result)); // 'success'ES2022(ES13)
- 类的私有字段和方法:使用
#前缀定义私有成员。
class Person {
#name;
constructor(name) {
this.#name = name;
}
#privateMethod() {
return 'Private method';
}
publicMethod() {
return this.#privateMethod();
}
}- Top-level await:在模块的顶层使用 await,不需要包裹在 async 函数中。
// module.js
const data = await fetch('https://api.example.com/data').then(res => res.json());
export default data;
// app.js
import data from './module.js';
console.log(data);- Array.at():通过索引访问数组元素,支持负索引。
const array = [1, 2, 3, 4, 5];
console.log(array.at(0)); // 1
console.log(array.at(-1)); // 5- Object.hasOwn():检查对象是否拥有指定的属性。
const obj = { a: 1 };
console.log(Object.hasOwn(obj, 'a')); // true
console.log(Object.hasOwn(obj, 'b')); // false- Error.cause:在创建 Error 对象时,可以指定错误的原因。
try {
// 操作
} catch (error) {
throw new Error('Operation failed', { cause: error });
}RegExp.exec() 的改进:在全局模式下,exec() 方法现在会正确设置 lastIndex。
WebAssembly 扩展:增强了 WebAssembly 与 JavaScript 的互操作性。
ES2023(ES14)
- Array findLast() 和 findLastIndex():从数组末尾开始查找元素。
const array = [1, 2, 3, 4, 5];
console.log(array.findLast(n => n % 2 === 0)); // 4
console.log(array.findLastIndex(n => n % 2 === 0)); // 3Hashbang 语法:支持在脚本文件开头使用
#!/usr/bin/env node作为 shebang。Symbol 作为 WeakMap 键:现在可以使用 Symbol 作为 WeakMap 的键。
Promise.withResolvers():提供一种更简洁的方式来创建和控制 Promise。
function delay(ms) {
const { promise, resolve } = Promise.withResolvers();
setTimeout(resolve, ms);
return promise;
}总结
ES6+ 引入了许多新特性,使 JavaScript 变得更加强大和易用。这些特性包括:
- 更简洁的语法(箭头函数、模板字符串、解构赋值等)
- 更好的异步处理(Promise、async/await)
- 更强大的数据结构(Map、Set、WeakMap、WeakSet)
- 更完善的模块系统
- 更安全的类型处理(BigInt)
- 更优雅的代码组织(类、私有字段和方法)
- 更便捷的操作符(可选链、空值合并、逻辑赋值等)
- 更强大的数组和字符串方法
- 更灵活的错误处理(Error.cause)
- 更现代的语法特性(Top-level await、Hashbang 等)
这些特性大大提高了 JavaScript 的开发效率和代码质量,使它成为一门更加成熟和现代化的编程语言。