ES6 (ECMAScript 2015) 是一次重大的语言升级,引入了许多现代化的语法和功能,极大的提高了开发效率和代码的可读性。
2000 年,ECMAScript 4.0 开始酝酿。这个版本并没有通过,但它的大部分内容被 ECMAScript6 继承了。因此,ECMAScript6 制定起点其实是 2000 年。
一、变量和作用域
使用 let 和 const 来代替 var
- 解决变量提升和作用域 的问题。
- 块级作用域:只在
{}内生效 let:可重新赋值,不可重复声明const:声明常量,必须初始化,引用地址不可变(但对象属性可变)
if (true) {
let a = 1;
const b = 2;
}
// 未声明变量的区域错误
console.log(a);
二、箭头函数(arrow functions)
箭头函数简化了部分场景下的函数使用
const add = (a, b) => a + b;
const greet = name => console.log(`Hello ${name}`);
// 返回对象需外套 `()`
const fn = () => ({ name: 'Alice' });
- 没有
this、arguments、super、new.target this继承自外层词法作用域(不会改变this的指向)- 不能用作构造函数(缺失
prototype) - 不能使用
yield(不能作为Generator)
const obj = {
name: 'Bob',
// this 指向外层,通常是 `undefined`
say: () => console.log(this.name),
};
obj.say(); // undefined
三、字符串的操作
字符的处理
字符串的包含
ES6 提供了三种新的方法用来确定一个字符串是否包含在另一个字符串中,而且这三个方法都支持第二参数,表示开始搜索的位置:
- includes():返回布尔值,表示是否找到了参数字符串
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部
字符串重复
添加了 repeat 方法返回了一个新的字符串,表示将原字符串重复 n 次。参数如果是小数,将被取整。参数为负数或 Infinity ,会报错。但是,如果参数是 0 到 -1 之间的小数,则等同于 0;
模版字符串(template literals)
const name = 'Alice';
const age = 25;
const msg = `Hello, I'm ${name}, ${age} years old.`;
- 多行字符串
- 表达式插值
${} - 标签模版 (tagged templates)
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] ? `<b>${values[i]}</b>` : '');
}, '');
}
const html = highlight`Hello, ${name} is ${age}!`;
// → "Hello, <b>Alice</b> is <b>25</b>!"
升级 Unicode
升级 Unicode 支持至 5.1.0 版本。
正则表达式
ES6 对正则表达式添加了 u修饰符,含义为 Unicode 模式,用来处理大于 \uFFFF 的 Unicode 字符。
四、解构赋值(destructuring)
数组解构
// 等长度结构
const [a, b] = [1, 2];
/// 不等长度结构
const [first, , third] = ['a', 'b', 'c'];
// 默认值
const [x, y = 10] = [1];
对象解构
const { name, age } = {
name: 'Tom',
age: 30,
};
// 重命名
const { name: n, age: a } = obj;
// 默认值
const { city = 'Beijing' } = obj;
函数参数的解构
function greet({ name, age }) {
console.log(`${name}, ${age}`);
}
greet({ name: 'John', age: 20 });
五、默认参数(default parameters)
function multiply(a, b = 1) {
return a * b;
}
multiply(5); // 5
六、剩余参数和扩展运算符
剩余参数 (rest parameters)
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
一个函数仅能有一个 ...erst,且必须在最后面
扩展运算符(spread operator)
const arr = [1, 2, 3];
console.log(...arr); // 1 2 3
// 合并数组
const newArr = [...arr, 4, 5];
// 复制对象
const newObj = {
...oldObj,
extra: 'value',
};
七、对象字面量增强
const name = 'Alice';
const person = {
// 简写:name: name
name,
age: 25,
greet() {
// 方法简写
console.log(`Hi, I'm ${this.name}`);
},
// 计算属性名
[dynamicKey]: value,
};
八、 Classes (类)
ES6 引入了基于原型的“语法糖”类
class Person {
// 构造函数
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法
greet() {
console.log(`Hello, I'm ${this.name}`);
}
// 静态方法,类方法,不属于实例
static info() {
console.log('This is a Person class');
}
}
// 类继承
class Student extends Person {
constructor(name, age, grade) {
// 调用父级的构造函数
super(name, age);
this.grade = grade;
}
study() {
console.log(`${this.name} is studying`);
}
}
九、 模块化(Modules)
使用模块化有一下的好处:
- 1、代码隔离:每个模块有自己的作用域,避免全局变量的污染
- 2、可维护性:代码结构清晰,便于管理和维护
- 3、可重用性:模块可以在多个项目中重复使用
- 4、依赖管理:明确依赖关系使代码更可靠
- 5、按需加载:可以只按需加载,提高性能
es6 明确了使用 import/export 实现模块化。
导出
// math.js
export const PI = 3.14;
export function add(a, b) {
return a + b;
}
// 或默认导出
export default function() {
...
}
导入
// main.js
import { PI, add } from './math.js';
import calc from './math.js'; // 默认导出
import * as MathUtils from './math.js';
支持 as 重命名,动态导入 import() 等。
十、 Promise
解决地狱循环,实现异步编程
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const time = new Date();
if (time % 2) {
resolve(true);
} else {
reject(false);
}
}, 420);
});
promise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error);
});
静态方法
Promise.resolve()Promise.reject()Promise.all([...])全部成功才算成功promise.race([...])第一个完成即返回
十一、 Map 和 Set
Set 唯一的值集合
const s = new Set([1, 2, 2, 3]);
// Set {1, 2, 3}
console.log(s);
s.add(4);
// Set {1, 2, 3, 4}
console.log(s);
s.add(4);
// Set {1, 2, 3, 4}
console.log(s);
Map 键值对集合(键可以是任何类型)
const m = new Map();
m.set('name', 'Tom');
const objKey = {};
m.set(objKey, 'obj value');
m.get(name); // 'Tom'
十二、 Symbols
原始数据类型,表示第一无二的值
const symbol1 = Symbol('tom');
const symbol2 = Symbol('tom');
// false
console.log(symbol1 === symbol2);
const obj = {
[symbol1]: 'private value',
};
常见内置 Symbol:
Symbol.iteratorSymbol.toStringTagSymbol.hasInstance
十三、 Proxy 和 Reflect
Proxy 拦截对象的操作(读或写)
const obj = {
name: 'Tom',
};
const proxy = new Proxy(obj, {
get(target, prop) {
console.log(`获取 ${prop} 属性`);
return target[prop];
},
});
proxy.name; // 输出: 获取 name 属性
Reflect 统一操作 API
const obj = {
name: 'Tom',
};
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
const originValue = Reflect.get(target, prop, receiver);
console.log(`获取 ${prop} 属性`);
if (originValue !== undefined) {
return target[prop];
}
},
});
proxy.name; // 输出: 获取 name 属性
十四、迭代器和生成器(iterators & generators)
迭代器和生成器将迭代的概念直接代入了核心语言,并提供了一种机制来自定义 for...of 循环行为。
- 迭代器:提供了遍历数据集合的统一接口
- 生成器:使用
function*和yield关键字创建可暂停的函数
迭代器
在 JavaScript 中,迭代器是一个对象,它定义了序列,并在终止时可能附带一个返回值。
在 ES6 之前,JavaScript 遍历数据结构主要依赖 for 循环和 forEach 等方法,这些方法有一个痛点:
- 语法复杂,易出错:使用
for循环需要手动管理索引变量(如i),在嵌套循环中容易出错 - 缺乏统一的接口:不同的数据结构(数组、对象、字符串等)需要不同的遍历方法,代码不统一
- 无法中断:
forEach等方法一旦开始就无法通过break和return中断 - 不支持自定义遍历逻辑:对于非原生数据结构,无法方便的定义其遍历方式
- 无法惰性求值:传统的
for循环会一次加载所有的数据到内存。如果遇到无限序列或超大数集,直接遍历会导致内存溢出
更具体的说,迭代器是通过使用 next() 方法实现迭代器协议的任何一个对象。该对象返回具有两个属性的对象:
- value:迭代序列的下一个值
- done:如果已经迭代到迭代序列的最后一个值,则它为
true。如果value和done一起出现,则它是迭代器的返回值
一旦创建,迭代器对象可以通过重复调用 next() 显式地迭代。迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次:在产生终值之后,对 next() 的额外调用应该继续返回 {done: true}。
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next() {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false };
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true };
},
};
return rangeIterator;
}
使用这个迭代器看起来像这样的:
let it = makeRangeIterator(1, 10, 2);
let result = it.next();
while (!result.done) {
console.log(result.value); // 1 3 5 7 9
result = it.next();
}
console.log(`已迭代序列的大小:${result.value}`); // 5
生成器
虽然自定义迭代器是一个有用的工具,但由于需要显式的维护内部状态,因此创建要格外谨慎。生成器函数(Generator 函数)提供了一个清大的替代选择,它允许你定义一个非连续执行的函数作为迭代算法,生成器函数使用 function* 语法编写。
最初调用时,生成器函数不执行任何代码,而是返回一种称为生成器的特殊迭代器。通过调用 next() 方法消耗该生成器时,生成器函数将执行,直至遇到 yield 关键字。
可以根据需求多次调用该函数,并且每次返回一个行的生成器,但每个生成器只能迭代一次。
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
let iterationCount = 0;
for (let i = start; i < end; i += step) {
iterationCount++;
yield i;
}
return iterationCount;
}
生成器特性
- 暂停与恢复:每次调用
next()方法,函数执行到下一个yield语句暂停; - 传递参数:可以通过
next()方法向生成器的内部传递参数 - 抛出异常:可以通过
throw()方法向生成器内部抛出异常 - 委托生成器:使用
yield*可以讲迭代权委托给另一个生成器
// 向生成器传递参数
function* generatorWithParams() {
const param1 = yield '请输入第一个参数';
const param2 = yield '请输入第二个参数';
return param1 + param2;
}
const generator = generatorWithParams();
console.log(generator.next()); // { done: false, value: '请输入第一个参数' }
console.log(generator.next(10)); // { done: false, value: '请输入第二个参数' }
console.log(generator.next(20)); // { done: true, value: 30 }
// 委托生成器示例
function* generator1() {
yield 1;
yield 2;
}
function* generator2() {
yield* generator1(); // 委托给 generator1
yield 3;
}
for (const value of generator2()) {
console.log(value); // 1, 2, 3
}
生成器的应用场景
- 生成无限序列:如自然数序列、斐波那契数列等
- 异步操作同步化:在
async/await出现前,常用生成器处理异步操作 - 控制流管理:实现复杂的流程控制
- 状态机:维护和切换不同的状态
可迭代对象
若一个对象拥有迭代行为,比如在 for...of 种会循环一些值,那么哪个对象便是一个可迭代的对象。一些内置类型,如 Array 和 Map 拥有默认的迭代行为,而其他类型(比如 Object )则没有。
为了实现可迭代,对象必须实现 [Symbol.iterator]() 方法,这意味着这个对象(或其原型链中任意一个对象)必须具有一个键值为 Symbol.iterator 的属性。
只能迭代一次的可迭代对象(例如生成器)通常从他们的 [Symbol.iterator]() 方法中返回 this ,而那些可以多次迭代的方法必须在每次调用 [Symbol.iterator]() 时返回一个新的迭代器。
function* makeIterator() {
yield 1;
yield 2;
}
const it = makeIterator();
for (const itItem of it) {
console.log(itItem);
}
console.log(it[Symbol.iterator]() === it); // true
// 这个例子向我们展示了生成器(迭代器)是可迭代对象,
// 它有一个 [Symbol.iterator]() 方法返回 it(它自己),
// 因此,it 对象只能迭代*一次*。
// 如果我们将它的 [Symbol.iterator]() 方法改为一个返回新的迭代器/生成器对象的函数/生成器,
// 它(it)就可以迭代多次了。
it[Symbol.iterator] = function* () {
yield 2;
yield 1;
};
自定义可迭代对象
创建:
var myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
};
使用:
for (let value of myIterable) {
console.log(value);
}
// 1
// 2
// 3
[...myIterable]; // [1, 2, 3]
内置可迭代对象
string、Array、TypedArray、Map、Set 都是内置的可迭代对象,因为他们的原型对象上都拥有一个 Symbol.iterator 方法
用于可迭代对象的语法
一些语法和表达式专用于可迭代对象,例如:for...of循环、展开语法、yield*和解构语法。
for (let value of ['a', 'b', 'c']) {
console.log(value);
}
// "a"
// "b"
// "c"
[...'abc']; // ["a", "b", "c"]
function* gen() {
yield* ['a', 'b', 'c'];
}
gen().next(); // { value: "a", done: false }
[a, b, c] = new Set(['a', 'b', 'c']);
a; // "a"
高级生成器
生成器会按需计算他们的 yield ,这使得他们能够高效的表示一个成本很高的序列。
next() 方法也接受一个参数用于修改生成器的内部状态。传递给 next() 的参数会被 yield 接收。
function* fibonacci() {
let current = 0;
let next = 1;
while (true) {
const reset = yield current;
[current, next] = [next, next + current];
if (reset) {
current = 0;
next = 1;
}
}
}
const sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
迭代器和生成器的关系
- 生成器对象自动实现了迭代器接口,因此生成器也是可迭代对象
- 生成器提供了更简洁的方式来创建迭代器,无需动手实现
next()方法 - 迭代器是一种设计模式,而生成器是创建迭代器的语法糖
// 用生成器创建迭代器更简洁
function* createIterator(items) {
for (const item of items) {
yield item;
}
}
// 等价于之前手动创建的迭代器,但代码更简洁