重新学习前端之ES6

1 阅读18分钟

ES6

一、let/const 与块级作用域

1. let 与 const 的区别

定义

  • let:声明块级作用域的变量,值可以重新赋值
  • const:声明块级作用域的常量,声明时必须初始化,且不能重新赋值

原理

  • 两者都不会发生变量提升,存在暂时性死区(TDZ)
  • 都绑定在块级作用域 {}
  • const 保证的是变量指向的内存地址不变,对于引用类型,其内部属性仍可修改

示例代码

// let 示例
let a = 10;
a = 20; // 可以重新赋值
console.log(a); // 20

// const 示例
const b = 10;
// b = 20; // TypeError: Assignment to constant variable

// const 引用类型
const obj = { name: 'Alice' };
obj.name = 'Bob'; // 可以修改属性
obj.age = 25;     // 可以添加属性
console.log(obj); // { name: 'Bob', age: 25 }

// obj = {}; // TypeError: 不能重新赋值

常见误区

  • 误区:const 声明的对象完全不能修改
  • 正解:const 只是保证变量指向的内存地址不变,对象的属性、数组的元素仍然可以修改
  • 如果需要完全不可变,应使用 Object.freeze()

2. let/const 与 var 的区别

对比表格

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升有(声明提升到函数顶部)无(存在暂时性死区)无(存在暂时性死区)
重复声明允许不允许不允许
初始化可选,默认 undefined可选,默认 undefined必须初始化
重新赋值允许允许不允许
全局变量属性会成为 window 的属性不会成为 window 的属性不会成为 window 的属性

示例代码

// 1. 作用域差异
if (true) {
  var a = 1;
  let b = 2;
  const c = 3;
}
console.log(a); // 1
// console.log(b); // ReferenceError: b is not defined
// console.log(c); // ReferenceError: c is not defined

// 2. 变量提升差异
console.log(x); // undefined(变量提升)
// console.log(y); // ReferenceError: 暂时性死区
var x = 10;
let y = 20;

// 3. 重复声明
var m = 1;
var m = 2; // 正常
// let n = 1;
// let n = 2; // SyntaxError: Identifier 'n' has already been declared

// 4. 全局变量
var global1 = 'var';
let global2 = 'let';
console.log(window.global1); // 'var'
console.log(window.global2); // undefined

选择策略

  • 优先使用 const,表示值不会被重新赋值
  • 需要重新赋值时使用 let
  • 避免使用 var

3. 什么是块级作用域?

定义: 块级作用域是指由一对大括号 {} 包裹的代码区域,包括 if 语句、for 循环、while 循环、函数体等。在块级作用域内声明的变量(使用 let/const)只在当前块内有效。

为什么需要块级作用域

  1. 避免内层变量覆盖外层变量
// 使用 var 的问题
var tmp = 'global';
function f() {
  console.log(tmp); // undefined(变量提升导致)
  if (true) {
    var tmp = 'local';
  }
}
f();

// 使用 let 解决
let tmp2 = 'global';
function f2() {
  console.log(tmp2); // 'global'
  if (true) {
    let tmp2 = 'local';
    console.log(tmp2); // 'local'
  }
}
f2();
  1. 避免循环变量泄露为全局变量
// 使用 var
for (var i = 0; i < 5; i++) {
  console.log(i);
}
console.log(i); // 5(i 泄露到外部)

// 使用 let
for (let j = 0; j < 5; j++) {
  console.log(j);
}
// console.log(j); // ReferenceError: j is not defined

经典面试题

// 使用 var
var arr1 = [];
for (var i = 0; i < 3; i++) {
  arr1.push(function() { console.log(i); });
}
arr1.forEach(fn => fn()); // 3, 3, 3

// 使用 let
var arr2 = [];
for (let i = 0; i < 3; i++) {
  arr2.push(function() { console.log(i); });
}
arr2.forEach(fn => fn()); // 0, 1, 2

4. 什么是暂时性死区(TDZ)?

定义: 暂时性死区(Temporal Dead Zone, TDZ)是指使用 let/const 声明变量时,从进入作用域到变量声明完成之间的区域。在这个区域内访问变量会抛出 ReferenceError

原理

  • let/const 声明的变量不会被提升
  • JavaScript 引擎在扫描代码时发现 let/const 声明,会在该作用域中创建绑定,但在执行到声明语句之前,变量处于"暂时性死区"
  • 访问 TDZ 中的变量会抛出 ReferenceError

示例代码

// TDZ 示例
console.log(a); // undefined(var 变量提升)
var a = 10;

// console.log(b); // ReferenceError: b is not defined
let b = 20;

// console.log(c); // ReferenceError: c is not defined
const c = 30;

// TDZ 检测
function test() {
  console.log(x); // ReferenceError
  let x = 10;
}
test();

// TDZ 与 typeof
console.log(typeof undeclared); // 'undefined'
// console.log(typeof tdzVar); // ReferenceError(即使使用 typeof)
let tdzVar = 10;

常见误区

  • TDZ 不是代码中的某个位置,而是时间上的概念
  • TDZ 内变量已经存在(已创建绑定),只是不可访问
  • typeof 在 TDZ 内也会抛出错误,这是 ES6 的改变

5. const 本质与 const 变量能否修改

const 本质const 实际上保证的是变量指向的内存地址不可变,而不是值不可变。

基本类型:值直接存储在变量指向的内存地址,所以不可修改

const num = 10;
// num = 20; // TypeError

const str = 'hello';
// str = 'world'; // TypeError

引用类型:变量存储的是引用地址,地址不可变,但地址指向的对象内容可以修改

const obj = { a: 1 };
obj.a = 2;       // 正常
obj.b = 3;       // 正常
// obj = {};      // TypeError

const arr = [1, 2, 3];
arr.push(4);     // 正常
arr[0] = 10;     // 正常
// arr = [];      // TypeError

完全冻结对象

const frozen = Object.freeze({ a: 1, b: 2 });
// frozen.a = 3; // 静默失败(严格模式下抛错)
// frozen.c = 4; // 静默失败
console.log(frozen.a); // 1

二、箭头函数

1. 箭头函数的特点

定义: 箭头函数是 ES6 引入的更简洁的函数写法,使用 => 语法定义。

基本语法

// 传统函数
function add(a, b) {
  return a + b;
}

// 箭头函数
const add = (a, b) => a + b;

// 单个参数可省略括号
const double = x => x * 2;

// 无参数需要空括号
const sayHi = () => console.log('Hi');

// 多行语句需要大括号和 return
const sum = (a, b) => {
  const result = a + b;
  return result;
};

// 返回对象字面量需要加括号
const createObj = (name, age) => ({ name, age });

核心特点

  1. 不绑定 this:this 指向定义时所在的外层作用域
  2. 不绑定 arguments:使用外层函数的 arguments
  3. 不能作为构造函数:不能使用 new 调用
  4. 没有 prototype 属性
  5. 不能使用 yield:不能作为 Generator 函数
  6. 不能换行写 =>

2. 箭头函数与普通函数的区别

对比表格

特性普通函数箭头函数
this 绑定动态绑定(调用时决定)静态绑定(定义时决定,继承外层)
arguments有自己的 arguments 对象没有,使用外层的 arguments
构造函数可以作为构造函数(new)不能作为构造函数
prototype有 prototype 属性没有 prototype 属性
call/apply/bind可以改变 this 指向无法改变 this 指向
语法标准函数语法简洁的 => 语法
yield可用于 Generator不能使用 yield

示例代码

// 1. this 指向差异
const obj = {
  name: 'Alice',
  regularFunc: function() {
    console.log(this.name); // 'Alice'
  },
  arrowFunc: () => {
    console.log(this.name); // undefined(指向全局)
  }
};
obj.regularFunc();
obj.arrowFunc();

// 2. 回调函数中的 this
const person = {
  name: 'Bob',
  greet: function() {
    setTimeout(function() {
      console.log(this.name); // undefined(this 指向 window)
    }, 100);
  }
};
person.greet();

const person2 = {
  name: 'Bob',
  greet: function() {
    setTimeout(() => {
      console.log(this.name); // 'Bob'(继承外层 this)
    }, 100);
  }
};
person2.greet();

// 3. arguments 差异
function normalFn() {
  console.log(arguments); // Arguments 对象
}
normalFn(1, 2, 3);

const arrowFn = () => {
  // console.log(arguments); // ReferenceError
};

// 4. 构造函数
function Person(name) {
  this.name = name;
}
const p = new Person('Alice'); // 正常

const ArrowPerson = (name) => {
  this.name = name;
};
// const p2 = new ArrowPerson('Bob'); // TypeError

不适用场景

  1. 对象方法(this 不指向对象)
  2. 需要动态 this 的回调函数
  3. 构造函数
  4. 原型方法
  5. 需要 arguments 的场景

3. 箭头函数的 this

定义: 箭头函数的 this 是在定义时绑定的,继承自外层最近的作用域的 this,且不能被 callapplybind 改变。

原理

  • 普通函数的 this 是调用时确定的(动态绑定)
  • 箭头函数的 this 是定义时确定的(词法作用域)
  • 箭头函数没有自己的 this 绑定

示例代码

// 全局环境
const fn = () => console.log(this);
fn(); // window(全局 this)

// 对象方法中的箭头函数
const obj = {
  name: 'obj',
  fn: () => console.log(this)
};
obj.fn(); // window(箭头函数定义在全局)

// 嵌套函数中的箭头函数
function outer() {
  console.log('outer this:', this);
  const inner = () => {
    console.log('inner this:', this);
  };
  inner();
}
outer.call({ name: 'test' });
// outer this: { name: 'test' }
// inner this: { name: 'test' }

// call/apply/bind 无法改变箭头函数的 this
const arrow = () => console.log(this);
arrow.call({ name: 'test' }); // 仍然指向外层 this

三、模板字符串

1. 模板字符串的特点与用法

定义: 模板字符串(Template Literals)是 ES6 引入的增强版字符串,使用反引号(`)包裹,支持字符串插值、多行字符串和标签模板。

特点

  1. 支持变量插值:使用 ${} 语法嵌入表达式
  2. 支持多行字符串:保留换行和空格
  3. 支持表达式计算${} 内可以是任意 JavaScript 表达式
  4. 支持标签模板:在模板字符串前加标签函数

示例代码

// 基本用法
const name = 'Alice';
const age = 25;

// 变量插值
const str1 = `My name is ${name}, I am ${age} years old.`;
console.log(str1); // 'My name is Alice, I am 25 years old.'

// 多行字符串
const str2 = `
  <div>
    <h1>${name}</h1>
    <p>Age: ${age}</p>
  </div>
`;
console.log(str2);

// 表达式计算
const a = 10, b = 20;
console.log(`${a} + ${b} = ${a + b}`); // '10 + 20 = 30'

// 嵌套模板字符串
const users = ['Alice', 'Bob'];
const html = `<ul>
  ${users.map(user => `<li>${user}</li>`).join('')}
</ul>`;

// 调用函数
const greet = () => 'Hello';
console.log(`${greet()}, World!`); // 'Hello, World!'

2. 标签模板

定义: 标签模板(Tagged Templates)是模板字符串的高级用法,在模板字符串前加上一个函数名(标签),该函数会接收解析后的字符串片段和表达式值作为参数。

原理: 标签函数接收两个参数:

  1. strings:模板字符串的静态部分(数组)
  2. ...values:模板字符串中的表达式值(剩余参数)

示例代码

// 标签函数
function tag(strings, ...values) {
  console.log(strings); // ['Hello ', ' world ', '']
  console.log(values);  // ['Alice']
  return strings[0] + values[0] + strings[1];
}

const name = 'Alice';
const result = tag`Hello ${name} world!`;
console.log(result); // 'Hello Alice world'

// 实际应用:防止 XSS
function safeHTML(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = i < values.length ? 
      String(values[i]).replace(/</g, '&lt;').replace(/>/g, '&gt;') : '';
    return result + str + value;
  }, '');
}

const userInput = '<script>alert(1)</script>';
const safe = safeHTML`<div>${userInput}</div>`;
console.log(safe); // '<div>&lt;script&gt;alert(1)&lt;/script&gt;</div>'

// 实际应用:国际化
function i18n(strings, ...values) {
  const lang = 'zh';
  // 根据语言翻译
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] || '');
  }, '');
}

3. 模板字符串与普通字符串的区别

对比表格

特性普通字符串模板字符串
引号单引号或双引号反引号(`)
变量插值需要拼接${} 语法
多行字符串需要 \n+直接换行
表达式不支持支持任意表达式
标签模板不支持支持

示例代码

const name = 'Alice';
const age = 25;

// 普通字符串
const str1 = 'My name is ' + name + ', I am ' + age + ' years old.';
const str2 = 'Line 1\nLine 2\nLine 3';

// 模板字符串
const str3 = `My name is ${name}, I am ${age} years old.`;
const str4 = `Line 1
Line 2
Line 3`;

四、解构赋值

1. 数组解构赋值

定义: 解构赋值是 ES6 引入的语法,允许从数组或对象中提取值,按对应位置或属性名赋值给变量。

基本语法

// 基本数组解构
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3

// 跳过某些值
const [x, , z] = [1, 2, 3];
console.log(x, z); // 1 3

// 剩余元素
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest);  // [2, 3, 4, 5]

// 默认值
const [p, q = 10] = [1];
console.log(p, q); // 1 10

// 嵌套解构
const [m, [n, o]] = [1, [2, 3]];
console.log(m, n, o); // 1 2 3

// 变量交换
let i = 1, j = 2;
[i, j] = [j, i];
console.log(i, j); // 2 1

成功条件

  • 等号右边必须是可迭代对象(具有 Iterator 接口)
  • 数组、字符串、Set、Map、NodeList 等可迭代
  • 普通对象不可进行数组解构
// 成功
const [a, b] = 'hi';
console.log(a, b); // 'h', 'i'

const [c, d] = new Set([1, 2]);
console.log(c, d); // 1, 2

// 失败
// const [e, f] = { a: 1, b: 2 }; // TypeError: is not iterable

2. 对象解构赋值

基本语法

// 基本对象解构
const { name, age } = { name: 'Alice', age: 25 };
console.log(name, age); // 'Alice', 25

// 重命名
const { name: userName, age: userAge } = { name: 'Bob', age: 30 };
console.log(userName, userAge); // 'Bob', 30

// 默认值
const { x = 1, y = 2 } = { x: 10 };
console.log(x, y); // 10, 2

// 嵌套解构
const { user: { name, address: { city } } } = {
  user: {
    name: 'Alice',
    address: { city: 'Beijing' }
  }
};
console.log(name, city); // 'Alice', 'Beijing'

// 函数参数解构
function greet({ name, age }) {
  console.log(`Hi ${name}, you are ${age}`);
}
greet({ name: 'Alice', age: 25 });

// 函数参数解构 + 默认值
function createConfig({ theme = 'light', fontSize = 14 } = {}) {
  return { theme, fontSize };
}
console.log(createConfig()); // { theme: 'light', fontSize: 14 }

解构赋值成功条件

  • 等号右边必须能转换为对象
  • 解构的属性名必须在对象中存在(或原型链上)
  • 如果等号右边是 undefinednull,会报错
// 成功
const { toString } = 123; // Number 包装对象
console.log(typeof toString); // 'function'

// 失败
// const { a } = undefined; // TypeError
// const { a } = null;      // TypeError

如何让 var [a, b] = {a:1, b:2} 解构成功: 对象本身不可迭代,需要为其添加 Iterator 接口:

const obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    const keys = Object.keys(this);
    let index = 0;
    return {
      next: () => {
        if (index < keys.length) {
          return { value: this[keys[index++]], done: false };
        }
        return { done: true };
      }
    };
  }
};
const [x, y] = obj;
console.log(x, y); // 1, 2

3. 解构赋值的应用场景

// 1. 交换变量
let x = 1, y = 2;
[x, y] = [y, x];

// 2. 函数多返回值
function getStats() {
  return { min: 1, max: 100, avg: 50 };
}
const { min, max, avg } = getStats();

// 3. 提取 JSON 数据
const json = '{"id": 1, "name": "Alice", "email": "alice@test.com"}';
const { id, name } = JSON.parse(json);

// 4. 函数参数默认值
function ajax({ url, method = 'GET', data = {} } = {}) {
  console.log(url, method, data);
}
ajax({ url: '/api/users' });

// 5. 遍历 Map
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
  console.log(key, value);
}

// 6. 加载模块
const { readFile, writeFile } = require('fs');
import { Component, useState } from 'react';

五、默认参数与剩余参数/扩展运算符

1. 函数默认参数

定义: ES6 允许为函数参数设置默认值,当参数未传入或为 undefined 时使用默认值。

示例代码

// 基本用法
function greet(name = 'Guest', age = 18) {
  console.log(`Hi ${name}, age ${age}`);
}
greet();              // 'Hi Guest, age 18'
greet('Alice');       // 'Hi Alice, age 18'
greet('Bob', 25);     // 'Hi Bob, age 25'
greet(undefined, 30); // 'Hi Guest, age 30'

// 注意:null 不会触发默认值
greet(null, 20); // 'Hi null, age 20'

// 默认值可以是表达式
function createId() {
  return Date.now();
}
function createUser(name, id = createId()) {
  return { name, id };
}

// 默认值与解构结合
function fetch({ url, method = 'GET' } = { url: '/default' }) {
  console.log(url, method);
}
fetch(); // '/default' 'GET'

// 参数作用域
let x = 'global';
function fn(y = x) {
  let x = 'local';
  console.log(y); // 'global'(参数默认值在全局作用域求值)
}
fn();

2. 剩余参数(Rest 参数)

定义: 剩余参数使用 ... 语法,将多余的参数收集到一个数组中,替代 arguments 对象。

示例代码

// 基本用法
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// 与普通参数结合
function mix(a, b, ...rest) {
  console.log(a, b, rest);
}
mix(1, 2, 3, 4, 5); // 1, 2, [3, 4, 5]

// 箭头函数中的 rest
const log = (...args) => console.log(...args);
log('a', 'b', 'c');

// 解构中的 rest
const [first, ...others] = [1, 2, 3, 4];
console.log(first); // 1
console.log(others); // [2, 3, 4]

const { name, ...rest } = { name: 'Alice', age: 25, city: 'Beijing' };
console.log(name); // 'Alice'
console.log(rest); // { age: 25, city: 'Beijing' }

3. Rest 参数与 arguments 的区别

对比表格

特性argumentsRest 参数
类型类数组对象真正的数组
包含内容所有实参只包含未命名的参数
箭头函数不可用可用(使用外层函数的)
数组方法不能直接使用可以直接使用
语法隐式存在显式声明 ...args

示例代码

// arguments 示例
function fn1() {
  console.log(arguments); // Arguments 对象
  console.log(Array.isArray(arguments)); // false
  // arguments.map is not a function
  const arr = Array.from(arguments);
  console.log(arr.map(x => x * 2)); // [2, 4, 6]
}
fn1(1, 2, 3);

// Rest 参数示例
function fn2(...args) {
  console.log(args); // [1, 2, 3]
  console.log(Array.isArray(args)); // true
  console.log(args.map(x => x * 2)); // [2, 4, 6]
}
fn2(1, 2, 3);

// 箭头函数
const arrowFn = (...args) => {
  console.log(args); // [1, 2, 3]
};
arrowFn(1, 2, 3);

4. 扩展运算符(Spread 运算符)

定义: 扩展运算符使用 ... 语法,将数组或对象展开为独立的元素,用于函数调用、数组/对象合并等场景。

示例代码

// 1. 数组展开
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2];
console.log(merged); // [1, 2, 3, 4, 5, 6]

// 2. 函数调用
function add(a, b, c) {
  return a + b + c;
}
const nums = [1, 2, 3];
console.log(add(...nums)); // 6

// 3. 数组拷贝(浅拷贝)
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3]
console.log(copy);     // [1, 2, 3, 4]

// 4. 字符串转数组
const chars = [...'hello'];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']

// 5. 对象展开(ES2018)
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // { a: 1, b: 3, c: 4 }

// 6. NodeList 转数组
const divs = document.querySelectorAll('div');
const divArray = [...divs];

// 7. 与解构结合
const [first, ...rest] = [1, 2, 3, 4];

六、Symbol 与迭代器

1. Symbol 的特点与用途

定义: Symbol 是 ES6 引入的第七种原始数据类型,表示独一无二的值,主要用于创建唯一的对象属性名。

特点

  1. 唯一性:每次调用 Symbol() 都返回不同的值
  2. 隐藏性:不会出现在 for...infor...ofObject.keys()
  3. 不可枚举:但可通过 Object.getOwnPropertySymbols() 获取
  4. 不能 new:Symbol 是原始值,不是对象

示例代码

// 基本用法
const s1 = Symbol('description');
const s2 = Symbol('description');
console.log(s1 === s2); // false(即使描述相同也不同)
console.log(s1.toString()); // 'Symbol(description)'

// 作为对象属性名
const mySymbol = Symbol();
const obj = {
  [mySymbol]: 'value'
};
obj[mySymbol]; // 'value'

// 隐藏属性
const obj2 = {
  [Symbol('hidden')]: 'secret',
  public: 'visible'
};
console.log(Object.keys(obj2)); // ['public']
console.log(Object.getOwnPropertySymbols(obj2)); // [Symbol(hidden)]

// Symbol.for() 共享 Symbol
const shared1 = Symbol.for('app.key');
const shared2 = Symbol.for('app.key');
console.log(shared1 === shared2); // true
console.log(Symbol.keyFor(shared1)); // 'app.key'

// 内置 Symbol
// Symbol.iterator - 定义对象的迭代行为
// Symbol.toPrimitive - 定义对象转原始值的行为
// Symbol.toStringTag - 定义 Object.prototype.toString 的返回值

用途

  1. 定义唯一的对象属性名,避免命名冲突
  2. 实现私有属性(模拟)
  3. 定义对象的迭代行为(Symbol.iterator)
  4. 定制类型转换(Symbol.toPrimitive)
  5. 定制 toString 输出(Symbol.toStringTag)

2. 内置 Symbol 值

// Symbol.iterator - 可迭代协议
const iterable = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          return { value: this.data[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};
for (const item of iterable) {
  console.log(item); // 1, 2, 3
}

// Symbol.toPrimitive - 类型转换
const obj = {
  value: 100,
  [Symbol.toPrimitive](hint) {
    console.log('hint:', hint); // 'number' 或 'string' 或 'default'
    if (hint === 'number') return this.value * 2;
    if (hint === 'string') return `Value: ${this.value}`;
    return this.value;
  }
};
console.log(+obj); // hint: number -> 200
console.log(String(obj)); // hint: string -> 'Value: 100'
console.log(obj == 100); // hint: default -> 100 -> true

// Symbol.toStringTag - 定制 toString
const myObj = {
  [Symbol.toStringTag]: 'MyCustomObject'
};
console.log(Object.prototype.toString.call(myObj)); // '[object MyCustomObject]'

3. 迭代器(Iterator)

定义: 迭代器是一种接口,为不同的数据结构提供统一的访问机制。任何数据结构只要部署了 Iterator 接口(即 Symbol.iterator 方法),就可以被 for...of 遍历。

迭代协议

  • 可迭代协议:对象必须有 Symbol.iterator 方法,返回一个迭代器
  • 迭代器协议:迭代器必须有 next() 方法,返回 { value, done }

示例代码

// 原生可迭代对象
// Array, String, Map, Set, arguments, NodeList, TypedArray

// 手动实现迭代器
const myIterable = {
  items: ['a', 'b', 'c'],
  [Symbol.iterator]() {
    let index = 0;
    const items = this.items;
    return {
      next() {
        if (index < items.length) {
          return { value: items[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const item of myIterable) {
  console.log(item); // 'a', 'b', 'c'
}

// 使用迭代器
const iterator = myIterable[Symbol.iterator]();
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { done: true }

4. for...of 与 for...in 的区别

对比表格

特性for...offor...in
遍历对象可迭代对象(数组、Set、Map、字符串等)所有对象
遍历内容键(属性名/索引)
原型链不遍历原型链属性会遍历原型链上的可枚举属性
自定义对象需要实现 Iterator 接口直接可用

示例代码

const arr = ['a', 'b', 'c'];
arr.custom = 'custom property';

// for...in - 遍历键(包括自定义属性)
for (const key in arr) {
  console.log(key); // '0', '1', '2', 'custom'
}

// for...of - 遍历值(仅可迭代元素)
for (const value of arr) {
  console.log(value); // 'a', 'b', 'c'
}

// 对象默认不可用 for...of
const obj = { a: 1, b: 2 };
// for (const v of obj) {} // TypeError: obj is not iterable

// 让对象可用 for...of
const iterableObj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    const values = Object.values(this);
    let index = 0;
    return {
      next() {
        if (index < values.length) {
          return { value: values[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};
for (const v of iterableObj) {
  console.log(v); // 1, 2
}

5. 自定义迭代器

// 范围迭代器
function range(start, end) {
  return {
    [Symbol.iterator]() {
      let current = start;
      return {
        next() {
          if (current <= end) {
            return { value: current++, done: false };
          }
          return { done: true };
        }
      };
    }
  };
}

for (const num of range(1, 5)) {
  console.log(num); // 1, 2, 3, 4, 5
}

// 斐波那契迭代器(无限)
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2

七、Generator 函数

1. Generator 的特点

定义: Generator 函数是 ES6 提供的异步编程解决方案,使用 function* 声明,内部使用 yield 表达式暂停和恢复执行。

特点

  1. 函数声明带 *function* gen() {}
  2. yield 暂停执行:每次调用 next() 执行到下一个 yield
  3. 返回迭代器对象:调用 Generator 函数返回迭代器,不会立即执行
  4. 可传递值next(value) 可以向 Generator 内部传值
  5. 可抛出错误throw() 方法向内部抛错
  6. 可提前结束return() 方法提前终止

示例代码

// 基本用法
function* generator() {
  console.log('start');
  yield 'first';
  console.log('middle');
  yield 'second';
  console.log('end');
  return 'done';
}

const gen = generator(); // 不执行
console.log(gen.next()); // start -> { value: 'first', done: false }
console.log(gen.next()); // middle -> { value: 'second', done: false }
console.log(gen.next()); // end -> { value: 'done', done: true }

// yield 表达式传值
function* gen2() {
  const a = yield 1;
  console.log('a =', a);
  const b = yield 2;
  console.log('b =', b);
  return a + b;
}

const g = gen2();
console.log(g.next());       // { value: 1, done: false }
console.log(g.next('hello')); // a = hello -> { value: 2, done: false }
console.log(g.next('world')); // b = world -> { value: 'helloworld', done: true }

// yield* 表达式(委托另一个 Generator)
function* genA() {
  yield 1;
  yield 2;
}
function* genB() {
  yield* genA();
  yield 3;
}
console.log([...genB()]); // [1, 2, 3]

2. Generator 与 Iterator 的关系

关系说明

  • Generator 函数是 Iterator 接口的生成器
  • Generator 函数执行后返回一个迭代器对象
  • Generator 是 Iterator 的一种更简洁的实现方式
// Generator 自动实现 Iterator 接口
function* myGenerator() {
  yield 'a';
  yield 'b';
  yield 'c';
}

const gen = myGenerator();
// 可以直接用于 for...of
for (const item of gen) {
  console.log(item);
}

// 也可以使用展开运算符
console.log([...myGenerator()]); // ['a', 'b', 'c']

3. Generator 异步应用

Generator + Promise 实现异步流程控制

// 模拟异步请求
function fetchUser(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, name: 'Alice' }), 1000);
  });
}

function fetchPosts(userId) {
  return new Promise(resolve => {
    setTimeout(() => resolve([{ id: 1, title: 'Post 1' }]), 1000);
  });
}

// Generator 异步控制
function* main() {
  const user = yield fetchUser(1);
  console.log('user:', user);
  const posts = yield fetchPosts(user.id);
  console.log('posts:', posts);
}

// 自动执行器(简化版 co)
function run(generator) {
  const gen = generator();
  function step(nextFn) {
    const result = nextFn();
    if (result.done) return result.value;
    Promise.resolve(result.value)
      .then(value => step(() => gen.next(value)))
      .catch(error => gen.throw(error));
  }
  step(() => gen.next());
}

run(main);

八、Module 模块系统

1. ES6 模块基础

定义: ES6 模块是 JavaScript 官方的模块化方案,使用 export 导出、import 导入,每个模块有独立的作用域。

export 命令

// 命名导出
export const name = 'Alice';
export function greet() { return 'Hello'; }
export class User {}

// 统一导出
const version = '1.0.0';
function log(msg) { console.log(msg); }
export { version, log };

// 重命名导出
export { version as v, log as logger };

import 命令

// 导入单个
import { name } from './module.js';

// 导入多个
import { name, greet } from './module.js';

// 重命名导入
import { name as userName } from './module.js';

// 整体导入
import * as module from './module.js';
console.log(module.name);

// 默认导入
import defaultExport from './module.js';

// 组合导入
import defaultExport, { name, greet } from './module.js';

// 仅执行模块
import './polyfill.js';

2. export 与 export default 的区别

对比表格

特性export(命名导出)export default(默认导出)
数量一个模块可以有多个一个模块只能有一个
导入语法import { name }import name(无大括号)
重命名导入时 { name as newName }导入时可任意命名
变量/函数/类直接导出导出表达式或值
匿名函数/类不支持支持

示例代码

// module.js
export const a = 1;           // 命名导出
export function fn() {}       // 命名导出
export default function() {}  // 默认导出

// main.js
import { a, fn } from './module.js';  // 命名导入
import myFn from './module.js';       // 默认导入

3. ES6 模块与 CommonJS 的区别

对比表格

特性CommonJSES6 Module
语法require() / module.exportsimport / export
加载时机运行时加载(动态)编译时加载(静态)
输出值值的拷贝(浅拷贝)值的引用(实时绑定)
顶层 this指向当前模块指向 undefined
动态导入支持需要 import()
循环依赖可能得到未初始化值更安全(引用方式)
Tree Shaking不支持支持(静态分析)
浏览器支持需要打包原生支持(script type="module")

示例代码

// CommonJS - 值的拷贝
// a.js
let counter = 0;
function add() { counter++; }
module.exports = { counter, add };

// b.js
const { counter, add } = require('./a.js');
add();
console.log(counter); // 0(拷贝的值,不受影响)

// ES6 Module - 值的引用
// a.mjs
export let counter = 0;
export function add() { counter++; }

// b.mjs
import { counter, add } from './a.mjs';
add();
console.log(counter); // 1(实时绑定)

选择策略

  • 浏览器端开发:优先使用 ES6 Module
  • Node.js 项目:根据版本选择(v14+ 支持 ES Module)
  • 需要动态加载:使用 import()require()
  • 需要 Tree Shaking:使用 ES6 Module

4. 动态导入 import()

定义import() 是 ES2020 引入的动态导入语法,返回 Promise,支持按需加载和条件加载。

示例代码

// 条件加载
if (condition) {
  import('./moduleA.js').then(module => {
    module.default();
  });
}

// 按需加载(路由懒加载)
function loadPage(page) {
  return import(`./pages/${page}.js`);
}
loadPage('home').then(module => {
  module.render();
});

// async/await
async function handleClick() {
  const module = await import('./heavyModule.js');
  module.init();
}

// 并行加载
const [moduleA, moduleB] = await Promise.all([
  import('./moduleA.js'),
  import('./moduleB.js')
]);

九、Set、Map 数据结构

1. Set 数据结构

定义: Set 是 ES6 引入的集合数据结构,成员值唯一,不重复。

属性和方法

// 创建
const set1 = new Set();
const set2 = new Set([1, 2, 3, 3, 3]);
console.log(set2); // Set(3) { 1, 2, 3 }

// 基本方法
const set = new Set();
set.add(1);
set.add(2);
set.add(2); // 重复值被忽略
set.add('3');

set.size;           // 3
set.has(1);         // true
set.delete(1);      // true
set.clear();        // 清空

// 遍历方法
const numSet = new Set([1, 2, 3]);
numSet.forEach(val => console.log(val));

for (const val of numSet) {
  console.log(val); // 1, 2, 3
}

// 转换为数组
const arr = [...numSet];
const arr2 = Array.from(numSet);

// 数组去重
const duplicateArr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(duplicateArr)];
console.log(unique); // [1, 2, 3]

// 集合运算
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

// 并集
const union = new Set([...a, ...b]); // { 1, 2, 3, 4 }

// 交集
const intersection = new Set([...a].filter(x => b.has(x))); // { 2, 3 }

// 差集
const difference = new Set([...a].filter(x => !b.has(x))); // { 1 }

2. WeakSet

定义: WeakSet 与 Set 类似,但成员只能是对象,且是弱引用(不阻止垃圾回收)。

与 Set 的区别

特性SetWeakSet
成员类型任意类型只能是对象
引用类型强引用弱引用
size 属性
遍历方法支持不支持
垃圾回收阻止不阻止

示例代码

const ws = new WeakSet();
const obj = {};
const arr = [];

ws.add(obj);
ws.add(arr);
// ws.add(1); // TypeError

console.log(ws.has(obj)); // true

// obj 被垃圾回收后,WeakSet 中的对应引用也会被清除
// 适用于存储对象的状态标记
const visited = new WeakSet();
function processNode(node) {
  if (visited.has(node)) return;
  visited.add(node);
  // 处理节点...
}

3. Map 数据结构

定义: Map 是 ES6 引入的键值对集合,键可以是任意类型(包括对象、函数等)。

属性和方法

// 创建
const map1 = new Map();
const map2 = new Map([
  ['name', 'Alice'],
  ['age', 25]
]);

// 基本方法
const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);
map.set({ key: 'obj' }, 'value');

map.size;              // 3
map.get('name');       // 'Alice'
map.has('age');        // true
map.delete('age');     // true
map.clear();           // 清空

// 遍历方法
const userMap = new Map([
  ['name', 'Alice'],
  ['age', 25],
  ['city', 'Beijing']
]);

for (const [key, value] of userMap) {
  console.log(key, value);
}

userMap.forEach((value, key) => {
  console.log(key, value);
});

// 转换为数组
const entries = [...userMap];
const keys = [...userMap.keys()];
const values = [...userMap.values()];

// 转换为对象
const obj = Object.fromEntries(userMap);
console.log(obj); // { name: 'Alice', age: 25, city: 'Beijing' }

4. Map 与 Object 的区别

对比表格

特性ObjectMap
键类型字符串、Symbol任意类型
键顺序不保证(有特定规则)按插入顺序
size 属性无(需手动计算)
遍历for...inObject.keys()for...offorEach
性能频繁增删较差频繁增删更优
序列化支持 JSON.stringify()不支持
原型链有默认键无默认键

选择策略

  • 键是字符串且需要序列化:使用 Object
  • 键是任意类型或需要频繁增删:使用 Map
  • 需要保持插入顺序:使用 Map

5. WeakMap

定义: WeakMap 与 Map 类似,但键只能是对象,且是弱引用

与 Map 的区别

特性MapWeakMap
键类型任意类型只能是对象
引用类型强引用弱引用
遍历支持不支持
size/clear支持不支持

用途

// 存储私有数据
const privateData = new WeakMap();

class Person {
  constructor(name) {
    privateData.set(this, { name, ssn: '123-45-6789' });
  }
  getName() {
    return privateData.get(this).name;
  }
}

const p = new Person('Alice');
console.log(p.getName()); // 'Alice'

// DOM 元素关联数据
const elementData = new WeakMap();
const div = document.createElement('div');
elementData.set(div, { createdAt: new Date(), id: 1 });

6. WeakRef 与 FinalizationRegistry

// WeakRef - 弱引用对象
const obj = { data: 'important' };
const weakRef = new WeakRef(obj);
console.log(weakRef.deref()); // { data: 'important' }

// FinalizationRegistry - 对象被垃圾回收时的回调
const registry = new FinalizationRegistry(heldValue => {
  console.log(`Object with ${heldValue} was garbage collected`);
});

const target = { name: 'target' };
registry.register(target, 'target-metadata');
// target 被回收时会执行回调

十、Promise

1. Promise 基础

定义: Promise 是 ES6 引入的异步编程解决方案,代表一个异步操作的最终完成或失败。

三种状态

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)

特点

  • 状态一旦改变就不可逆
  • 只能从 pending -> fulfilledpending -> rejected

示例代码

// 基本用法
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功');
    } else {
      reject('操作失败');
    }
  }, 1000);
});

promise
  .then(result => console.log(result))
  .catch(error => console.error(error));

// 链式调用
function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id: 1 }), 500);
  });
}

function fetchUser(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, name: 'Alice' }), 500);
  });
}

fetchData()
  .then(data => fetchUser(data.id))
  .then(user => console.log(user))
  .catch(err => console.error(err));

2. Promise 的方法

// Promise.all - 全部成功才成功,一个失败就失败
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
  .then(results => console.log(results)) // [1, 2, 3]
  .catch(err => console.error(err));

// 一个失败的情况
const pFail = Promise.reject('error');
Promise.all([p1, pFail, p3])
  .catch(err => console.error(err)); // 'error'

// Promise.race - 最先完成的决定结果
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100));
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 500));

Promise.race([fast, slow])
  .then(result => console.log(result)); // 'fast'

// Promise.allSettled - 等待所有完成,返回每个的状态
const promises = [
  Promise.resolve(1),
  Promise.reject('error'),
  Promise.resolve(3)
];

Promise.allSettled(promises).then(results => {
  console.log(results);
  // [
  //   { status: 'fulfilled', value: 1 },
  //   { status: 'rejected', reason: 'error' },
  //   { status: 'fulfilled', value: 3 }
  // ]
});

// Promise.any - 第一个成功的决定结果
Promise.any([
  Promise.reject('err1'),
  Promise.resolve('success'),
  Promise.reject('err2')
]).then(result => console.log(result)); // 'success'

// Promise.resolve / Promise.reject
Promise.resolve('value').then(v => console.log(v));
Promise.reject('error').catch(e => console.error(e));

3. Promise 错误处理

// 1. catch 方法
Promise.reject('error')
  .then(() => console.log('不执行'))
  .catch(err => console.error('捕获错误:', err));

// 2. then 的第二个参数
Promise.reject('error')
  .then(
    result => console.log(result),
    error => console.error('捕获错误:', error)
  );

// 3. finally 方法
let isLoading = true;
fetchData()
  .then(data => console.log(data))
  .catch(err => console.error(err))
  .finally(() => {
    isLoading = false;
    console.log('清理工作');
  });

// 4. 链式错误处理
Promise.resolve(1)
  .then(x => x * 2)
  .then(x => { throw new Error('出错'); })
  .catch(err => {
    console.error(err);
    return 0; // 可以恢复链
  })
  .then(x => console.log(x)); // 0

十一、Class 类

1. Class 的基本语法

定义: ES6 的 class 是构造函数的语法糖,提供更清晰的面向对象编程方式。

示例代码

// 基本类定义
class Person {
  // constructor 方法
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // 实例方法
  greet() {
    return `Hi, I'm ${this.name}`;
  }

  // getter
  get info() {
    return `${this.name}(${this.age})`;
  }

  // setter
  set info(value) {
    const [name, age] = value.split(',');
    this.name = name;
    this.age = parseInt(age);
  }
}

// 创建实例
const p = new Person('Alice', 25);
console.log(p.greet()); // "Hi, I'm Alice"
console.log(p.info);    // "Alice(25)"
p.info = 'Bob,30';
console.log(p.info);    // "Bob(30)"

2. 类的继承

// extends 继承
class Student extends Person {
  constructor(name, age, grade) {
    super(name, age); // 必须调用 super
    this.grade = grade;
  }

  // 重写方法
  greet() {
    return `${super.greet()}, a student`;
  }

  study() {
    return `${this.name} is studying`;
  }
}

const s = new Student('Alice', 18, 'A');
console.log(s.greet()); // "Hi, I'm Alice, a student"
console.log(s.study()); // "Alice is studying"

// super 关键字
class Teacher extends Person {
  constructor(name, age, subject) {
    super(name, age);
    this.subject = subject;
  }

  teach() {
    return `${super.greet()} teaches ${this.subject}`;
  }
}

3. 静态方法、静态属性、私有属性

class MathHelper {
  // 静态属性(ES2022)
  static PI = 3.14159;
  static version = '1.0';

  // 静态方法
  static add(a, b) {
    return a + b;
  }

  static subtract(a, b) {
    return a - b;
  }
}

console.log(MathHelper.add(1, 2)); // 3
console.log(MathHelper.PI);        // 3.14159

// 私有属性(ES2022)
class BankAccount {
  #balance = 0; // 私有属性

  constructor(initial) {
    this.#balance = initial;
  }

  deposit(amount) {
    this.#balance += amount;
    return this;
  }

  withdraw(amount) {
    if (amount > this.#balance) {
      throw new Error('余额不足');
    }
    this.#balance -= amount;
    return this;
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(100);
account.deposit(50).withdraw(30);
console.log(account.getBalance()); // 120
// console.log(account.#balance); // SyntaxError

4. 类与构造函数的区别

对比表格

特性ES5 构造函数ES6 Class
语法函数 + prototypeclass 关键字
提升有变量提升无提升(暂时性死区)
调用可以不使用 new必须使用 new
继承原型链模拟extends + super
静态方法直接挂载到构造函数static 关键字
私有属性闭包模拟# 前缀
可读性较混乱清晰

示例代码

// ES5 构造函数
function PersonES5(name, age) {
  this.name = name;
  this.age = age;
}
PersonES5.prototype.greet = function() {
  return `Hi, I'm ${this.name}`;
};

// ES6 Class
class PersonES6 {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  greet() {
    return `Hi, I'm ${this.name}`;
  }
}

// 本质相同
console.log(typeof PersonES6); // 'function'
console.log(PersonES6 === PersonES6.prototype.constructor); // true

十二、对象扩展

1. Object.is

定义Object.is() 用于比较两个值是否严格相等,修正了 === 的一些行为。

console.log(Object.is(NaN, NaN));       // true(=== 为 false)
console.log(Object.is(+0, -0));         // false(=== 为 true)
console.log(Object.is(1, 1));           // true
console.log(Object.is('foo', 'foo'));   // true
console.log(Object.is(null, null));     // true
console.log(Object.is({}, {}));         // false

2. Object.assign

定义Object.assign() 用于对象的浅拷贝,将源对象的所有可枚举属性复制到目标对象。

// 基本用法
const target = { a: 1 };
const source = { b: 2, c: 3 };
Object.assign(target, source);
console.log(target); // { a: 1, b: 2, c: 3 }

// 合并对象
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = Object.assign({}, obj1, obj2);
console.log(merged); // { a: 1, b: 3, c: 4 }

// 浅拷贝
const original = { a: 1, nested: { b: 2 } };
const copy = Object.assign({}, original);
copy.nested.b = 99;
console.log(original.nested.b); // 99(影响原对象)

// 默认值
function createUser(options) {
  const defaults = { theme: 'light', fontSize: 14 };
  return Object.assign({}, defaults, options);
}
console.log(createUser({ theme: 'dark' }));
// { theme: 'dark', fontSize: 14 }

3. Object.keys / Object.values / Object.entries / Object.fromEntries

const obj = { a: 1, b: 2, c: 3 };

// Object.keys - 获取键数组
console.log(Object.keys(obj)); // ['a', 'b', 'c']

// Object.values - 获取值数组
console.log(Object.values(obj)); // [1, 2, 3]

// Object.entries - 获取键值对数组
console.log(Object.entries(obj)); // [['a', 1], ['b', 2], ['c', 3]]

// 遍历
for (const [key, value] of Object.entries(obj)) {
  console.log(`${key}: ${value}`);
}

// Object.fromEntries - 键值对数组转对象
const entries = [['name', 'Alice'], ['age', 25]];
const fromEntries = Object.fromEntries(entries);
console.log(fromEntries); // { name: 'Alice', age: 25 }

// Map 转 Object
const map = new Map([['x', 1], ['y', 2]]);
const objFromMap = Object.fromEntries(map);
console.log(objFromMap); // { x: 1, y: 2 }

// 过滤对象属性
const filtered = Object.fromEntries(
  Object.entries(obj).filter(([key, value]) => value > 1)
);
console.log(filtered); // { b: 2, c: 3 }

4. Object.getOwnPropertyDescriptors

定义: 获取对象所有自身属性的描述符(包括不可枚举属性)。

const obj = { name: 'Alice' };
Object.defineProperty(obj, 'age', {
  value: 25,
  writable: false,
  enumerable: false,
  configurable: true
});

console.log(Object.getOwnPropertyDescriptors(obj));
// {
//   name: { value: 'Alice', writable: true, enumerable: true, configurable: true },
//   age: { value: 25, writable: false, enumerable: false, configurable: true }
// }

// 用途:正确复制属性描述符
const source = Object.defineProperties({}, {
  foo: { value: 1, enumerable: true },
  bar: { value: 2, enumerable: false }
});

const target = Object.create(
  Object.getPrototypeOf(source),
  Object.getOwnPropertyDescriptors(source)
);

十三、数组扩展

1. Array.from

定义: 将类数组对象或可迭代对象转换为真正的数组。

// 类数组转数组
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const arr = Array.from(arrayLike);
console.log(arr); // ['a', 'b', 'c']

// NodeList 转数组
const divs = document.querySelectorAll('div');
const divArray = Array.from(divs);

// 字符串转数组
console.log(Array.from('hello')); // ['h', 'e', 'l', 'l', 'o']

// Set 转数组
console.log(Array.from(new Set([1, 2, 2, 3]))); // [1, 2, 3]

// 映射函数
console.log(Array.from([1, 2, 3], x => x * 2)); // [2, 4, 6]

// 创建数组
console.log(Array.from({ length: 5 }, (_, i) => i)); // [0, 1, 2, 3, 4]

2. Array.of

定义: 将一组值转换为数组,弥补 Array() 构造函数的行为不一致问题。

console.log(Array.of(1, 2, 3));     // [1, 2, 3]
console.log(Array.of(10));          // [10]
console.log(Array.of(undefined));   // [undefined]

// 对比 Array()
console.log(Array(3));              // [empty × 3]
console.log(Array(1, 2, 3));        // [1, 2, 3]

3. find 与 findIndex

const users = [
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 },
  { id: 3, name: 'Charlie', age: 35 }
];

// find - 找到第一个匹配的元素
const user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: 'Bob', age: 30 }

// findIndex - 找到第一个匹配元素的索引
const index = users.findIndex(u => u.name === 'Charlie');
console.log(index); // 2

// 未找到返回 undefined / -1
console.log(users.find(u => u.id === 99));     // undefined
console.log(users.findIndex(u => u.id === 99)); // -1

4. fill

// 填充数组
const arr1 = new Array(5).fill(0);
console.log(arr1); // [0, 0, 0, 0, 0]

// 部分填充
const arr2 = [1, 2, 3, 4, 5];
arr2.fill('x', 1, 3);
console.log(arr2); // [1, 'x', 'x', 4, 5]

// 注意:引用类型共享同一引用
const arr3 = new Array(3).fill([]);
arr3[0].push(1);
console.log(arr3); // [[1], [1], [1]]

5. includes

const arr = [1, 2, NaN, 4, 5];

console.log(arr.includes(2));    // true
console.log(arr.includes(3));    // false
console.log(arr.includes(NaN));  // true(indexOf 无法检测 NaN)
console.log(arr.includes(2, 2)); // false(从索引 2 开始)

// 对比 indexOf
console.log(arr.indexOf(2));     // 1
console.log(arr.indexOf(NaN));   // -1
console.log(arr.indexOf(2) !== -1); // true

6. flat 与 flatMap

// flat - 扁平化嵌套数组
const nested = [1, [2, [3, [4]]]];

console.log(nested.flat());        // [1, 2, [3, [4]]](默认深度 1)
console.log(nested.flat(2));       // [1, 2, 3, [4]]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4]

// 去除空项
const withEmpty = [1, , 3, , 5];
console.log(withEmpty.flat()); // [1, 3, 5]

// flatMap - map + flat(深度 1)
const sentences = ['Hello World', 'JavaScript ES6'];
const words = sentences.flatMap(s => s.split(' '));
console.log(words); // ['Hello', 'World', 'JavaScript', 'ES6']

7. entries、keys、values

const arr = ['a', 'b', 'c'];

// keys - 索引
for (const index of arr.keys()) {
  console.log(index); // 0, 1, 2
}

// values - 值
for (const value of arr.values()) {
  console.log(value); // 'a', 'b', 'c'
}

// entries - 索引和值
for (const [index, value] of arr.entries()) {
  console.log(index, value); // 0 'a', 1 'b', 2 'c'
}

十四、字符串扩展

1. includes、startsWith、endsWith

const str = 'Hello, World!';

// includes - 是否包含
console.log(str.includes('World'));    // true
console.log(str.includes('world'));    // false
console.log(str.includes('o', 5));     // true(从索引 5 开始)

// startsWith - 是否以...开头
console.log(str.startsWith('Hello'));  // true
console.log(str.startsWith('World', 7)); // true

// endsWith - 是否以...结尾
console.log(str.endsWith('!'));        // true
console.log(str.endsWith('Hello', 5)); // true(前 5 个字符)

2. repeat

console.log('x'.repeat(3));          // 'xxx'
console.log('hello'.repeat(2));      // 'hellohello'
console.log('na'.repeat(0));         // ''
console.log('na'.repeat(2.9));       // 'nana'(向下取整)
// console.log('na'.repeat(-1));     // RangeError
// console.log('na'.repeat(Infinity)); // RangeError

3. padStart、padEnd

// padStart - 头部补全
console.log('5'.padStart(3, '0'));   // '005'
console.log('12345'.padStart(3, '0')); // '12345'

// padEnd - 尾部补全
console.log('5'.padEnd(3, '0'));     // '500'

// 实际应用
// 格式化日期
const month = '3';
console.log(month.padStart(2, '0')); // '03'

// 提示长度
console.log('***'.padStart(10, '*')); // '**********'

4. trimStart、trimEnd

const str = '  hello  ';
console.log(str.trim());       // 'hello'
console.log(str.trimStart());  // 'hello  '
console.log(str.trimEnd());    // '  hello'

5. at

const arr = [1, 2, 3, 4, 5];
const str = 'hello';

// 正向索引
console.log(arr.at(0));  // 1
console.log(str.at(0));  // 'h'

// 负向索引(从末尾开始)
console.log(arr.at(-1)); // 5
console.log(str.at(-1)); // 'o'
console.log(arr.at(-2)); // 4

十五、正则扩展

1. u 修饰符

定义u 修饰符表示 Unicode 模式,正确处理大于 \uFFFF 的 Unicode 字符。

// 正确处理四字节的 Unicode 字符
const str = '\uD83D\uDC2A'; // 🐪

console.log(/^.$/.test(str));       // false(误认为两个字符)
console.log(/^.$/u.test(str));      // true

// 正确匹配码点
console.log(/\u{61}/.test('a'));    // false
console.log(/\u{61}/u.test('a'));   // true

2. y 修饰符(sticky)

定义y 修饰符表示粘连模式,每次匹配都必须从上一次匹配的结束位置开始。

const str = 'aaa_aa_a';

// g 修饰符
const gRegex = /a+/g;
console.log(gRegex.exec(str)); // ['aaa']
console.log(gRegex.exec(str)); // ['aa']

// y 修饰符
const yRegex = /a+/y;
console.log(yRegex.exec(str)); // ['aaa']
console.log(yRegex.exec(str)); // null(下一个字符是 _)

// flags 属性
const regex = /abc/giy;
console.log(regex.flags); // 'giy'

十六、数值扩展

1. 二进制与八进制表示法

// 二进制
console.log(0b1010);  // 10
console.log(0B1010);  // 10

// 八进制
console.log(0o755);   // 493
console.log(0O755);   // 493

// 十六进制
console.log(0xFF);    // 255

2. Number 新增方法

// Number.isFinite - 是否有限
console.log(Number.isFinite(10));      // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite('10'));    // false(不转换类型)

// Number.isNaN - 是否 NaN
console.log(Number.isNaN(NaN));        // true
console.log(Number.isNaN('NaN'));      // false

// Number.isInteger - 是否整数
console.log(Number.isInteger(10));     // true
console.log(Number.isInteger(10.0));   // true
console.log(Number.isInteger(10.1));   // false

// Number.parseInt / Number.parseFloat
console.log(Number.parseInt('12.34')); // 12
console.log(Number.parseFloat('12.34')); // 12.34

// Number.EPSILON - 最小精度差
console.log(Number.EPSILON); // 2.220446049250313e-16
// 用于浮点数比较
function isEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true

3. Math 新增方法

// Math.trunc - 去除小数部分
console.log(Math.trunc(4.9));    // 4
console.log(Math.trunc(-4.9));   // -4

// Math.sign - 判断正负
console.log(Math.sign(5));       // 1
console.log(Math.sign(-5));      // -1
console.log(Math.sign(0));       // 0

// Math.cbrt - 立方根
console.log(Math.cbrt(27));      // 3

// Math.hypot - 平方和的平方根
console.log(Math.hypot(3, 4));   // 5

// 指数运算符
console.log(2 ** 3);             // 8
console.log(2 ** 10);            // 1024

十七、函数扩展

1. 严格模式

// 全局严格模式
'use strict';

// 函数内严格模式
function strictFn() {
  'use strict';
  // x = 1; // ReferenceError
}

// 严格模式的变化
// 1. 变量必须先声明
// 2. 禁止 this 指向全局对象
// 3. 禁止使用 with
// 4. 函数参数不能重名
// 5. 禁止八进制字面量

2. name 属性

function myFunction() {}
console.log(myFunction.name); // 'myFunction'

const anonymous = function() {};
console.log(anonymous.name); // 'anonymous'

const named = function myName() {};
console.log(named.name); // 'myName'

const arrow = () => {};
console.log(arrow.name); // 'arrow'

const bound = myFunction.bind({});
console.log(bound.name); // 'bound myFunction'

3. 尾调用优化

定义: 尾调用是指函数的最后一步是调用另一个函数。尾调用优化是指在尾调用时不增加调用栈,节省内存。

// 尾调用
function f(x) {
  return g(x); // 尾调用
}

// 非尾调用
function f(x) {
  return g(x) + 1; // 不是尾调用
}

// 尾递归
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc); // 尾递归
}

console.log(factorial(5)); // 120

十八、Proxy 与 Reflect

1. Proxy 的用法

定义: Proxy 用于创建一个对象的代理,可以拦截并自定义对象的基本操作(属性读取、赋值、删除等)。

示例代码

// 基本用法
const target = { name: 'Alice', age: 25 };

const proxy = new Proxy(target, {
  get(obj, prop) {
    console.log(`get: ${prop}`);
    return prop in obj ? obj[prop] : 'default';
  },
  set(obj, prop, value) {
    console.log(`set: ${prop} = ${value}`);
    obj[prop] = value;
    return true;
  },
  has(obj, prop) {
    console.log(`has: ${prop}`);
    return prop in obj;
  },
  deleteProperty(obj, prop) {
    console.log(`delete: ${prop}`);
    delete obj[prop];
    return true;
  }
});

console.log(proxy.name);        // 'get: name' -> 'Alice'
console.log(proxy.nonexistent); // 'get: nonexistent' -> 'default'
proxy.age = 30;                 // 'set: age = 30'
console.log('age' in proxy);    // 'has: age' -> true

2. Proxy 的拦截操作

const target = { a: 1 };
const handler = {
  get(target, prop, receiver) {
    console.log(`读取 ${prop}`);
    return target[prop];
  },
  set(target, prop, value, receiver) {
    console.log(`设置 ${prop} = ${value}`);
    target[prop] = value;
    return true;
  },
  has(target, prop) {
    return prop in target;
  },
  deleteProperty(target, prop) {
    delete target[prop];
    return true;
  },
  apply(target, thisArg, args) {
    console.log('函数调用');
    return target.apply(thisArg, args);
  },
  construct(target, args) {
    console.log('构造函数调用');
    return new target(...args);
  }
};

// 可撤销的 Proxy
const { proxy: revProxy, revoke } = Proxy.revocable(target, handler);
console.log(revProxy.a); // 1
revoke();
// console.log(revProxy.a); // TypeError

3. Proxy 的应用场景

// 1. 数据验证
function createValidator(target, validator) {
  return new Proxy(target, {
    set(obj, prop, value) {
      if (validator[prop]) {
        if (!validator[prop](value)) {
          throw new Error(`Invalid value for ${prop}`);
        }
      }
      obj[prop] = value;
      return true;
    }
  });
}

const user = createValidator(
  { name: '', age: 0 },
  {
    name: v => typeof v === 'string' && v.length > 0,
    age: v => Number.isInteger(v) && v > 0
  }
);
user.name = 'Alice';
// user.age = -1; // Error

// 2. 负数索引数组
function createArray(...elements) {
  const handler = {
    get(target, prop) {
      let index = Number(prop);
      if (index < 0) {
        index = target.length + index;
      }
      return target[index];
    }
  };
  const proxy = new Proxy(elements, handler);
  return proxy;
}

const arr = createArray('a', 'b', 'c', 'd');
console.log(arr[-1]); // 'd'
console.log(arr[-2]); // 'c'

// 3. 隐藏私有属性
function hidePrivate(obj) {
  return new Proxy(obj, {
    get(target, prop) {
      if (typeof prop === 'string' && prop.startsWith('_')) {
        return undefined;
      }
      return target[prop];
    },
    has(target, prop) {
      if (typeof prop === 'string' && prop.startsWith('_')) {
        return false;
      }
      return prop in target;
    }
  });
}

const obj = { name: 'Alice', _password: 'secret' };
const safe = hidePrivate(obj);
console.log(safe.name);      // 'Alice'
console.log(safe._password); // undefined
console.log('_password' in safe); // false

4. Reflect

定义: Reflect 是 ES6 引入的新对象,提供了一组与对象操作相关的静态方法,与 Proxy 的拦截方法一一对应。

设计目的

  1. 将 Object 的一些内部方法移植到 Reflect 上
  2. 修改某些 Object 方法的返回结果,使其更合理
  3. 让对象操作变成函数行为
  4. 与 Proxy 方法对应,确保 Proxy 可以调用默认行为
// Reflect 方法示例
const obj = { a: 1, b: 2 };

// Reflect.get
console.log(Reflect.get(obj, 'a')); // 1

// Reflect.set
Reflect.set(obj, 'c', 3);
console.log(obj); // { a: 1, b: 2, c: 3 }

// Reflect.has
console.log(Reflect.has(obj, 'b')); // true

// Reflect.deleteProperty
Reflect.deleteProperty(obj, 'b');
console.log(obj); // { a: 1, c: 3 }

// Reflect.apply
function sum(a, b) { return a + b; }
console.log(Reflect.apply(sum, null, [1, 2])); // 3

// Reflect.construct
function Person(name) { this.name = name; }
const p = Reflect.construct(Person, ['Alice']);
console.log(p.name); // 'Alice'

// Reflect.ownKeys
const obj2 = { a: 1 };
Object.defineProperty(obj2, 'b', { value: 2, enumerable: false });
console.log(Reflect.ownKeys(obj2)); // ['a', 'b']

5. Reflect 与 Proxy 配合

const target = { name: 'Alice' };

const proxy = new Proxy(target, {
  get(target, prop, receiver) {
    console.log(`get: ${prop}`);
    // 使用 Reflect 调用默认行为
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`set: ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
});

console.log(proxy.name); // 'get: name' -> 'Alice'
proxy.name = 'Bob';      // 'set: name = Bob'

十九、综合问题

1. ES6 引入了哪些新特性?

核心特性汇总

类别特性
变量声明letconst
解构数组解构、对象解构
函数箭头函数、默认参数、剩余参数
字符串模板字符串、标签模板
对象属性简写、Object.assign、Object.keys/values/entries
数组Array.from、Array.of、扩展方法
classextendssuper
模块importexport
异步Promise
数据结构SetMapWeakSetWeakMap
迭代Iterator、for...of
生成器Generator 函数
代理Proxy、Reflect
符号Symbol
数值二进制/八进制、Number 扩展方法
正则u/y 修饰符
其他尾调用优化、指数运算符

2. setTimeout、Promise、async/await 的区别

对比表格

特性setTimeoutPromiseasync/await
类型宏任务(MacroTask)微任务(MicroTask)语法糖(基于 Promise)
执行时机事件循环的宏任务队列微任务队列,优先执行同 Promise
语法回调函数.then() 链式await 同步写法
错误处理try-catch 无法捕获.catch()try-catch
可读性回调地狱链式较清晰同步风格最清晰
返回值定时器 IDPromise 对象Promise 对象

执行顺序示例

console.log('1. 同步代码');

setTimeout(() => {
  console.log('2. setTimeout(宏任务)');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('3. Promise.then(微任务)');
  });

async function asyncFn() {
  console.log('4. async 函数同步部分');
  await Promise.resolve();
  console.log('5. await 后的代码(微任务)');
}
asyncFn();

console.log('6. 同步代码结束');

// 执行顺序:
// 1. 同步代码
// 4. async 函数同步部分
// 6. 同步代码结束
// 3. Promise.then(微任务)
// 5. await 后的代码(微任务)
// 2. setTimeout(宏任务)

3. ES5 与 ES6 继承的区别

对比表格

特性ES5 继承ES6 继承
实现方式原型链 + 构造函数借用extends + super
this 创建时机先创建子类 this,再添加父类方法先创建父类 this,再加工
语法复杂、不规范简洁、语义清晰
静态方法继承需手动实现自动继承
可读性较差清晰

示例代码

// ES5 继承(寄生组合式)
function ParentES5(name) {
  this.name = name;
}
ParentES5.prototype.greet = function() {
  return `Hi, I'm ${this.name}`;
};

function ChildES5(name, age) {
  ParentES5.call(this, name); // 借用父类构造函数
  this.age = age;
}
ChildES5.prototype = Object.create(ParentES5.prototype);
ChildES5.prototype.constructor = ChildES5;
ChildES5.prototype.study = function() {
  return `${this.name} is studying`;
};

// ES6 继承
class ParentES6 {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return `Hi, I'm ${this.name}`;
  }
}

class ChildES6 extends ParentES6 {
  constructor(name, age) {
    super(name); // 必须先调用 super
    this.age = age;
  }
  study() {
    return `${this.name} is studying`;
  }
}

4. Array.forEach() 与 Array.map() 的区别

对比表格

特性forEachmap
返回值undefined新数组
用途遍历执行副作用遍历并转换数据
链式调用不支持支持
原数组不修改(除非手动修改)不修改
性能稍快稍慢(创建新数组)

示例代码

const numbers = [1, 2, 3, 4, 5];

// forEach - 执行操作,无返回值
numbers.forEach((num, index) => {
  console.log(`Index ${index}: ${num * 2}`);
});

// map - 转换数据,返回新数组
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// 链式调用
const result = numbers
  .filter(n => n > 2)
  .map(n => n * 10)
  .reduce((sum, n) => sum + n, 0);
console.log(result); // 100

// 选择建议
// 需要返回值 -> 使用 map
// 仅执行操作(如打印、修改外部变量)-> 使用 forEach

5. 模块化的发展历程

发展历程

阶段方案特点
1IIFE(立即执行函数)闭包隔离作用域,全局变量挂载
2AMD(RequireJS)异步加载,浏览器端
3CMD(SeaJS)延迟执行,浏览器端
4CommonJS(Node.js)同步加载,服务端
5UMD兼容 AMD + CommonJS
6ES Module语言标准,编译时加载

示例代码

// IIFE
var myModule = (function() {
  var privateVar = 'secret';
  function privateFn() { return privateVar; }
  return {
    publicFn: function() { return privateFn(); }
  };
})();

// AMD
define(['dep1', 'dep2'], function(dep1, dep2) {
  return { fn: function() {} };
});

// CommonJS
const fs = require('fs');
module.exports = { fn: function() {} };

// ES Module
import fs from 'fs';
export function fn() {}

6. 装饰器模式

定义: 装饰器是一种设计模式,用于在不修改原对象的情况下增强其功能。ES6 中可以使用 Proxy 或高阶函数实现。

高阶函数实现

// 日志装饰器
function withLogging(fn) {
  return function(...args) {
    console.log(`调用 ${fn.name},参数:`, args);
    const result = fn.apply(this, args);
    console.log(`返回:`, result);
    return result;
  };
}

function add(a, b) { return a + b; }
const loggedAdd = withLogging(add);
console.log(loggedAdd(1, 2));
// 调用 add,参数:[1, 2]
// 返回:3
// 3

// 缓存装饰器
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoFib = memoize(fibonacci);
console.log(memoFib(10)); // 55(快速)

7. 新旧版本支持:ES6+ 特性在 IE11 中的兼容性

解决方案

  1. Babel 转译:将 ES6+ 代码转换为 ES5
  2. Polyfill:为不支持的 API 提供垫片(如 Promise、Set、Map)
  3. core-js:现代化的 polyfill 库
  4. 浏览器升级提示
// .browserslistrc
> 1%
last 2 versions
not ie <= 11
// webpack + babel 配置
// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
      corejs: 3,
      targets: {
        ie: '11'
      }
    }]
  ]
};

8. Vue 3 响应式原理基于 Proxy

// 简化版 Vue 3 响应式系统
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      track(target, key); // 收集依赖
      return typeof result === 'object' ? reactive(result) : result;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, key); // 触发更新
      }
      return result;
    }
  });
}

// 依赖收集
const targetMap = new WeakMap();

function track(target, key) {
  // 收集当前活跃的 effect
}

function trigger(target, key) {
  // 执行收集的 effects
}

// 使用
const state = reactive({ count: 0 });
// effect(() => { console.log(state.count); });
state.count++; // 触发更新

9. 微前端架构:Module Federation

定义: Webpack 5 的 Module Federation 允许在运行时动态加载其他应用的模块,实现微前端架构。

// host 应用配置
// webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js'
      }
    })
  ]
};

// 动态加载
import('app1/Button').then(Button => {
  // 使用远程组件
});

// async/await
const { default: Button } = await import('app1/Button');