ES6 (ECMAScript 2015) 详解

文章目录

一、ES6简介

1.1 什么是ES6?

ES6,全称ECMAScript 2015,是ECMAScript标准的第6个版本,于2015年6月发布。ECMAScript是JavaScript语言的规范标准,而JavaScript是ECMAScript的一个实现。

ES6是JavaScript语言的一次重大更新,引入了大量新的语法特性和API,使JavaScript编程变得更加强大和灵活。

1.2 为什么要学习ES6?

  1. 行业标准:ES6已经成为现代JavaScript开发的标准,被广泛应用于前端和Node.js开发。
  2. 提高生产力:ES6提供了许多语法糖和新功能,使代码更简洁、更易读、更易维护。
  3. 现代框架基础:诸如React、Vue、Angular等现代JavaScript框架大量使用ES6特性。
  4. 向后兼容:ES6完全向后兼容,你可以逐步将ES6特性引入现有项目。

1.3 浏览器支持情况

现代浏览器已经支持大部分ES6特性:

  • Chrome 51+
  • Firefox 54+
  • Safari 10+
  • Edge 14+

对于需要支持旧浏览器的项目,可以使用Babel等转译工具将ES6代码转换为ES5代码。

二、let和const关键字

ES6引入了letconst两个新的变量声明关键字,用来解决var关键字的一些问题。

2.1 let关键字

let声明的变量具有块级作用域,只在声明它的代码块内有效。

javascript 复制代码
// 使用var(函数作用域)
function varExample() {
  if (true) {
    var x = 10;
    console.log(x); // 10
  }
  console.log(x); // 10 - x在函数内依然可访问
}

// 使用let(块级作用域)
function letExample() {
  if (true) {
    let y = 20;
    console.log(y); // 20
  }
  // console.log(y); // 报错:y未定义 - y只在if块内可访问
}

varExample();
letExample();

let声明的变量不会被提升(hoisting),必须先声明后使用:

javascript 复制代码
console.log(a); // undefined(var声明被提升,但赋值没有)
var a = 5;

// console.log(b); // 报错:b未定义(let不会被提升)
let b = 5;

在同一块作用域内,不能重复声明同一个变量:

javascript 复制代码
var c = 1;
var c = 2; // 正常,c被重新赋值为2

let d = 1;
// let d = 2; // 报错:d已经被声明过了

let解决了"循环中的闭包"问题:

javascript 复制代码
// 使用var的问题
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出三次 3
  }, 1000);
}

// 使用let解决
for (let j = 0; j < 3; j++) {
  setTimeout(function() {
    console.log(j); // 输出 0, 1, 2
  }, 1000);
}

2.2 const关键字

const用于声明常量,其值一旦设定就不能再更改。

javascript 复制代码
const PI = 3.14159;
// PI = 3.14; // 报错:不能给常量赋值

const也具有块级作用域,与let相似:

javascript 复制代码
if (true) {
  const MAX = 100;
  console.log(MAX); // 100
}
// console.log(MAX); // 报错:MAX未定义

对于对象和数组,const只保证引用不变,内容可以修改:

javascript 复制代码
const person = {
  name: "张三",
  age: 30
};

// 可以修改对象的属性
person.age = 31;
console.log(person.age); // 31

// 但不能重新赋值整个对象
// person = { name: "李四", age: 25 }; // 报错

const numbers = [1, 2, 3];
// 可以修改数组内容
numbers.push(4);
console.log(numbers); // [1, 2, 3, 4]

// 但不能重新赋值整个数组
// numbers = [5, 6, 7]; // 报错

2.3 var、let和const的选择

  • 使用const声明那些不会改变的变量。
  • 使用let声明那些会改变的变量。
  • 避免使用var,除非你有特定的理由需要函数作用域而非块级作用域。

三、箭头函数

箭头函数是一种更简洁的函数语法,使用=>符号定义函数。

3.1 基本语法

javascript 复制代码
// 传统函数
function add(a, b) {
  return a + b;
}

// 箭头函数
const addArrow = (a, b) => {
  return a + b;
};

// 如果函数体只有一条返回语句,可以省略大括号和return
const addSimple = (a, b) => a + b;

console.log(add(2, 3));       // 5
console.log(addArrow(2, 3));  // 5
console.log(addSimple(2, 3)); // 5

对于只有一个参数的函数,可以省略参数的小括号:

javascript 复制代码
// 传统函数
function square(x) {
  return x * x;
}

// 箭头函数
const squareArrow = x => x * x;

console.log(square(4));     // 16
console.log(squareArrow(4)); // 16

对于没有参数的函数,必须保留小括号:

javascript 复制代码
// 传统函数
function sayHello() {
  return "你好!";
}

// 箭头函数
const sayHelloArrow = () => "你好!";

console.log(sayHello());     // 你好!
console.log(sayHelloArrow()); // 你好!

3.2 箭头函数的特点

1. 没有自己的this

箭头函数不会创建自己的this,它会捕获其所在上下文的this值,作为自己的this值。

javascript 复制代码
// 传统函数中的this
const person = {
  name: "张三",
  sayHiTraditional: function() {
    setTimeout(function() {
      console.log("你好,我是" + this.name); // this.name是undefined
    }, 1000);
  },
  sayHiArrow: function() {
    setTimeout(() => {
      console.log("你好,我是" + this.name); // this.name是"张三"
    }, 1000);
  }
};

person.sayHiTraditional(); // 你好,我是undefined
person.sayHiArrow();       // 你好,我是张三

2. 没有arguments对象

箭头函数没有自己的arguments对象,但可以访问外围函数的arguments对象:

javascript 复制代码
function traditionalFunc() {
  const arrowFunc = () => {
    console.log(arguments); // 能访问外围函数的arguments
  };
  arrowFunc();
}

traditionalFunc(1, 2, 3); // Arguments对象 [1, 2, 3]

// 箭头函数内没有自己的arguments
const arrowOutside = () => {
  // console.log(arguments); // 报错:arguments未定义
};

3. 不能用作构造函数

箭头函数不能使用new操作符调用,不能作为构造函数使用:

javascript 复制代码
const Person = (name) => {
  this.name = name;
};

// const person = new Person("张三"); // 报错:Person不是构造函数

4. 没有prototype属性

javascript 复制代码
const arrowFunc = () => {};
console.log(arrowFunc.prototype); // undefined

function normalFunc() {}
console.log(normalFunc.prototype); // {}(一个空对象)

5. 不能用作Generator函数

箭头函数不能使用yield关键字,不能作为Generator函数。

3.3 何时使用箭头函数

适合使用箭头函数的场景:

  • 简短的单行函数
  • 需要this与所在作用域保持一致的场景
  • 回调函数,尤其是在数组方法或Promise链中

不适合使用箭头函数的场景:

  • 对象方法(可能导致this指向问题)
  • 构造函数
  • 需要动态this的场景
  • 需要使用arguments对象的场景
javascript 复制代码
// 适合使用箭头函数
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]

// 不适合使用箭头函数
const person = {
  name: "张三",
  // 不推荐
  sayHi: () => {
    console.log(`你好,我是${this.name}`); // this不指向person
  },
  // 推荐
  sayHello() {
    console.log(`你好,我是${this.name}`); // this指向person
  }
};

四、模板字符串

模板字符串是增强版的字符串,使用反引号(`````)标识,可以包含变量、表达式,以及多行文本。

4.1 基本语法

使用反引号(`````)包裹文本,使用${表达式}插入变量或表达式:

javascript 复制代码
const name = "张三";
const age = 30;

// 传统字符串拼接
const message1 = "我叫" + name + ",今年" + age + "岁。";

// 使用模板字符串
const message2 = `我叫${name},今年${age}岁。`;

console.log(message1); // 我叫张三,今年30岁。
console.log(message2); // 我叫张三,今年30岁。

4.2 多行字符串

模板字符串支持多行文本,无需使用\n转义符:

javascript 复制代码
// 传统多行字符串
const multiLine1 = "第一行\n" +
                  "第二行\n" +
                  "第三行";

// 使用模板字符串
const multiLine2 = `第一行
第二行
第三行`;

console.log(multiLine1);
// 第一行
// 第二行
// 第三行

console.log(multiLine2);
// 第一行
// 第二行
// 第三行

4.3 表达式计算

模板字符串中的${}内可以放置任何有效的JavaScript表达式:

javascript 复制代码
const a = 5;
const b = 10;

console.log(`计算结果: ${a} + ${b} = ${a + b}`);
// 计算结果: 5 + 10 = 15

console.log(`现在是${new Date().getHours()}点钟`);
// 现在是XX点钟(取决于当前时间)

const isLoggedIn = true;
console.log(`用户${isLoggedIn ? '已' : '未'}登录`);
// 用户已登录

4.4 嵌套模板

模板字符串可以嵌套使用:

javascript 复制代码
const people = [
  { name: "张三", age: 20 },
  { name: "李四", age: 25 },
  { name: "王五", age: 30 }
];

const html = `
<ul>
  ${people.map(person => `
    <li>${person.name} - ${person.age}岁</li>
  `).join('')}
</ul>
`;

console.log(html);
/*
<ul>
  
    <li>张三 - 20岁</li>
  
    <li>李四 - 25岁</li>
  
    <li>王五 - 30岁</li>
  
</ul>
*/

4.5 标签模板

模板字符串可以和标签函数(tag function)一起使用,实现更复杂的字符串处理:

javascript 复制代码
function highlight(strings, ...values) {
  let result = '';
  strings.forEach((string, i) => {
    result += string;
    if (i < values.length) {
      result += `<span class="highlight">${values[i]}</span>`;
    }
  });
  return result;
}

const name = "张三";
const age = 30;
const highlightedText = highlight`我叫${name},今年${age}岁。`;

console.log(highlightedText);
// 我叫<span class="highlight">张三</span>,今年<span class="highlight">30</span>岁。

五、解构赋值

解构赋值是一种从数组或对象中提取数据并赋值给变量的简洁方法。

5.1 数组解构

从数组中提取值,按照位置将值赋给变量:

javascript 复制代码
// 传统方式
const numbers = [1, 2, 3];
const a = numbers[0]; // 1
const b = numbers[1]; // 2
const c = numbers[2]; // 3

// 数组解构
const [x, y, z] = numbers;
console.log(x, y, z); // 1 2 3
跳过某些值

可以在解构过程中跳过某些值:

javascript 复制代码
const [a, , c] = [1, 2, 3];
console.log(a, c); // 1 3
剩余元素

可以使用...收集剩余元素:

javascript 复制代码
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest);  // [2, 3, 4, 5]
交换变量

解构赋值可以轻松交换变量值,无需临时变量:

javascript 复制代码
let a = 1;
let b = 2;

// 传统方式
// let temp = a;
// a = b;
// b = temp;

// 使用解构
[a, b] = [b, a];

console.log(a, b); // 2 1
设置默认值

解构时可以设置默认值,防止解构的值为undefined

javascript 复制代码
const [a = 1, b = 2, c = 3] = [10, 20];
console.log(a, b, c); // 10 20 3

5.2 对象解构

从对象中提取属性并赋值给变量:

javascript 复制代码
// 传统方式
const person = { name: '张三', age: 30, city: '北京' };
const name = person.name;
const age = person.age;
const city = person.city;

// 对象解构
const { name: n, age: a, city: c } = person;
console.log(n, a, c); // 张三 30 北京

// 如果变量名与属性名相同,可以简写
const { name, age, city } = person;
console.log(name, age, city); // 张三 30 北京
嵌套对象解构

可以解构嵌套的对象:

javascript 复制代码
const student = {
  name: '张三',
  age: 20,
  scores: {
    math: 95,
    english: 85
  }
};

// 解构嵌套对象
const { name, scores: { math, english } } = student;
console.log(name, math, english); // 张三 95 85
设置默认值

对象解构也可以设置默认值:

javascript 复制代码
const { name = '匿名', age = 0, gender = '未知' } = { name: '张三', age: 30 };
console.log(name, age, gender); // 张三 30 未知

5.3 函数参数解构

可以在函数参数中使用解构:

javascript 复制代码
// 传统方式
function printPerson(person) {
  console.log(`${person.name} 今年 ${person.age} 岁,来自 ${person.city}`);
}

// 使用参数解构
function printPersonDestructured({ name, age, city = '未知城市' }) {
  console.log(`${name} 今年 ${age} 岁,来自 ${city}`);
}

const person = { name: '张三', age: 30 };

printPerson(person); // 张三 今年 30 岁,来自 undefined
printPersonDestructured(person); // 张三 今年 30 岁,来自 未知城市

5.4 实际应用场景

解构赋值在很多场景都非常有用:

javascript 复制代码
// React中的解构使用
function ProfileComponent({ user, isAdmin, onLogout }) {
  // ...
}

// 从API返回结果中提取数据
fetch('/api/user')
  .then(response => response.json())
  .then(({ name, email, isActive }) => {
    console.log(name, email, isActive);
  });

// 在循环中使用解构
const users = [
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' }
];

for (const { id, name } of users) {
  console.log(`ID: ${id}, 姓名: ${name}`);
}

// 结合ES6模块导入
// import { Component, useState, useEffect } from 'react';

六、默认参数

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

6.1 基本语法

javascript 复制代码
// ES5中设置默认值的方式
function greetOld(name) {
  name = name || '访客';
  return `你好,${name}!`;
}

// ES6中的默认参数
function greet(name = '访客') {
  return `你好,${name}!`;
}

console.log(greet());        // 你好,访客!
console.log(greet('张三'));   // 你好,张三!
console.log(greet(undefined)); // 你好,访客!
console.log(greet(null));    // 你好,null!

注意:默认参数只在参数为undefined时生效,传入null时不会使用默认值。

6.2 多个默认参数

可以给多个参数设置默认值:

javascript 复制代码
function createUser(name = '匿名用户', age = 0, isAdmin = false) {
  return {
    name,
    age,
    isAdmin,
    createdAt: new Date()
  };
}

console.log(createUser()); // {name: '匿名用户', age: 0, isAdmin: false, createdAt: ...}
console.log(createUser('张三', 30)); // {name: '张三', age: 30, isAdmin: false, createdAt: ...}
console.log(createUser('管理员', undefined, true)); // {name: '管理员', age: 0, isAdmin: true, createdAt: ...}

6.3 表达式作为默认值

默认参数可以是任何表达式,包括函数调用:

javascript 复制代码
function getDefaultName() {
  return `用户${Math.floor(Math.random() * 1000)}`;
}

function createUser(name = getDefaultName(), registerDate = new Date()) {
  return {
    name,
    registeredAt: registerDate
  };
}

const user1 = createUser();
console.log(user1); // 随机用户名和当前时间

// 等待1秒
setTimeout(() => {
  const user2 = createUser();
  console.log(user2); // 不同的随机用户名和新的时间
}, 1000);

默认参数表达式在每次函数调用时都会重新计算。

6.4 后面的参数引用前面的参数

在默认参数中,可以引用已经声明过的参数:

javascript 复制代码
function calculateTotal(price, taxRate = 0.1, discount = price * 0.05) {
  return price + (price * taxRate) - discount;
}

console.log(calculateTotal(100)); // 100 + (100 * 0.1) - 5 = 105
console.log(calculateTotal(100, 0.2)); // 100 + (100 * 0.2) - 5 = 115
console.log(calculateTotal(100, 0.2, 10)); // 100 + (100 * 0.2) - 10 = 110

6.5 默认参数的暂时性死区

默认参数和letconst一样具有暂时性死区,不能在参数被定义前引用:

javascript 复制代码
// 这会报错
// function error(x = y, y = 1) {
//   return [x, y];
// }

// 这是正确的顺序
function correct(x = 1, y = x) {
  return [x, y];
}

console.log(correct()); // [1, 1]
console.log(correct(2)); // [2, 2]
console.log(correct(2, 3)); // [2, 3]

6.6 与解构赋值结合

默认参数可以与解构赋值结合使用:

javascript 复制代码
function printUserInfo({ name = '匿名', age = 0, email = 'N/A' } = {}) {
  console.log(`姓名:${name}, 年龄:${age}, 邮箱:${email}`);
}

printUserInfo(); // 姓名:匿名, 年龄:0, 邮箱:N/A
printUserInfo({ name: '张三', age: 30 }); // 姓名:张三, 年龄:30, 邮箱:N/A
printUserInfo({ name: '李四', email: '[email protected]' }); // 姓名:李四, 年龄:0, 邮箱:[email protected]

注意{ ... } = {}设置了一个空对象作为参数的默认值,可以防止没有参数传入时解构出错。

七、展开运算符和剩余参数

7.1 展开运算符(Spread Operator)

展开运算符使用三个点(...)可以"展开"数组、对象或字符串。

数组展开
javascript 复制代码
// 连接数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// ES5方式
const combinedOld = arr1.concat(arr2);

// 使用展开运算符
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]

// 在特定位置插入元素
const withItem = [1, 2, ...arr1, 7, ...arr2];
console.log(withItem); // [1, 2, 1, 2, 3, 7, 4, 5, 6]

// 复制数组(浅复制)
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] - 不受影响
console.log(copy);     // [1, 2, 3, 4]
对象展开
javascript 复制代码
const person = { name: '张三', age: 30 };
const job = { title: '工程师', salary: 10000 };

// 合并对象
const employee = { ...person, ...job };
console.log(employee); // {name: '张三', age: 30, title: '工程师', salary: 10000}

// 创建对象副本并修改部分属性
const updatedPerson = { ...person, age: 31, city: '上海' };
console.log(updatedPerson); // {name: '张三', age: 31, city: '上海'}
console.log(person);        // {name: '张三', age: 30} - 原对象不变

// 对象展开的顺序很重要
const withOverride1 = { ...person, name: '李四' };
const withOverride2 = { name: '李四', ...person };
console.log(withOverride1); // {name: '李四', age: 30} - 后面的覆盖前面的
console.log(withOverride2); // {name: '张三', age: 30} - 后面的覆盖前面的
函数调用中的展开
javascript 复制代码
function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];

// ES5方式
console.log(sum.apply(null, numbers)); // 6

// 使用展开运算符
console.log(sum(...numbers)); // 6

// 部分展开
console.log(sum(0, ...numbers.slice(1))); // 0 + 2 + 3 = 5
字符串展开
javascript 复制代码
const str = "Hello";
const chars = [...str];
console.log(chars); // ['H', 'e', 'l', 'l', 'o']

7.2 剩余参数(Rest Parameters)

剩余参数也使用三个点(...),但功能与展开运算符相反,它将多个元素收集为一个数组。

函数参数中的剩余参数
javascript 复制代码
// ES5处理不定数量参数的方式
function sumOld() {
  let sum = 0;
  for (let i = 0; i < arguments.length; i++) {
    sum += arguments[i];
  }
  return sum;
}

// 使用剩余参数
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10, 20));        // 30
console.log(sum());              // 0

// 剩余参数与普通参数结合
function multiply(multiplier, ...numbers) {
  return numbers.map(n => multiplier * n);
}

console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]

剩余参数必须是函数参数列表中的最后一个参数。

解构赋值中的剩余参数

在解构赋值中,剩余参数可以收集其余的数组元素或对象属性:

javascript 复制代码
// 数组解构中的剩余元素
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first);  // 1
console.log(second); // 2
console.log(rest);   // [3, 4, 5]

// 对象解构中的剩余属性
const { name, age, ...otherInfo } = { 
  name: '张三', 
  age: 30, 
  city: '北京', 
  job: '工程师',
  isAdmin: false
};

console.log(name, age); // 张三 30
console.log(otherInfo); // {city: '北京', job: '工程师', isAdmin: false}

7.3 展开运算符与剩余参数的区别

尽管展开运算符和剩余参数使用相同的语法(...),但它们在不同上下文中有不同的用途:

  • 展开运算符:用于展开一个可迭代对象(如数组、对象或字符串)为别的形式
  • 剩余参数:用于将多个参数收集为一个数组
javascript 复制代码
// 展开运算符 - 将数组展开为独立元素
const arr = [1, 2, 3];
console.log(...arr); // 1 2 3

// 剩余参数 - 将独立元素收集为数组
function gather(...elements) {
  return elements;
}
console.log(gather(1, 2, 3)); // [1, 2, 3]

// 组合使用
function process(first, ...rest) {
  console.log(first);
  console.log(rest);
  return [first * 2, ...rest.map(x => x * 3)];
}

const result = process(1, 2, 3, 4);
console.log(result); // [2, 6, 9, 12]

7.4 实际应用场景

javascript 复制代码
// 1. 创建不影响原数据的纯函数
function addItem(array, item) {
  return [...array, item];
}

// 2. 对React组件属性的拷贝和扩展
function Button(props) {
  const { className, ...otherProps } = props;
  const btnClass = `btn ${className || ''}`;
  return <button className={btnClass} {...otherProps} />;
}

// 3. 复制和合并配置对象
const defaultConfig = { theme: 'light', fontSize: 14, cache: true };
const userConfig = { theme: 'dark' };

const finalConfig = { ...defaultConfig, ...userConfig };
// { theme: 'dark', fontSize: 14, cache: true }

// 4. 解构赋值并获取其余属性
function processUser(user) {
  const { id, permissions, ...publicInfo } = user;
  
  // 处理敏感信息...
  
  // 返回可公开的信息
  return publicInfo;
} 

八、类(Class)

ES6引入了类(Class)的概念,提供了一种更清晰、更面向对象的语法来创建对象和处理继承。

8.1 基本语法

javascript 复制代码
// ES5中创建"类"的方式(构造函数)
function PersonOld(name, age) {
  this.name = name;
  this.age = age;
}

PersonOld.prototype.sayHello = function() {
  console.log(`你好,我是${this.name},今年${this.age}岁。`);
};

// ES6中使用类
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  // 实例方法
  sayHello() {
    console.log(`你好,我是${this.name},今年${this.age}岁。`);
  }
}

const person1 = new Person('张三', 30);
person1.sayHello(); // 你好,我是张三,今年30岁。

8.2 类的特性

类的定义方式

类可以通过类声明或类表达式定义:

javascript 复制代码
// 类声明
class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

// 类表达式
const Circle = class {
  constructor(radius) {
    this.radius = radius;
  }
};
严格模式

类内部的代码自动运行在严格模式下,无需显式声明"use strict"

类不会被提升

类声明不会被提升,必须先声明后使用:

javascript 复制代码
// 这会报错
// const p = new Person(); // ReferenceError: Person未定义
// class Person {}

// 这没问题
class Person {}
const p = new Person();

8.3 构造函数

constructor方法是类的特殊方法,用于创建和初始化对象。一个类只能有一个名为constructor的方法:

javascript 复制代码
class Person {
  constructor(name) {
    this.name = name;
    console.log('Person构造函数被调用');
  }
}

const p = new Person('张三'); // "Person构造函数被调用"
console.log(p.name); // "张三"

如果没有显式定义构造函数,会自动添加一个空的构造函数:

javascript 复制代码
class EmptyClass {
  // 自动添加:constructor() {}
}

8.4 实例方法

类中定义的方法成为原型方法,被所有实例共享:

javascript 复制代码
class Calculator {
  add(a, b) {
    return a + b;
  }
  
  subtract(a, b) {
    return a - b;
  }
  
  multiply(a, b) {
    return a * b;
  }
  
  divide(a, b) {
    if (b === 0) throw new Error('除数不能为零');
    return a / b;
  }
}

const calc = new Calculator();
console.log(calc.add(5, 3));      // 8
console.log(calc.subtract(5, 3)); // 2
console.log(calc.multiply(5, 3)); // 15
console.log(calc.divide(6, 3));   // 2

8.5 静态方法

使用static关键字定义静态方法,这些方法属于类本身,而不是类的实例:

javascript 复制代码
class MathHelper {
  static add(a, b) {
    return a + b;
  }
  
  static random(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
  
  static isPrime(n) {
    if (n <= 1) return false;
    if (n <= 3) return true;
    if (n % 2 === 0 || n % 3 === 0) return false;
    
    for (let i = 5; i * i <= n; i += 6) {
      if (n % i === 0 || n % (i + 2) === 0) return false;
    }
    return true;
  }
}

// 调用静态方法
console.log(MathHelper.add(5, 3));    // 8
console.log(MathHelper.random(1, 10)); // 1到10之间的随机数
console.log(MathHelper.isPrime(7));    // true
console.log(MathHelper.isPrime(8));    // false

// 不能通过实例调用静态方法
// const helper = new MathHelper();
// console.log(helper.add(5, 3)); // TypeError: helper.add不是一个函数

8.6 访问器属性(Getters和Setters)

类可以包含getters和setters,用于拦截属性的访问和设置:

javascript 复制代码
class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }
  
  // firstName的getter
  get firstName() {
    return this._firstName;
  }
  
  // firstName的setter
  set firstName(value) {
    if (typeof value !== 'string') {
      throw new Error('firstName必须是字符串');
    }
    this._firstName = value;
  }
  
  // lastName的getter
  get lastName() {
    return this._lastName;
  }
  
  // lastName的setter
  set lastName(value) {
    if (typeof value !== 'string') {
      throw new Error('lastName必须是字符串');
    }
    this._lastName = value;
  }
  
  // 只读属性
  get fullName() {
    return `${this._firstName} ${this._lastName}`;
  }
}

const person = new Person('张', '三');
console.log(person.firstName); // "张"
console.log(person.lastName);  // "三"
console.log(person.fullName);  // "张 三"

person.firstName = '李';
person.lastName = '四';
console.log(person.fullName);  // "李 四"

// 尝试设置只读属性
// person.fullName = '王五'; // 没有效果,不会报错

// 尝试设置非字符串值
// person.firstName = 123; // Error: firstName必须是字符串

8.7 类的继承

使用extends关键字实现类的继承:

javascript 复制代码
// 父类
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name}发出了声音`);
  }
}

// 子类
class Dog extends Animal {
  constructor(name, breed) {
    // 调用父类构造函数
    super(name);
    this.breed = breed;
  }
  
  // 覆盖父类的方法
  speak() {
    console.log(`${this.name}汪汪叫!`);
  }
  
  // 子类特有的方法
  fetch() {
    console.log(`${this.name}去捡球了`);
  }
}

// 创建实例
const animal = new Animal('动物');
animal.speak(); // "动物发出了声音"

const dog = new Dog('小黑', '拉布拉多');
dog.speak(); // "小黑汪汪叫!"
dog.fetch(); // "小黑去捡球了"
console.log(dog.name);  // "小黑"
console.log(dog.breed); // "拉布拉多"
super关键字

super关键字有两种用法:

  1. 作为函数调用:super(...args)调用父类的构造函数
  2. 作为对象:super.method(...args)调用父类的方法
javascript 复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    return `${this.name}发出了声音`;
  }
}

class Cat extends Animal {
  constructor(name, color) {
    // 必须先调用super,再使用this
    super(name);
    this.color = color;
  }
  
  speak() {
    // 调用父类的speak方法
    const result = super.speak();
    return `${result},是喵喵喵!`;
  }
  
  describe() {
    return `这是一只${this.color}色的猫,名叫${this.name}`;
  }
}

const cat = new Cat('咪咪', '橘');
console.log(cat.speak());    // "咪咪发出了声音,是喵喵喵!"
console.log(cat.describe()); // "这是一只橘色的猫,名叫咪咪"

在子类构造函数中,必须先调用super(),再使用this

javascript 复制代码
class Parent {
  constructor() {
    this.type = 'parent';
  }
}

class CorrectChild extends Parent {
  constructor() {
    super(); // 正确:先调用super()
    this.name = 'child';
  }
}

class WrongChild extends Parent {
  constructor() {
    this.name = 'child'; // 错误:使用this前未调用super()
    super();
  }
}

const correct = new CorrectChild(); // 正常
// const wrong = new WrongChild(); // ReferenceError: 在调用super()之前不能使用this
继承内置类

可以扩展JavaScript的内置类,如Array、String等:

javascript 复制代码
// 扩展Array类
class MyArray extends Array {
  // 添加求和方法
  sum() {
    return this.reduce((acc, val) => acc + val, 0);
  }
  
  // 添加去重方法
  unique() {
    return [...new Set(this)];
  }
}

const arr = new MyArray(1, 2, 2, 3, 3, 4);
console.log(arr.length);    // 6
console.log(arr.sum());     // 15
console.log(arr.unique());  // [1, 2, 3, 4]

// 继承的数组方法也能正常使用
console.log(arr.map(x => x * 2)); // MyArray [2, 4, 4, 6, 6, 8]
console.log(arr.filter(x => x > 2)); // MyArray [3, 3, 4]

8.8 类表达式和匿名类

类表达式可以命名或不命名(匿名):

javascript 复制代码
// 命名类表达式
const Animal = class AnimalClass {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name}发出了声音`);
  }
};

// AnimalClass只在类内部可见
// console.log(AnimalClass); // ReferenceError: AnimalClass未定义

const a = new Animal('动物');
a.speak(); // "动物发出了声音"

// 匿名类表达式
const Person = class {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    console.log(`你好,我是${this.name}`);
  }
};

const p = new Person('张三');
p.sayHello(); // "你好,我是张三"

8.9 私有字段和方法

现代JavaScript(ES2020+)支持使用#前缀定义私有字段和方法,只能在类内部访问:

javascript 复制代码
class BankAccount {
  // 私有字段
  #balance = 0;
  #accountNumber;
  
  constructor(accountNumber, initialBalance) {
    this.owner = 'Unknown'; // 公有字段
    this.#accountNumber = accountNumber;
    if (initialBalance > 0) {
      this.#deposit(initialBalance);
    }
  }
  
  // 私有方法
  #deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`存入: ${amount}`);
    }
  }
  
  // 私有方法
  #withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`取出: ${amount}`);
      return true;
    }
    return false;
  }
  
  // 公有方法
  deposit(amount) {
    this.#deposit(amount);
  }
  
  // 公有方法
  withdraw(amount) {
    if (!this.#withdraw(amount)) {
      console.log('取款失败:余额不足');
      return false;
    }
    return true;
  }
  
  // 公有方法
  getBalance() {
    return this.#balance;
  }
  
  // 公有方法
  getAccountInfo() {
    return {
      owner: this.owner,
      // 只显示账号的最后4位
      accountNumber: '******' + this.#accountNumber.slice(-4),
      balance: this.#balance
    };
  }
}

const account = new BankAccount('1234567890', 1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.getAccountInfo()); // {owner: 'Unknown', accountNumber: '******7890', balance: 1300}

// 无法直接访问私有字段和方法
// console.log(account.#balance); // SyntaxError: 私有字段不能在类外部访问
// account.#deposit(100); // SyntaxError: 私有方法不能在类外部访问

九、模块(Modules)

ES6引入了官方的模块系统,使用importexport语句来共享代码。

9.1 导出(Export)

可以导出变量、函数、类等:

javascript 复制代码
// utils.js

// 命名导出
export const PI = 3.14159;
export const DAYS_OF_WEEK = 7;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

export class Calculator {
  add(a, b) {
    return a + b;
  }
  
  subtract(a, b) {
    return a - b;
  }
}

// 另一种命名导出的语法
const square = x => x * x;
const cube = x => x * x * x;
const API_URL = 'https://api.example.com';

export { square, cube, API_URL };

// 使用别名导出
const sum = (a, b) => a + b;
export { sum as addNumbers };
默认导出

一个模块只能有一个默认导出:

javascript 复制代码
// person.js

// 默认导出一个类
export default class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    return `你好,我是${this.name}`;
  }
}

// 或者导出函数
// export default function greet(name) {
//   return `你好,${name}!`;
// }

// 或者导出对象
// export default {
//   name: 'DefaultExport',
//   value: 123
// };

9.2 导入(Import)

可以导入其他模块导出的内容:

javascript 复制代码
// 导入命名导出
import { PI, add, multiply, Calculator } from './utils.js';

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

const calc = new Calculator();
console.log(calc.subtract(10, 5)); // 5

// 使用别名导入
import { PI as piValue, add as sum } from './utils.js';
console.log(piValue); // 3.14159
console.log(sum(2, 3)); // 5

// 导入默认导出
import Person from './person.js';

const person = new Person('张三');
console.log(person.sayHello()); // "你好,我是张三"

// 同时导入默认导出和命名导出
import DefaultExport, { namedExport1, namedExport2 } from './module.js';

// 导入所有导出内容
import * as Utils from './utils.js';

console.log(Utils.PI); // 3.14159
console.log(Utils.add(2, 3)); // 5
console.log(Utils.square(4)); // 16
console.log(Utils.API_URL); // "https://api.example.com"

9.3 动态导入

使用import()函数可以动态导入模块:

javascript 复制代码
// 静态导入(必须在文件顶层)
import { add } from './math.js';

// 动态导入(可以在任何地方)
async function loadModule() {
  try {
    const math = await import('./math.js');
    console.log(math.add(2, 3)); // 5
    
    // 导入默认导出
    const personModule = await import('./person.js');
    const Person = personModule.default;
    const person = new Person('张三');
    console.log(person.sayHello());
  } catch (error) {
    console.error('模块加载失败', error);
  }
}

loadModule();

// 基于条件动态导入
async function conditionalImport(condition) {
  if (condition) {
    const module1 = await import('./module1.js');
    return module1;
  } else {
    const module2 = await import('./module2.js');
    return module2;
  }
}

9.4 模块特性

  • 模块代码自动运行在严格模式下
  • 模块有自己的作用域,不会污染全局作用域
  • 模块只被执行一次,即使被多次导入
  • 模块导入是静态的,发生在运行前
  • 顶级thisundefined,而不是window
  • 可以使用聚合模块(汇总多个模块的导出)
javascript 复制代码
// aggregator.js
export { foo, bar } from './module1.js';
export { baz, qux } from './module2.js';
export { default } from './module3.js';

// 导入聚合模块
import { foo, bar, baz, qux } from './aggregator.js';

9.5 在浏览器中使用模块

在HTML中使用type="module"属性在浏览器中使用ES模块:

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <title>ES模块示例</title>
</head>
<body>
  <script type="module">
    import { add, multiply } from './utils.js';
    
    console.log(add(2, 3)); // 5
    console.log(multiply(2, 3)); // 6
  </script>
  
  <script type="module" src="app.js"></script>
</body>
</html>

浏览器中的模块具有以下特点:

  • 自动使用严格模式
  • 每个模块有自己的作用域
  • 加载模块时使用CORS规则
  • 模块被延迟执行(类似defer属性)

十、Promise

Promise是ES6引入的一种处理异步操作的对象,提供了比回调函数更优雅的方式来处理异步结果。

10.1 Promise基础

Promise是一个代表异步操作最终完成或失败的对象,它有三种状态:

  • pending(进行中):初始状态,既不是成功也不是失败
  • fulfilled(已完成):操作成功完成
  • rejected(已拒绝):操作失败
javascript 复制代码
// 创建一个Promise
const promise = new Promise((resolve, reject) => {
  // 异步操作
  const success = true;
  
  if (success) {
    // 操作成功,调用resolve
    resolve('操作成功');
  } else {
    // 操作失败,调用reject
    reject(new Error('操作失败'));
  }
});

// 处理Promise结果
promise
  .then(result => {
    console.log('成功:', result); // "成功: 操作成功"
  })
  .catch(error => {
    console.error('失败:', error);
  });

10.2 Promise链

Promise的一个强大特性是可以链式调用,用于顺序执行异步操作:

javascript 复制代码
function step1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('步骤1完成');
      resolve(100);
    }, 1000);
  });
}

function step2(value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('步骤2完成');
      resolve(value + 200);
    }, 1000);
  });
}

function step3(value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('步骤3完成');
      resolve(value + 300);
    }, 1000);
  });
}

// 链式调用Promise
step1()
  .then(step2)
  .then(step3)
  .then(finalValue => {
    console.log('最终结果:', finalValue); // "最终结果: 600"
  })
  .catch(error => {
    console.error('过程中出错:', error);
  });

// 输出:
// 步骤1完成(1秒后)
// 步骤2完成(再过1秒)
// 步骤3完成(再过1秒)
// 最终结果: 600

每个.then()返回一个新的Promise,这使得Promise可以链式调用。

10.3 Promise方法

Promise.all()

等待所有Promise都完成(或第一个失败):

javascript 复制代码
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, 'foo');
});
const promise3 = Promise.resolve(42);

Promise.all([promise1, promise2, promise3])
  .then(values => {
    console.log(values); // [3, "foo", 42]
  })
  .catch(error => {
    console.error('至少一个Promise失败:', error);
  });

// 如果其中一个Promise失败
const p1 = Promise.resolve('成功');
const p2 = Promise.reject(new Error('失败'));
const p3 = new Promise(resolve => setTimeout(resolve, 1000, 'also成功'));

Promise.all([p1, p2, p3])
  .then(values => {
    console.log(values); // 不会被调用
  })
  .catch(error => {
    console.error('至少一个Promise失败:', error); // "至少一个Promise失败: Error: 失败"
  });
Promise.race()

返回第一个完成(或失败)的Promise的结果:

javascript 复制代码
const p1 = new Promise(resolve => setTimeout(resolve, 500, '慢'));
const p2 = new Promise(resolve => setTimeout(resolve, 100, '快'));
const p3 = new Promise((resolve, reject) => setTimeout(reject, 300, new Error('失败')));

Promise.race([p1, p2, p3])
  .then(value => {
    console.log('最快的结果:', value); // "最快的结果: 快"
  })
  .catch(error => {
    console.error('最快的失败:', error);
  });

// 如果最快的Promise失败
Promise.race([p3, p1])
  .then(value => {
    console.log('最快的结果:', value); // 不会被调用
  })
  .catch(error => {
    console.error('最快的失败:', error); // "最快的失败: Error: 失败"
  });
Promise.allSettled()

等待所有Promise都完成(无论成功或失败):

javascript 复制代码
const p1 = Promise.resolve('成功1');
const p2 = Promise.reject('失败');
const p3 = Promise.resolve('成功2');

Promise.allSettled([p1, p2, p3])
  .then(results => {
    console.log(results);
    /*
    [
      { status: "fulfilled", value: "成功1" },
      { status: "rejected", reason: "失败" },
      { status: "fulfilled", value: "成功2" }
    ]
    */
  });
Promise.any()

返回第一个成功的Promise的结果,如果所有Promise都失败,则返回AggregateError:

javascript 复制代码
const p1 = Promise.reject('失败1');
const p2 = new Promise(resolve => setTimeout(resolve, 500, '成功2'));
const p3 = new Promise(resolve => setTimeout(resolve, 100, '成功3'));

Promise.any([p1, p2, p3])
  .then(value => {
    console.log('第一个成功的结果:', value); // "第一个成功的结果: 成功3"
  })
  .catch(error => {
    console.error('所有Promise都失败:', error);
  });

// 如果所有Promise都失败
Promise.any([
  Promise.reject('失败1'),
  Promise.reject('失败2'),
  Promise.reject('失败3')
])
  .then(value => {
    console.log('第一个成功的结果:', value); // 不会被调用
  })
  .catch(error => {
    console.error('所有Promise都失败:', error); // AggregateError: All promises were rejected
    console.error('失败原因:', error.errors); // ["失败1", "失败2", "失败3"]
  });
Promise.resolve()和Promise.reject()

创建已解决或已拒绝的Promise:

javascript 复制代码
// 创建已解决的Promise
const resolved = Promise.resolve('已解决');
resolved.then(value => console.log(value)); // "已解决"

// 创建已拒绝的Promise
const rejected = Promise.reject(new Error('已拒绝'));
rejected.catch(error => console.error(error.message)); // "已拒绝"

10.4 实际应用场景

javascript 复制代码
// 封装fetch API
function fetchJSON(url) {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP错误! 状态: ${response.status}`);
      }
      return response.json();
    });
}

// 使用封装的函数
fetchJSON('https://api.example.com/data')
  .then(data => {
    console.log('获取的数据:', data);
  })
  .catch(error => {
    console.error('获取数据失败:', error);
  });

// 并行加载多个资源
const urls = [
  'https://api.example.com/users',
  'https://api.example.com/products',
  'https://api.example.com/orders'
];

Promise.all(urls.map(url => fetchJSON(url)))
  .then(([users, products, orders]) => {
    console.log('用户:', users);
    console.log('产品:', products);
    console.log('订单:', orders);
  })
  .catch(error => {
    console.error('加载资源失败:', error);
  });

// 超时处理
function timeoutPromise(promise, timeout) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('操作超时')), timeout)
    )
  ]);
}

timeoutPromise(fetchJSON('https://api.example.com/data'), 5000)
  .then(data => console.log('数据:', data))
  .catch(error => console.error('错误:', error.message));

10.5 async/await

ES2017引入了asyncawait关键字,它们构建在Promise之上,提供了更简洁的异步编程方式。

async函数始终返回一个Promise,而await只能在async函数内部使用:

javascript 复制代码
// 使用Promise
function fetchUserData(userId) {
  return fetch(`https://api.example.com/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP错误! 状态: ${response.status}`);
      }
      return response.json();
    });
}

function displayUserInfo(userId) {
  fetchUserData(userId)
    .then(user => {
      console.log(`用户: ${user.name}, 邮箱: ${user.email}`);
    })
    .catch(error => {
      console.error('获取用户信息失败:', error);
    });
}

// 使用async/await
async function fetchUserData(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  
  if (!response.ok) {
    throw new Error(`HTTP错误! 状态: ${response.status}`);
  }
  
  return await response.json();
}

async function displayUserInfo(userId) {
  try {
    const user = await fetchUserData(userId);
    console.log(`用户: ${user.name}, 邮箱: ${user.email}`);
  } catch (error) {
    console.error('获取用户信息失败:', error);
  }
}

// 调用异步函数
displayUserInfo(123);

async/await的优势:

  • 代码看起来更像同步代码,更易读
  • 错误处理使用标准的try/catch结构
  • 可以轻松处理Promise链中的错误
  • 调试更容易
并行处理异步操作
javascript 复制代码
// 串行处理 - 每个操作等待前一个完成
async function serialProcessing() {
  console.time('串行');
  
  const result1 = await fetchJSON('https://api.example.com/data1');
  const result2 = await fetchJSON('https://api.example.com/data2');
  const result3 = await fetchJSON('https://api.example.com/data3');
  
  console.timeEnd('串行');
  return [result1, result2, result3];
}

// 并行处理 - 所有操作同时开始
async function parallelProcessing() {
  console.time('并行');
  
  const results = await Promise.all([
    fetchJSON('https://api.example.com/data1'),
    fetchJSON('https://api.example.com/data2'),
    fetchJSON('https://api.example.com/data3')
  ]);
  
  console.timeEnd('并行');
  return results;
}

十一、Map和Set

11.1 Map

Map是ES6引入的键值对集合,与对象不同,Map的键可以是任何类型的值(包括对象、函数等)。

创建和使用Map
javascript 复制代码
// 创建一个空Map
const map1 = new Map();

// 使用数组创建Map
const map2 = new Map([
  ['key1', 'value1'],
  ['key2', 'value2'],
  [42, 'answer']
]);

// 添加键值对
map1.set('name', '张三');
map1.set(42, '数字键');
map1.set(true, '布尔键');

// 使用对象作为键
const user = { id: 1 };
map1.set(user, '用户对象');

// 获取值
console.log(map1.get('name')); // "张三"
console.log(map1.get(42)); // "数字键"
console.log(map1.get(user)); // "用户对象"
console.log(map1.get('不存在')); // undefined

// 检查键是否存在
console.log(map1.has('name')); // true
console.log(map1.has('age')); // false

// 获取Map大小
console.log(map1.size); // 4

// 删除键值对
map1.delete(42);
console.log(map1.size); // 3

// 清空Map
map1.clear();
console.log(map1.size); // 0
Map迭代

Map保持键的插入顺序,可以按照添加顺序进行迭代:

javascript 复制代码
const map = new Map([
  ['apple', '🍎'],
  ['banana', '🍌'],
  ['orange', '🍊']
]);

// 使用forEach
map.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});
// apple: 🍎
// banana: 🍌
// orange: 🍊

// 迭代所有键值对
for (const [key, value] of map) {
  console.log(`${key}: ${value}`);
}
// apple: 🍎
// banana: 🍌
// orange: 🍊

// 只迭代键
for (const key of map.keys()) {
  console.log(key);
}
// apple
// banana
// orange

// 只迭代值
for (const value of map.values()) {
  console.log(value);
}
// 🍎
// 🍌
// 🍊

// 获取键值对数组
console.log([...map]); // [["apple", "🍎"], ["banana", "🍌"], ["orange", "🍊"]]
console.log([...map.keys()]); // ["apple", "banana", "orange"]
console.log([...map.values()]); // ["🍎", "🍌", "🍊"]
Map与对象的比较
javascript 复制代码
// 对象只能使用字符串或Symbol作为键
const obj = {
  name: '张三',
  42: '被转为字符串的数字', // 键会被转为字符串 "42"
  true: '被转为字符串的布尔值' // 键会被转为字符串 "true"
};

console.log(obj['42']); // "被转为字符串的数字"
console.log(obj['true']); // "被转为字符串的布尔值"

// 而Map可以使用任何值作为键
const map = new Map();
map.set('name', '张三');
map.set(42, '数字键');
map.set(true, '布尔键');

// 对象属性的默认迭代顺序不可靠
// Map的迭代顺序就是插入顺序

// 获取对象大小需要额外计算
console.log(Object.keys(obj).length); // 3

// Map有内置的size属性
console.log(map.size); // 3
Map应用场景
javascript 复制代码
// 1. 缓存函数结果
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;
  };
}

// 使用缓存的斐波那契函数
const fibonacci = memoize(n => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.time('fib40');
console.log(fibonacci(40)); // 102334155
console.timeEnd('fib40'); // 非常快,因为中间结果被缓存了

// 2. 用户数据存储
const userRoles = new Map();

function addUser(user) {
  userRoles.set(user, []);
}

function addRole(user, role) {
  if (!userRoles.has(user)) {
    addUser(user);
  }
  userRoles.get(user).push(role);
}

// 3. 更好的键值存储
const weakMap = new WeakMap();

11.2 WeakMap

WeakMap是Map的一个变种,主要区别:

  • WeakMap的键必须是对象
  • WeakMap的键是弱引用,不会阻止垃圾回收
  • WeakMap不可迭代,没有size属性和clear()方法
javascript 复制代码
// 创建一个WeakMap
const wm = new WeakMap();

// 只能使用对象作为键
let obj1 = { id: 1 };
let obj2 = { id: 2 };

wm.set(obj1, '对象1的数据');
wm.set(obj2, '对象2的数据');

console.log(wm.get(obj1)); // "对象1的数据"
console.log(wm.has(obj2)); // true

wm.delete(obj1);
console.log(wm.has(obj1)); // false

// 当键对象不再有引用时,对应的条目会被垃圾回收
obj2 = null; // obj2引用被丢弃
// 此时WeakMap中obj2相关的条目最终会被垃圾回收

WeakMap常见用途:

  1. 存储DOM节点数据,不会造成内存泄漏
  2. 关联私有数据与对象
  3. 实现对象的私有属性

11.3 Set

Set是ES6引入的唯一值集合,类似于数组,但只存储唯一值(不重复)。

创建和使用Set
javascript 复制代码
// 创建一个空Set
const set1 = new Set();

// 使用数组创建Set
const set2 = new Set([1, 2, 3, 4, 5, 5, 5]); // 重复的值会被忽略
console.log(set2.size); // 5

// 添加值
set1.add('apple');
set1.add(42);
set1.add(true);
set1.add('apple'); // 重复,不会被添加

// 检查值是否存在
console.log(set1.has('apple')); // true
console.log(set1.has('banana')); // false

// 获取Set大小
console.log(set1.size); // 3

// 删除值
set1.delete(42);
console.log(set1.size); // 2

// 清空Set
set1.clear();
console.log(set1.size); // 0
Set迭代
javascript 复制代码
const set = new Set(['apple', 'banana', 'orange']);

// 使用forEach
set.forEach(value => {
  console.log(value);
});
// apple
// banana
// orange

// 使用for...of迭代
for (const value of set) {
  console.log(value);
}
// apple
// banana
// orange

// 转换为数组
const array = [...set];
console.log(array); // ["apple", "banana", "orange"]
Set的实用操作
javascript 复制代码
// 1. 数组去重
const numbers = [1, 2, 3, 3, 4, 4, 5, 5, 6];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3, 4, 5, 6]

// 2. 求两个数组的交集
function intersection(arr1, arr2) {
  const set = new Set(arr2);
  return arr1.filter(item => set.has(item));
}

const arr1 = [1, 2, 3, 4];
const arr2 = [3, 4, 5, 6];
console.log(intersection(arr1, arr2)); // [3, 4]

// 3. 求两个数组的并集
function union(arr1, arr2) {
  return [...new Set([...arr1, ...arr2])];
}

console.log(union(arr1, arr2)); // [1, 2, 3, 4, 5, 6]

// 4. 求两个数组的差集(在arr1中但不在arr2中的元素)
function difference(arr1, arr2) {
  const set = new Set(arr2);
  return arr1.filter(item => !set.has(item));
}

console.log(difference(arr1, arr2)); // [1, 2]
console.log(difference(arr2, arr1)); // [5, 6]

11.4 WeakSet

WeakSet是Set的一个变种,主要区别:

  • WeakSet只能存储对象
  • WeakSet中的对象是弱引用,不会阻止垃圾回收
  • WeakSet不可迭代,没有size属性和clear()方法
javascript 复制代码
// 创建一个WeakSet
const ws = new WeakSet();

// 只能添加对象
let obj1 = { id: 1 };
let obj2 = { id: 2 };

ws.add(obj1);
ws.add(obj2);

console.log(ws.has(obj1)); // true
console.log(ws.has(obj2)); // true

ws.delete(obj1);
console.log(ws.has(obj1)); // false

// 当对象不再有引用时,对应的条目会被垃圾回收
obj2 = null; // obj2引用被丢弃
// 此时WeakSet中obj2最终会被垃圾回收

WeakSet常见用途:

  1. 存储DOM节点,不会造成内存泄漏
  2. 标记已处理过的对象
  3. 确保对象的唯一性

十二、Symbol

Symbol是ES6引入的一种新的原始数据类型,表示独一无二的值。

12.1 Symbol基础

javascript 复制代码
// 创建Symbol
const sym1 = Symbol();
const sym2 = Symbol('描述');
const sym3 = Symbol('描述'); // 即使描述相同,Symbol也不同

// Symbol是唯一的
console.log(sym2 === sym3); // false

// 获取Symbol的描述
console.log(sym2.description); // "描述"

// Symbol不能使用new关键字
// const sym4 = new Symbol(); // TypeError

12.2 全局Symbol注册表

使用Symbol.for()创建共享的Symbol:

javascript 复制代码
// 通过键在全局注册表中查找,如果不存在则创建
const globalSym1 = Symbol.for('全局Symbol');
const globalSym2 = Symbol.for('全局Symbol');

// 使用相同键获取的是同一个Symbol
console.log(globalSym1 === globalSym2); // true

// 获取已注册Symbol的键
console.log(Symbol.keyFor(globalSym1)); // "全局Symbol"

// 普通Symbol不在全局注册表中
const localSym = Symbol('局部Symbol');
console.log(Symbol.keyFor(localSym)); // undefined

12.3 作为对象属性

Symbol最常见的用途是作为对象的唯一属性键:

javascript 复制代码
const mySymbol = Symbol('mySymbol');
const obj = {
  [mySymbol]: '这是一个Symbol属性',
  regularProp: '这是一个常规属性'
};

// 访问Symbol属性
console.log(obj[mySymbol]); // "这是一个Symbol属性"

// Symbol属性不会出现在常规遍历方法中
console.log(Object.keys(obj)); // ["regularProp"]
console.log(Object.getOwnPropertyNames(obj)); // ["regularProp"]

// 但可以通过专门的方法获取
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(mySymbol)]
console.log(Reflect.ownKeys(obj)); // ["regularProp", Symbol(mySymbol)]

12.4 内置Symbol

JavaScript定义了一些内置的Symbol值,称为"众所周知的Symbol"(well-known symbols),用于定制对象的行为。

javascript 复制代码
// Symbol.iterator - 定义对象的默认迭代器
const myArray = [1, 2, 3];
const iterator = myArray[Symbol.iterator]();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

// Symbol.toStringTag - 自定义Object.prototype.toString()的结果
class MyClass {
  get [Symbol.toStringTag]() {
    return 'MyClass';
  }
}
console.log(Object.prototype.toString.call(new MyClass())); // "[object MyClass]"

// Symbol.species - 创建派生对象时使用的构造函数
class SpecialArray extends Array {
  static get [Symbol.species]() {
    return Array; // 派生对象使用Array构造函数
  }
}
const instance = new SpecialArray(1, 2, 3);
const mapped = instance.map(x => x * 2); // 使用Symbol.species
console.log(mapped instanceof SpecialArray); // false
console.log(mapped instanceof Array); // true

其他常用的内置Symbol:

  • Symbol.hasInstance:自定义instanceof行为
  • Symbol.isConcatSpreadable:自定义Array.prototype.concat()行为
  • Symbol.match、Symbol.replace、Symbol.search、Symbol.split:自定义字符串方法行为
  • Symbol.unscopables:自定义with语句行为

12.5 Symbol的实际应用场景

javascript 复制代码
// 1. 定义对象的私有属性(有限的私有性)
const _hidden = Symbol('hidden');

class MyClass {
  constructor() {
    this[_hidden] = 'private data';
  }
  
  getHiddenData() {
    return this[_hidden];
  }
}

const instance = new MyClass();
console.log(instance.getHiddenData()); // "private data"
// 外部无法直接获取Symbol键名
console.log(instance._hidden); // undefined

// 2. 防止属性名冲突
function addFeature(obj) {
  const featureSymbol = Symbol('feature');
  obj[featureSymbol] = function() {
    return 'feature';
  };
  return obj;
}

// 3. 使用Symbol作为常量
const DIRECTION = {
  UP: Symbol('UP'),
  DOWN: Symbol('DOWN'),
  LEFT: Symbol('LEFT'),
  RIGHT: Symbol('RIGHT')
};

function move(direction) {
  switch (direction) {
    case DIRECTION.UP:
      console.log('向上移动');
      break;
    case DIRECTION.DOWN:
      console.log('向下移动');
      break;
    case DIRECTION.LEFT:
      console.log('向左移动');
      break;
    case DIRECTION.RIGHT:
      console.log('向右移动');
      break;
    default:
      console.log('无效方向');
  }
}

move(DIRECTION.UP); // "向上移动"

十三、迭代器和生成器

13.1 迭代器(Iterator)

迭代器是一种特殊对象,它提供了一种统一的方式来访问集合中的元素。

迭代器协议

实现迭代器协议的对象必须有一个next()方法,该方法返回一个包含两个属性的对象:

  • value:当前迭代的值
  • done:表示迭代是否已完成的布尔值
javascript 复制代码
// 手动实现迭代器
function createIterator(array) {
  let index = 0;
  
  return {
    next: function() {
      if (index < array.length) {
        return { value: array[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
可迭代协议

实现可迭代协议的对象必须有一个以Symbol.iterator为键的方法,该方法返回一个迭代器。

JavaScript中许多内置类型都是可迭代的:

  • Array
  • String
  • Map
  • Set
  • arguments对象
  • NodeList等DOM集合
javascript 复制代码
// 使用for...of迭代可迭代对象
const array = [1, 2, 3];
for (const value of array) {
  console.log(value);
}
// 1
// 2
// 3

const str = "Hello";
for (const char of str) {
  console.log(char);
}
// "H"
// "e"
// "l"
// "l"
// "o"

// 手动实现一个可迭代对象
const myIterable = {
  data: [1, 2, 3],
  
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;
    
    return {
      next: function() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (const value of myIterable) {
  console.log(value);
}
// 1
// 2
// 3
可迭代对象的其他用途
javascript 复制代码
// 解构赋值
const [a, b, c] = [1, 2, 3];
const [first, ...rest] = "Hello";
console.log(first, rest); // "H" ["e", "l", "l", "o"]

// 展开运算符
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5]

// Array.from()
const set = new Set([1, 2, 3]);
const arrayFromSet = Array.from(set);
console.log(arrayFromSet); // [1, 2, 3]

// Map, Set构造函数
const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const setFromArray = new Set([1, 2, 3]);

// Promise.all(), Promise.race()等
const promises = [Promise.resolve(1), Promise.resolve(2)];
Promise.all(promises).then(console.log); // [1, 2]

13.2 生成器(Generator)

生成器是一种特殊的函数,可以暂停执行,稍后再恢复。生成器函数返回一个生成器对象,该对象符合迭代器协议。

生成器函数语法

使用星号(*)定义生成器函数:

javascript 复制代码
// 生成器函数声明
function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

// 生成器函数表达式
const simpleGenerator = function*() {
  yield 1;
  yield 2;
  yield 3;
};

// 作为对象方法的生成器
const obj = {
  *generator() {
    yield 1;
    yield 2;
  }
};

// 作为类方法的生成器
class MyClass {
  *generator() {
    yield 1;
    yield 2;
  }
}
生成器的基本使用
javascript 复制代码
function* countUp() {
  yield 1;
  yield 2;
  yield 3;
}

// 生成器返回一个迭代器
const iterator = countUp();

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

// 使用for...of迭代生成器
for (const value of countUp()) {
  console.log(value);
}
// 1
// 2
// 3

// 展开生成器
console.log([...countUp()]); // [1, 2, 3]
yield关键字

yield关键字用于暂停生成器函数的执行并返回一个值给调用者:

javascript 复制代码
function* generateSequence() {
  yield 1;
  console.log('第一个yield之后');
  yield 2;
  console.log('第二个yield之后');
  return 3; // return设置最终值,并完成迭代
}

const gen = generateSequence();

console.log(gen.next()); // {value: 1, done: false}
// "第一个yield之后"
console.log(gen.next()); // {value: 2, done: false}
// "第二个yield之后"
console.log(gen.next()); // {value: 3, done: true}
console.log(gen.next()); // {value: undefined, done: true}

// 注意:for...of循环会忽略生成器返回的值
for (const value of generateSequence()) {
  console.log(value); // 只输出1和2,不会输出3
}
向生成器传递值

调用next()方法时可以传入参数,该参数会成为上一个yield表达式的结果:

javascript 复制代码
function* twoWayGenerator() {
  const a = yield 1;
  console.log('收到:', a);
  
  const b = yield 2;
  console.log('收到:', b);
  
  return 3;
}

const gen = twoWayGenerator();

console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next('hello')); // 输出"收到: hello", 返回 {value: 2, done: false}
console.log(gen.next('world')); // 输出"收到: world", 返回 {value: 3, done: true}

注意:第一个next()调用无法向生成器传值,因为此时没有等待接收值的yield表达式。

yield*表达式

使用yield*委托给另一个生成器或可迭代对象:

javascript 复制代码
function* gen1() {
  yield 1;
  yield 2;
}

function* gen2() {
  yield 3;
  yield 4;
}

function* combined() {
  yield* gen1();
  yield* gen2();
  yield* [5, 6]; // 委托给数组
  yield* "78";   // 委托给字符串
}

for (const value of combined()) {
  console.log(value);
}
// 1, 2, 3, 4, 5, 6, "7", "8"
生成器方法

生成器对象除了有next()方法外,还有两个额外的方法:

javascript 复制代码
function* gen() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (e) {
    console.log('捕获到错误:', e);
  }
}

const iterator = gen();

console.log(iterator.next()); // {value: 1, done: false}

// return()方法终止生成器并返回指定值
console.log(iterator.return(10)); // {value: 10, done: true}
console.log(iterator.next()); // {value: undefined, done: true}

// 重新创建一个生成器
const iterator2 = gen();

console.log(iterator2.next()); // {value: 1, done: false}

// throw()方法向生成器抛出错误
console.log(iterator2.throw(new Error('生成器错误'))); // 输出"捕获到错误: Error: 生成器错误"
// 如果生成器捕获了错误,它会恢复执行,到下一个yield
// {value: 2, done: false}

13.3 生成器的实际应用

1. 生成无限序列
javascript 复制代码
// 生成无限的斐波那契数列
function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

// 取前10个斐波那契数
const fib = fibonacci();
const first10 = Array.from({ length: 10 }, () => fib.next().value);
console.log(first10); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
2. 简化异步代码
javascript 复制代码
// 使用生成器处理异步操作
function* fetchUsers() {
  try {
    const response = yield fetch('https://jsonplaceholder.typicode.com/users');
    const users = yield response.json();
    return users;
  } catch (error) {
    console.error('获取用户失败:', error);
  }
}

// 执行生成器
function run(generator) {
  const iterator = generator();
  
  function iterate(prevResult) {
    const { value, done } = iterator.next(prevResult);
    
    if (done) {
      return value;
    }
    
    // 处理Promise值
    if (value instanceof Promise) {
      return value.then(iterate, error => iterator.throw(error));
    }
    
    return iterate(value);
  }
  
  return iterate();
}

run(fetchUsers).then(users => console.log('用户:', users));

// 注意:这是async/await出现前的一种处理异步的模式
// 现在更推荐使用async/await
3. 实现迭代协议
javascript 复制代码
// 为自定义数据结构实现迭代协议
class Tree {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
  
  // 使用生成器实现中序遍历
  *[Symbol.iterator]() {
    if (this.left) {
      yield* this.left;
    }
    
    yield this.value;
    
    if (this.right) {
      yield* this.right;
    }
  }
  
  // 层序遍历
  *breadthFirst() {
    const queue = [this];
    
    while (queue.length > 0) {
      const node = queue.shift();
      yield node.value;
      
      if (node.left) {
        queue.push(node.left);
      }
      
      if (node.right) {
        queue.push(node.right);
      }
    }
  }
}

// 构建一棵树
const tree = new Tree(4);
tree.left = new Tree(2);
tree.right = new Tree(6);
tree.left.left = new Tree(1);
tree.left.right = new Tree(3);
tree.right.left = new Tree(5);
tree.right.right = new Tree(7);

// 中序遍历(默认迭代器)
for (const value of tree) {
  console.log(value);
}
// 1, 2, 3, 4, 5, 6, 7

// 层序遍历
for (const value of tree.breadthFirst()) {
  console.log(value);
}
// 4, 2, 6, 1, 3, 5, 7

十四、Proxy和Reflect

14.1 Proxy

Proxy对象用于创建一个对象的代理,从而可以拦截和自定义对象的基本操作,如属性查找、赋值、枚举、函数调用等。

基本语法
javascript 复制代码
const proxy = new Proxy(target, handler);
  • target: 要代理的目标对象
  • handler: 包含"捕获器"(traps)的对象,定义哪些操作需要被拦截
常用捕获器
javascript 复制代码
// 创建一个简单的代理
const person = {
  name: '张三',
  age: 30
};

const handler = {
  // 拦截属性读取
  get(target, property, receiver) {
    console.log(`正在获取${property}属性`);
    return property in target ? target[property] : '不存在';
  },
  
  // 拦截属性设置
  set(target, property, value, receiver) {
    console.log(`正在设置${property}属性为${value}`);
    
    if (property === 'age' && typeof value !== 'number') {
      throw new TypeError('age必须是数字');
    }
    
    target[property] = value;
    return true; // 表示设置成功
  },
  
  // 拦截属性删除
  deleteProperty(target, property) {
    console.log(`正在删除${property}属性`);
    delete target[property];
    return true; // 表示删除成功
  },
  
  // 拦截Object.keys等
  ownKeys(target) {
    console.log('正在获取所有键');
    return Reflect.ownKeys(target);
  },
  
  // 拦截属性存在性检查
  has(target, property) {
    console.log(`正在检查${property}属性是否存在`);
    return property in target;
  }
};

const proxy = new Proxy(person, handler);

// 使用代理
console.log(proxy.name); // 输出"正在获取name属性",然后是"张三"
console.log(proxy.notExist); // 输出"正在获取notExist属性",然后是"不存在"

proxy.age = 31; // 输出"正在设置age属性为31"
// proxy.age = '三十一'; // 抛出TypeError: age必须是数字

delete proxy.name; // 输出"正在删除name属性"

console.log('name' in proxy); // 输出"正在检查name属性是否存在",然后是false

console.log(Object.keys(proxy)); // 输出"正在获取所有键",然后是["age"]
代理的应用场景
javascript 复制代码
// 1. 数据验证
function createValidator(object, validations) {
  return new Proxy(object, {
    set(target, property, value) {
      if (validations.hasOwnProperty(property)) {
        if (!validations[property].test(value)) {
          throw new Error(`无效的${property}: ${value}`);
        }
      }
      
      target[property] = value;
      return true;
    }
  });
}

const user = createValidator(
  { name: '张三', age: 30 },
  {
    name: /^[a-zA-Z\u4e00-\u9fa5]{2,10}$/,
    age: /^(1[8-9]|[2-9][0-9])$/
  }
);

user.name = '李四'; // 有效
// user.name = 'A'; // 抛出错误:无效的name: A(不满足2-10个字符)
// user.age = 17; // 抛出错误:无效的age: 17(不满足18岁以上)

// 2. 访问控制
function createAccessControl(object, access) {
  return new Proxy(object, {
    get(target, property, receiver) {
      if (access === 'readonly' && typeof target[property] === 'function') {
        if (property.startsWith('set') || property === 'push' || property === 'pop') {
          throw new Error(`只读模式下不允许调用${property}`);
        }
      }
      return target[property];
    },
    
    set(target, property, value, receiver) {
      if (access === 'readonly') {
        throw new Error('只读模式下不允许修改属性');
      }
      target[property] = value;
      return true;
    },
    
    deleteProperty(target, property) {
      if (access === 'readonly') {
        throw new Error('只读模式下不允许删除属性');
      }
      delete target[property];
      return true;
    }
  });
}

const data = { value: 42 };
const readonlyData = createAccessControl(data, 'readonly');

console.log(readonlyData.value); // 42
// readonlyData.value = 43; // 抛出错误:只读模式下不允许修改属性

// 3. 自动填充属性
function createAutoFill(object, defaults) {
  return new Proxy(object, {
    get(target, property, receiver) {
      if (!(property in target) && property in defaults) {
        target[property] = defaults[property];
      }
      return target[property];
    }
  });
}

const settings = createAutoFill({}, {
  theme: 'dark',
  fontSize: 14,
  language: 'zh-CN'
});

console.log(settings.theme); // 'dark'(自动填充)
settings.theme = 'light';
console.log(settings.theme); // 'light'(已修改)

14.2 Reflect

Reflect是一个内置对象,提供了与Proxy处理程序捕获器对应的方法,用于实现JavaScript操作的默认行为。

基本用法
javascript 复制代码
// 操作属性
const obj = { x: 1, y: 2 };
console.log(Reflect.get(obj, 'x')); // 1
Reflect.set(obj, 'z', 3);
console.log(obj.z); // 3
console.log(Reflect.has(obj, 'y')); // true
Reflect.deleteProperty(obj, 'y');
console.log(obj.y); // undefined

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

// 对象创建
const instance = Reflect.construct(Array, [1, 2, 3]);
console.log(instance); // [1, 2, 3]

// 原型操作
const prototype = { inherited: true };
const obj2 = {};
Reflect.setPrototypeOf(obj2, prototype);
console.log(Reflect.getPrototypeOf(obj2)); // { inherited: true }
console.log(obj2.inherited); // true

// 属性描述符
Reflect.defineProperty(obj, 'name', {
  value: 'test',
  writable: false
});
console.log(obj.name); // 'test'
// obj.name = 'changed'; // 在严格模式下会抛出错误,不能修改

console.log(Reflect.getOwnPropertyDescriptor(obj, 'name'));
// {value: "test", writable: false, enumerable: false, configurable: false}
Reflect与Proxy结合使用
javascript 复制代码
const target = {
  name: '张三',
  age: 30
};

const handler = {
  get(target, property, receiver) {
    console.log(`正在获取${property}属性`);
    // 使用Reflect.get实现默认行为
    return Reflect.get(target, property, receiver);
  },
  
  set(target, property, value, receiver) {
    console.log(`正在设置${property}属性为${value}`);
    // 使用Reflect.set实现默认行为
    return Reflect.set(target, property, value, receiver);
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.name); // 输出"正在获取name属性",然后是"张三"
proxy.age = 31; // 输出"正在设置age属性为31"

十五、ES6新增的数组和对象方法

15.1 新的数组方法

ES6添加了多个有用的数组方法,使数组操作更加方便。

Array.from()

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

javascript 复制代码
// 转换字符串
console.log(Array.from('hello')); // ["h", "e", "l", "l", "o"]

// 转换Set
const set = new Set([1, 2, 3, 3, 4]);
console.log(Array.from(set)); // [1, 2, 3, 4]

// 转换Map
const map = new Map([['a', 1], ['b', 2]]);
console.log(Array.from(map)); // [["a", 1], ["b", 2]]

// 转换类数组对象
function fn() {
  return Array.from(arguments);
}
console.log(fn(1, 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 + 1)); // [1, 2, 3, 4, 5]
Array.of()

创建一个新数组,将传入的参数作为元素:

javascript 复制代码
console.log(Array.of(1, 2, 3)); // [1, 2, 3]
console.log(Array.of('a', 'b', 'c')); // ["a", "b", "c"]
console.log(Array.of(5)); // [5]

// 对比普通Array构造函数
console.log(new Array(1, 2, 3)); // [1, 2, 3]
console.log(new Array(5)); // [empty × 5] - 创建5个空位的数组
find()和findIndex()

查找数组中满足条件的元素或其索引:

javascript 复制代码
const numbers = [1, 2, 3, 4, 5];

// find返回第一个满足条件的元素
const found = numbers.find(num => num > 3);
console.log(found); // 4

// findIndex返回第一个满足条件的元素的索引
const foundIndex = numbers.findIndex(num => num > 3);
console.log(foundIndex); // 3

// 查找对象数组
const users = [
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' }
];

const user = users.find(user => user.id === 2);
console.log(user); // {id: 2, name: "李四"}
includes()

检查数组是否包含某个元素:

javascript 复制代码
const array = [1, 2, 3, 4, 5, NaN];

// 与indexOf不同,includes可以正确判断NaN
console.log(array.includes(3)); // true
console.log(array.includes(6)); // false
console.log(array.includes(NaN)); // true
console.log(array.indexOf(NaN) !== -1); // false(indexOf不能找到NaN)

// 指定开始搜索的索引
console.log(array.includes(1, 1)); // false(从索引1开始搜索)
其他数组方法扩展
javascript 复制代码
// fill() - 用一个固定值填充数组
console.log(new Array(3).fill('a')); // ["a", "a", "a"]
console.log([1, 2, 3].fill('a', 1, 2)); // [1, "a", 3]

// copyWithin() - 复制数组的一部分到同一数组中的另一个位置
console.log([1, 2, 3, 4, 5].copyWithin(0, 3)); // [4, 5, 3, 4, 5]
console.log([1, 2, 3, 4, 5].copyWithin(0, 3, 4)); // [4, 2, 3, 4, 5]

// entries(), keys(), values() - 返回数组的迭代器
const array = ['a', 'b', 'c'];
console.log([...array.keys()]); // [0, 1, 2]
console.log([...array.values()]); // ["a", "b", "c"]
console.log([...array.entries()]); // [[0, "a"], [1, "b"], [2, "c"]]

// flat() - 扁平化嵌套数组
console.log([1, [2, [3]]].flat()); // [1, 2, [3]]
console.log([1, [2, [3]]].flat(2)); // [1, 2, 3]
console.log([1, [2, [3, [4]]]].flat(Infinity)); // [1, 2, 3, 4]

// flatMap() - 先映射再扁平化
console.log([1, 2, 3].flatMap(x => [x, x * 2])); // [1, 2, 2, 4, 3, 6]

15.2 新的对象方法

ES6增强了对象的能力,添加了多个新方法。

Object.assign()

合并对象,将源对象的属性复制到目标对象:

javascript 复制代码
const target = { a: 1, b: 2 };
const source1 = { b: 3, c: 4 };
const source2 = { c: 5, d: 6 };

const result = Object.assign(target, source1, source2);
console.log(target); // {a: 1, b: 3, c: 5, d: 6}
console.log(result); // {a: 1, b: 3, c: 5, d: 6}(与target相同)

// 创建一个新对象而不修改原对象
const newObj = Object.assign({}, target, source1);
console.log(newObj); // {a: 1, b: 3, c: 4}
console.log(target); // 保持不变

// 注意:Object.assign执行的是浅拷贝
const obj = { a: { b: 1 } };
const copy = Object.assign({}, obj);
obj.a.b = 2;
console.log(copy.a.b); // 2(修改obj也会影响copy)
Object.keys(), Object.values(), Object.entries()

获取对象的键、值或键值对:

javascript 复制代码
const person = { name: '张三', age: 30, city: '北京' };

// 获取所有键
console.log(Object.keys(person)); // ["name", "age", "city"]

// 获取所有值
console.log(Object.values(person)); // ["张三", 30, "北京"]

// 获取所有键值对
console.log(Object.entries(person)); // [["name", "张三"], ["age", 30], ["city", "北京"]]

// 使用entries遍历对象
for (const [key, value] of Object.entries(person)) {
  console.log(`${key}: ${value}`);
}
// name: 张三
// age: 30
// city: 北京

// 从entries创建Map
const map = new Map(Object.entries(person));
console.log(map.get('name')); // "张三"
Object.fromEntries()

将键值对数组转换为对象,是Object.entries()的逆操作:

javascript 复制代码
const entries = [
  ['name', '张三'],
  ['age', 30],
  ['city', '北京']
];

const obj = Object.fromEntries(entries);
console.log(obj); // {name: "张三", age: 30, city: "北京"}

// 将Map转换为对象
const map = new Map([
  ['name', '李四'],
  ['age', 25]
]);
console.log(Object.fromEntries(map)); // {name: "李四", age: 25}

// 转换查询字符串
const searchParams = new URLSearchParams('name=王五&age=35');
console.log(Object.fromEntries(searchParams)); // {name: "王五", age: "35"}
Object.getOwnPropertyDescriptor(s)

获取属性的描述符:

javascript 复制代码
const obj = { name: '张三' };

// 定义带有特定特性的属性
Object.defineProperty(obj, 'age', {
  value: 30,
  writable: false,
  enumerable: true,
  configurable: true
});

// 获取单个属性的描述符
console.log(Object.getOwnPropertyDescriptor(obj, 'name'));
// {value: "张三", writable: true, enumerable: true, configurable: true}

console.log(Object.getOwnPropertyDescriptor(obj, 'age'));
// {value: 30, writable: false, enumerable: true, configurable: true}

// 获取所有属性的描述符
console.log(Object.getOwnPropertyDescriptors(obj));
/*
{
  name: {value: "张三", writable: true, enumerable: true, configurable: true},
  age: {value: 30, writable: false, enumerable: true, configurable: true}
}
*/
Object.is()

判断两个值是否相同:

javascript 复制代码
// 大多数情况下与===相同
console.log(Object.is(1, 1)); // true
console.log(Object.is('foo', 'foo')); // true
console.log(Object.is({}, {})); // false(不同对象引用)

// 但处理某些特殊情况不同
console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false

console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true

十六、ES6其他特性

16.1 新的字符串方法

javascript 复制代码
// startsWith() - 检查字符串是否以指定文本开头
console.log('Hello'.startsWith('He')); // true
console.log('Hello'.startsWith('lo', 3)); // true(从索引3开始)

// endsWith() - 检查字符串是否以指定文本结尾
console.log('Hello'.endsWith('lo')); // true
console.log('Hello'.endsWith('He', 2)); // true(只考虑前2个字符)

// includes() - 检查字符串是否包含指定文本
console.log('Hello'.includes('ell')); // true
console.log('Hello'.includes('ell', 2)); // false(从索引2开始)

// repeat() - 重复字符串
console.log('abc'.repeat(3)); // "abcabcabc"
console.log('abc'.repeat(0)); // ""

// padStart() & padEnd() - 填充字符串到指定长度
console.log('5'.padStart(2, '0')); // "05"
console.log('5'.padEnd(2, '0')); // "50"
console.log('abc'.padStart(8, '123')); // "12312abc"
console.log('abc'.padEnd(8, '123')); // "abc12312"

// trimStart() & trimEnd() - 去除字符串开头或结尾的空白字符
console.log('  abc  '.trimStart()); // "abc  "
console.log('  abc  '.trimEnd()); // "  abc"

16.2 for...of循环

for...of循环用于迭代可迭代对象:

javascript 复制代码
// 迭代数组
const array = [1, 2, 3];
for (const item of array) {
  console.log(item);
}
// 1, 2, 3

// 迭代字符串
for (const char of 'hello') {
  console.log(char);
}
// "h", "e", "l", "l", "o"

// 迭代Map
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
  console.log(`${key}: ${value}`);
}
// "a: 1", "b: 2"

// 迭代Set
const set = new Set([1, 2, 3]);
for (const item of set) {
  console.log(item);
}
// 1, 2, 3

// for...of vs for...in
const arr = ['a', 'b', 'c'];
arr.name = 'myArray';

for (const item of arr) {
  console.log(item); // 只迭代值: "a", "b", "c"
}

for (const key in arr) {
  console.log(key); // 迭代索引和属性: "0", "1", "2", "name"
}

16.3 Math和Number新功能

javascript 复制代码
// Number.isInteger() - 检查值是否为整数
console.log(Number.isInteger(1));    // true
console.log(Number.isInteger(1.0));  // true
console.log(Number.isInteger(1.1));  // false
console.log(Number.isInteger('1'));  // false

// Number.isFinite() - 检查值是否为有限数字
console.log(Number.isFinite(42));    // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite('42'));  // false(与全局isFinite不同)

// Number.isNaN() - 检查值是否为NaN
console.log(Number.isNaN(NaN));      // true
console.log(Number.isNaN('NaN'));    // false(与全局isNaN不同)

// Number.parseFloat() 和 Number.parseInt() - 解析字符串为数字
console.log(Number.parseFloat('3.14')); // 3.14
console.log(Number.parseInt('42px'));   // 42

// Number.EPSILON - 表示"机器精度"
console.log(Number.EPSILON); // 2.220446049250313e-16
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON); // true

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

// Math.sign() - 返回数字的符号
console.log(Math.sign(10));   // 1
console.log(Math.sign(0));    // 0
console.log(Math.sign(-10));  // -1

// Math中的其他新方法
console.log(Math.cbrt(8));     // 2(立方根)
console.log(Math.hypot(3, 4)); // 5(平方和的平方根)
console.log(Math.log10(100));  // 2(以10为底的对数)
console.log(Math.log2(8));     // 3(以2为底的对数)

16.4 尾调用优化

尾调用优化(Tail Call Optimization)是ES6的一个性能优化,当函数的最后一个操作是调用另一个函数时,JS引擎可以优化调用栈。

javascript 复制代码
// 不是尾调用(有后续操作)
function notTailCall() {
  return 1 + factorial(n - 1);
}

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

// 传统递归实现
function factorialTraditional(n) {
  if (n <= 1) return 1;
  return n * factorialTraditional(n - 1); // 不是尾调用
}

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

尾调用优化仅在严格模式("use strict")下可用,且并非所有浏览器都实现了它。

16.5 标签模板字符串进阶

javascript 复制代码
// 自定义模板标签函数
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = i < values.length ? 
      `<span class="highlight">${values[i]}</span>` : 
      '';
    return result + str + value;
  }, '');
}

const name = '张三';
const age = 30;
const html = highlight`我的名字是${name},今年${age}岁。`;
console.log(html);
// "我的名字是<span class="highlight">张三</span>,今年<span class="highlight">30</span>岁。"

// 原始字符串值
function raw(strings, ...values) {
  return strings.raw.reduce((result, str, i) => {
    const value = i < values.length ? values[i] : '';
    return result + str + value;
  }, '');
}

console.log(raw`Hello\nWorld`); // 输出"Hello\nWorld"(不解释转义序列)
console.log(`Hello\nWorld`);     // 输出两行(解释转义序列)

十七、总结与最佳实践

17.1 ES6的主要特性总结

ES6(ECMAScript 2015)带来了JavaScript语言的重大改进和增强:

  1. 语法糖和改进

    • let和const变量声明
    • 箭头函数
    • 模板字符串
    • 解构赋值
    • 默认参数
    • 展开运算符和剩余参数
    • 对象字面量增强
  2. 新的数据结构和类型

    • Map和Set(及其弱引用版本)
    • Symbol原始类型
    • 类语法(Class)
  3. 模块化

    • ES模块 (import/export)
  4. 异步编程

    • Promise
    • 生成器函数
  5. 迭代和集合处理

    • 迭代器和for...of循环
    • 新的数组方法
    • 新的对象方法
  6. 反射和元编程

    • Proxy
    • Reflect
  7. 其他功能

    • 新的字符串方法
    • 新的数学方法
    • 尾调用优化

17.2 ES6最佳实践

  1. 使用const和let替代var

    • 优先使用const声明不会改变的变量
    • 使用let声明会改变的变量
    • 避免使用var
  2. 优先使用箭头函数

    • 对于简短的函数表达式,特别是回调函数
    • 当函数需要继承上下文的this值时
    • 但避免在需要自己的this、arguments或使用new的地方使用
  3. 使用模板字符串

    • 避免使用字符串拼接(+)
    • 利用模板字符串进行多行字符串创建和变量插值
  4. 使用解构赋值

    • 从对象和数组中提取值
    • 交换变量值
    • 提取函数参数
  5. 使用扩展语法

    • 用于数组和对象的浅复制
    • 合并数组和对象
    • 将可迭代对象转换为数组
  6. 使用类语法

    • 当需要创建具有方法和状态的对象实例时
    • 当需要利用继承时
  7. 使用模块

    • 将代码组织成模块
    • 使用命名导出明确表达模块API
    • 限制默认导出的使用
  8. 异步编程

    • 使用Promise或async/await而不是回调
    • async/await比Promise链更具可读性
  9. 使用新的集合类型

    • 当需要非字符串键时使用Map
    • 当需要存储唯一值集合时使用Set
    • 处理对象引用和内存问题时考虑WeakMap和WeakSet
  10. 使用新的API和方法

    • 利用ES6添加的新数组方法(如find、includes)
    • 利用新的对象方法(如Object.assign、Object.entries)
    • 利用新的数学和数字方法

17.3 ES6之后的发展

ES6之后,JavaScript语言规范继续以每年一个版本的节奏发布更新:

  • ES2016 (ES7)

    • Array.prototype.includes
    • 指数运算符 (**)
  • ES2017 (ES8)

    • async/await
    • Object.values/entries
    • String padding
    • Object.getOwnPropertyDescriptors
    • 尾逗号函数参数
  • ES2018 (ES9)

    • 异步迭代
    • Rest/Spread属性
    • Promise.finally
    • 正则表达式增强
  • ES2019 (ES10)

    • Array.flat/flatMap
    • Object.fromEntries
    • String.trimStart/trimEnd
    • Symbol.description
    • 可选的catch绑定
  • ES2020 (ES11)

    • 空值合并运算符 (??)
    • 可选链操作符 (.?)
    • Promise.allSettled
    • BigInt
    • globalThis
    • 动态import
  • ES2021 (ES12)

    • 字符串替换:replaceAll
    • Promise.any
    • 逻辑赋值运算符 (&&=, ||=, ??=)
    • 数字分隔符 (1_000_000)
  • ES2022 (ES13)

    • Class私有字段和方法
    • Top-level await
    • Object.hasOwn
    • 正则表达式匹配索引

通过持续学习这些新特性,你可以更有效地编写现代JavaScript代码。

17.4 最终建议

  1. 渐进式采用

    • 从最有用的特性开始,如箭头函数、let/const、解构赋值等
    • 使用Babel等工具转译代码以兼容旧浏览器
  2. 了解浏览器兼容性

  3. 保持学习

    • JavaScript语言在持续进化
    • 关注TC39提案和即将到来的特性
  4. 实践

    • 通过实际项目应用这些知识
    • 重构现有代码以使用ES6特性
    • 编写更简洁、更具表达力和可维护性的代码
相关推荐
运维@小兵20 分钟前
vue开发用户注册功能
前端·javascript·vue.js
蓝婷儿40 分钟前
前端面试每日三题 - Day 30
前端·面试·职场和发展
oMMh44 分钟前
使用C# ASP.NET创建一个可以由服务端推送信息至客户端的WEB应用(2)
前端·c#·asp.net
一口一个橘子1 小时前
[ctfshow web入门] web69
前端·web安全·网络安全
读心悦2 小时前
CSS:盒子阴影与渐变完全解析:从基础语法到创意应用
前端·css
m0_616188492 小时前
使用vue3-seamless-scroll实现列表自动滚动播放
开发语言·javascript·ecmascript
湛海不过深蓝3 小时前
【ts】defineProps数组的类型声明
前端·javascript·vue.js
layman05283 小时前
vue 中的数据代理
前端·javascript·vue.js
柒七爱吃麻辣烫3 小时前
前端项目打包部署流程j
前端
layman05284 小时前
vue中理解MVVM
前端·javascript·vue.js