文章目录
-
- 一、ES6简介
-
- [1.1 什么是ES6?](#1.1 什么是ES6?)
- [1.2 为什么要学习ES6?](#1.2 为什么要学习ES6?)
- [1.3 浏览器支持情况](#1.3 浏览器支持情况)
- 二、let和const关键字
-
- [2.1 let关键字](#2.1 let关键字)
- [2.2 const关键字](#2.2 const关键字)
- [2.3 var、let和const的选择](#2.3 var、let和const的选择)
- 三、箭头函数
-
- [3.1 基本语法](#3.1 基本语法)
- [3.2 箭头函数的特点](#3.2 箭头函数的特点)
- [3.3 何时使用箭头函数](#3.3 何时使用箭头函数)
- 四、模板字符串
-
- [4.1 基本语法](#4.1 基本语法)
- [4.2 多行字符串](#4.2 多行字符串)
- [4.3 表达式计算](#4.3 表达式计算)
- [4.4 嵌套模板](#4.4 嵌套模板)
- [4.5 标签模板](#4.5 标签模板)
- 五、解构赋值
- 六、默认参数
-
- [6.1 基本语法](#6.1 基本语法)
- [6.2 多个默认参数](#6.2 多个默认参数)
- [6.3 表达式作为默认值](#6.3 表达式作为默认值)
- [6.4 后面的参数引用前面的参数](#6.4 后面的参数引用前面的参数)
- [6.5 默认参数的暂时性死区](#6.5 默认参数的暂时性死区)
- [6.6 与解构赋值结合](#6.6 与解构赋值结合)
- 七、展开运算符和剩余参数
-
- [7.1 展开运算符(Spread Operator)](#7.1 展开运算符(Spread Operator))
- [7.2 剩余参数(Rest Parameters)](#7.2 剩余参数(Rest Parameters))
- [7.3 展开运算符与剩余参数的区别](#7.3 展开运算符与剩余参数的区别)
- [7.4 实际应用场景](#7.4 实际应用场景)
- 八、类(Class)
- 九、模块(Modules)
-
- [9.1 导出(Export)](#9.1 导出(Export))
- [9.2 导入(Import)](#9.2 导入(Import))
- [9.3 动态导入](#9.3 动态导入)
- [9.4 模块特性](#9.4 模块特性)
- [9.5 在浏览器中使用模块](#9.5 在浏览器中使用模块)
- 十、Promise
-
- [10.1 Promise基础](#10.1 Promise基础)
- [10.2 Promise链](#10.2 Promise链)
- [10.3 Promise方法](#10.3 Promise方法)
- [10.4 实际应用场景](#10.4 实际应用场景)
- [10.5 async/await](#10.5 async/await)
- 十一、Map和Set
- 十二、Symbol
-
- [12.1 Symbol基础](#12.1 Symbol基础)
- [12.2 全局Symbol注册表](#12.2 全局Symbol注册表)
- [12.3 作为对象属性](#12.3 作为对象属性)
- [12.4 内置Symbol](#12.4 内置Symbol)
- [12.5 Symbol的实际应用场景](#12.5 Symbol的实际应用场景)
- 十三、迭代器和生成器
- 十四、Proxy和Reflect
-
- [14.1 Proxy](#14.1 Proxy)
- [14.2 Reflect](#14.2 Reflect)
- 十五、ES6新增的数组和对象方法
-
- [15.1 新的数组方法](#15.1 新的数组方法)
- [15.2 新的对象方法](#15.2 新的对象方法)
-
- Object.assign()
- [Object.keys(), Object.values(), Object.entries()](#Object.keys(), Object.values(), Object.entries())
- Object.fromEntries()
- Object.getOwnPropertyDescriptor(s)
- Object.is()
- 十六、ES6其他特性
-
- [16.1 新的字符串方法](#16.1 新的字符串方法)
- [16.2 for...of循环](#16.2 for...of循环)
- [16.3 Math和Number新功能](#16.3 Math和Number新功能)
- [16.4 尾调用优化](#16.4 尾调用优化)
- [16.5 标签模板字符串进阶](#16.5 标签模板字符串进阶)
- 十七、总结与最佳实践
-
- [17.1 ES6的主要特性总结](#17.1 ES6的主要特性总结)
- [17.2 ES6最佳实践](#17.2 ES6最佳实践)
- [17.3 ES6之后的发展](#17.3 ES6之后的发展)
- [17.4 最终建议](#17.4 最终建议)
一、ES6简介
1.1 什么是ES6?
ES6,全称ECMAScript 2015,是ECMAScript标准的第6个版本,于2015年6月发布。ECMAScript是JavaScript语言的规范标准,而JavaScript是ECMAScript的一个实现。
ES6是JavaScript语言的一次重大更新,引入了大量新的语法特性和API,使JavaScript编程变得更加强大和灵活。
1.2 为什么要学习ES6?
- 行业标准:ES6已经成为现代JavaScript开发的标准,被广泛应用于前端和Node.js开发。
- 提高生产力:ES6提供了许多语法糖和新功能,使代码更简洁、更易读、更易维护。
- 现代框架基础:诸如React、Vue、Angular等现代JavaScript框架大量使用ES6特性。
- 向后兼容:ES6完全向后兼容,你可以逐步将ES6特性引入现有项目。
1.3 浏览器支持情况
现代浏览器已经支持大部分ES6特性:
- Chrome 51+
- Firefox 54+
- Safari 10+
- Edge 14+
对于需要支持旧浏览器的项目,可以使用Babel等转译工具将ES6代码转换为ES5代码。
二、let和const关键字
ES6引入了let
和const
两个新的变量声明关键字,用来解决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 默认参数的暂时性死区
默认参数和let
、const
一样具有暂时性死区,不能在参数被定义前引用:
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
关键字有两种用法:
- 作为函数调用:
super(...args)
调用父类的构造函数 - 作为对象:
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引入了官方的模块系统,使用import
和export
语句来共享代码。
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 模块特性
- 模块代码自动运行在严格模式下
- 模块有自己的作用域,不会污染全局作用域
- 模块只被执行一次,即使被多次导入
- 模块导入是静态的,发生在运行前
- 顶级
this
是undefined
,而不是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引入了async
和await
关键字,它们构建在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常见用途:
- 存储DOM节点数据,不会造成内存泄漏
- 关联私有数据与对象
- 实现对象的私有属性
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常见用途:
- 存储DOM节点,不会造成内存泄漏
- 标记已处理过的对象
- 确保对象的唯一性
十二、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语言的重大改进和增强:
-
语法糖和改进
- let和const变量声明
- 箭头函数
- 模板字符串
- 解构赋值
- 默认参数
- 展开运算符和剩余参数
- 对象字面量增强
-
新的数据结构和类型
- Map和Set(及其弱引用版本)
- Symbol原始类型
- 类语法(Class)
-
模块化
- ES模块 (import/export)
-
异步编程
- Promise
- 生成器函数
-
迭代和集合处理
- 迭代器和for...of循环
- 新的数组方法
- 新的对象方法
-
反射和元编程
- Proxy
- Reflect
-
其他功能
- 新的字符串方法
- 新的数学方法
- 尾调用优化
17.2 ES6最佳实践
-
使用const和let替代var
- 优先使用const声明不会改变的变量
- 使用let声明会改变的变量
- 避免使用var
-
优先使用箭头函数
- 对于简短的函数表达式,特别是回调函数
- 当函数需要继承上下文的this值时
- 但避免在需要自己的this、arguments或使用new的地方使用
-
使用模板字符串
- 避免使用字符串拼接(+)
- 利用模板字符串进行多行字符串创建和变量插值
-
使用解构赋值
- 从对象和数组中提取值
- 交换变量值
- 提取函数参数
-
使用扩展语法
- 用于数组和对象的浅复制
- 合并数组和对象
- 将可迭代对象转换为数组
-
使用类语法
- 当需要创建具有方法和状态的对象实例时
- 当需要利用继承时
-
使用模块
- 将代码组织成模块
- 使用命名导出明确表达模块API
- 限制默认导出的使用
-
异步编程
- 使用Promise或async/await而不是回调
- async/await比Promise链更具可读性
-
使用新的集合类型
- 当需要非字符串键时使用Map
- 当需要存储唯一值集合时使用Set
- 处理对象引用和内存问题时考虑WeakMap和WeakSet
-
使用新的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 最终建议
-
渐进式采用
- 从最有用的特性开始,如箭头函数、let/const、解构赋值等
- 使用Babel等工具转译代码以兼容旧浏览器
-
了解浏览器兼容性
- 使用Can I Use (https://caniuse.com/)检查特性支持
- 考虑使用polyfill填补缺失功能
-
保持学习
- JavaScript语言在持续进化
- 关注TC39提案和即将到来的特性
-
实践
- 通过实际项目应用这些知识
- 重构现有代码以使用ES6特性
- 编写更简洁、更具表达力和可维护性的代码