ES6语法速通(必学知识点总结)

235 阅读8分钟

常用语法

  1. 模板字符串:允许嵌入表达式的字符串字面量
let a = 'Hello'
let b = `${a}, World`
b === 'Hello, World' // true
  1. 默认参数:在没有值或undefined被传入时使用默认形参。
const add = (x = 10, y = 20) => { console.log( x + y ) }
  1. 剩余参数:接收不定数量的参数合并为一个数组。
const add = (...args, x, y) => { console.log(args) }
  1. 展开运算符:将数组表达式和string在语法层面展开,还可以将对象表达式按 key-value 的方式展开。
// 数组展开
let array = [1, 2, 3]
let newArray = [ ...array ]
// 对象展开
let object = { name: '小红', age: 24 }
let newObject = { ...object }
  1. 可选链运算符:允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。当访问的对象引用不存在时,返回undefined。可解决因对象引用不存在而报错中止程序的情况。
let list = {
    friends: [
        { name: '小明', age: 26 },
        { name: '小华', age: 26 }
    ]
}
console.log(a.friends[3]?.name)

其中a.friends[3]?.name相当于以下三元表达式:

(a.friends[3] === undefined || a.friends[3] === null)
    ? undefined : a.friends[3].name
  1. 解构赋值:将数组中的值或对象的属性值取出,赋值给其他变量。
let object = { name: '小华', age: 24 }
const { name, age } = object
console.log(`${name} ${age}岁`) // 输出:小华 24岁

不带声明使用解构赋值表达式,需要注意代码风格问题。

// 数组解构赋值
let a = 20, b = 40
;[a, b] = [b, a]
console.log(a, b)
// 对象解构赋值
let c = { a: 1, b: 2 }
;({ a, b } = c) // 必须带括号作为独立的表达式
console.log(a, b)

说明:编码风格不尾随分号:但在以 “(“、”[“ 、”/“、”+”、”-“ 开头的语句前面必须加分号。

  1. 属性简写:定义的变量可以作为键值对放入构建的对象中,变量名作为键名变量的值作为键值
let name =  '小华'
let age =  '24'
let object = { name, age } // 输出:{name: '小华', age: '24'}
  1. for…of可迭代对象)数组、类数组、字符串、Map和Set等等。
// obj[Symbol.iterator] === 'function' 检测对象是否可迭代
for (let value of array) { ... } // let定义可修改value的值,进而改变数组中的元素
for (const value of array) { ... } // 其中value为数组中的元素
  1. for…in对象)迭代对象除Symbol以外的可枚举属性,一般用于检测对象属性
for(const key in object){ ... } // 其中key为对象的可枚举属性

数据结构和类型

BigInt:用于表示任意大的整数。BigInt类型转换方法如下:

// 可以数值后追加字母n,直接转化为BigInt类型。
typeof (1n) === 'bigint' // true
// 利用BigInt()函数生成,传入数值作为参数即可。
typeof (BigInt(1)) === 'bigint' // true

Symbol:通过 Symbol()函数返回的值都是唯一的。使用方法和场景如下:

方法说明
Symbol.for(key)在全局Symbol注册表返回与key对应的Symbol值,否则在全局Symbol注册表中重新创建这个值
Symbol.keyFor(sym)返回全局 symbol 注册表与key对应的Symbol值,否则返回undefined
Symbol.prototype.description返回Symbol对象的描述,在全局 symbol 注册表作为key值存在
Symbol.prototype.valueOf返回Symbol对象的原始值
Symbol.prototype[@@toPrimitive]转化Symbol对象为原始值
Object.getOwnPropertySymbols()返回对象自身所有Symbol属性的数组
  1. 作为对象属性名称
let name = Symbol('1');
let object = {
  [name]: '小华'
}
console.log(object[name])
// 注意:Symbol值作为对象属性名时,只能通过方括号访问,不能通过点运算符访问。
  1. 减少魔法字符串
// 定义红黄绿灯,作为状态记录
let red = Symbol()
let yellow = Symbol()
let green = Symbol()

Set:由**序号+值(任何类型)**作为基本单元组成的可迭代对象,其内部元素唯一。

方法说明
Set.prototype.add(value)Set对象尾部添加一个元素。返回该 Set 对象。
Set.prototype.delete(value)返回一个布尔值,判断 Set 对象中是否移除指定的value
Set.prototype.has(value)返回一个布尔值,表示该值在 Set 对象中是否存在。

WeakSet:由序号+值(仅object类型)作为基本单元组成的不可迭代对象,其内部元素唯一。

方法说明
WeakSet.prototype.add(value)WeakSet对象尾部添加一个元素。返回该WeakSet 对象。
WeakSet.prototype.delete(value)返回一个布尔值,判断WeakSet对象中是否移除指定的value
WeakSet.prototype.has(value)返回一个布尔值,表示该值在WeakSet 对象中是否存在。

Map:由序号+键值对作为基本单元组成的可迭代对象。

产生的原因:解决普通对象只能由字符串作为属性的特性,Map可以由不同类型的值作为键值对存储。

方法说明
Map.prototype.get(key)返回 Map 对象中指定的键 key 对应的值,否则返回undefined
Map.prototype.set(key, value)接收两个参数keyvalue,作为键值对存储在 Map 对象中
Map.prototype.has(key)返回一个布尔值,判断Map 对象中是否存在key对应的键
Map.prototype.delete(key)返回一个布尔值,判断 Map 对象中是否移除指定的键值对

WeakMap:由序号+键值对作为基本单元组成的不可迭代对象

注意:WeakMap中的键值对只能由object类型(非null类型)作为键,而值可以是任何类型。

方法说明
WeakMap.prototype.get(key)返回 WeakMap 对象中指定的键 key 对应的值,否则返回undefined
WeakMap.prototype.set(key, value)接收两个参数keyvalue,作为键值对存储在WeakMap 对象中
WeakMap.prototype.has(key)返回一个布尔值,判断WeakMap 对象中是否存在key对应的键
WeakMap.prototype.delete(key)返回一个布尔值,判断WeakMap 对象中是否移除指定的键值对

问题:如何理解WeakMap和WeakSet的弱引用?

当WeakMap的键(对象)和WeakSet中存储的对象都不再被其他变量强引用,那么这些对象对应的内存会被垃圾机制所回收,进而在WeakMap和WeakSet中随之消失。也就是说,弱引用会随着对象所有的强引用解除而被自动解除,而不必手动解除。

根据阮一峰老师ES6教程中的例子,可以使用node命令行可以验证上述行为。具体过程如下:

$ node --expose-gc
global.gc();
process.memoryUsage(); // 内存初始占用为5M

let wm = new WeakMap();
let key1 = new Array(5 * 1024 * 1024); // key1强引用
let key2 = key1; // key2强引用
let key3 = key2; // key3强引用

wm.set(key3, 1);
process.memoryUsage(); // 内存开始占用47M

key3 = null; // 解除第一个强引用
global.gc();
process.memoryUsage(); // 内存依然占用47M

key2 = null; // 解除第二个强引用
global.gc();
process.memoryUsage(); // 内存依然占用47M

key1 = null; // 此时所有强引用都被解除
global.gc(); // 再次执行垃圾回收
process.memoryUsage(); // 可以看到内存占用恢复为5M,说明弱应用也被解除

对象代理和反射

Proxy:用于拦截目标对象的取值和赋值操作进行过滤和改写。

方法说明
handler.get(target, propKey, receiver)属性读取操作的捕捉器。
handler.set(target, propKey, value, receiver)属性设置操作的捕捉器。
handler.has(target, propKey)in 操作符的捕捉器。

Reflect:为可拦截JavaScript的操作提供方法,与Proxy中的方法对应。

方法说明
Reflect.get(target, propKey, receiver)获取对象身上某个属性的值,类似于 target[name]
Reflect.set(target, propKey, value, receiver)将值分配给属性。若更新成功返回true。否则返回false
Reflect.has(target, propKey)判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。

Proxy和Reflect通常搭配进行使用,完成对象的一些默认行为。例程如下:

const obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting: ${propKey}`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting: ${propKey}`);
    return Reflect.set(target, propKey, value, receiver);
  }
});
obj.name = '小华' // 触发obj.name的set操作,然后通过Reflect.set进行赋值操作并返回true
console.log(obj.name) // 触发obj.name的get操作,然后通过Reflect.get进行取值操作并返回对象的值

迭代协议和生成器函数

迭代器协议:定义了产生一系列值(有限个或无限个)的标准方式。

迭代器对象具有next方法,返回一个 IteratorResult 对象(具有valuedone属性或其中之一)

一般情况下,next方法返回一个具有{value: 任何类型的值, done: 布尔值}结构的对象

value:未迭代完成时可以是任何类型的值,迭代完成后为undefined

done:未迭代完成时为false,迭代完成后为true

根据上述的迭代器协议,实现一个基本的迭代器对象。如下:

const iter = {
    next: function () {
        return { value: undefined, done: true }
    }
}

似乎没什么用,我们需要加以改造。设定一个倒计时,让它能够动起来。如下:

const iter = {
    index: 3,
    next: function () {
        return this.index >= 1 ?
            { value: this.index--, done: false } :
            { value: '倒计时结束', done: true }
    }
}
iter.next() // {value: 3, done: false}
iter.next() // {value: 2, done: false}
iter.next() // {value: 1, done: false}
iter.next() // {value: '倒计时已结束', done: true}

还有一种用法:通过闭包获取外部变量进行value的值更改。不局限于迭代器对象内部属性值的迭代。

function makeIterator(array) {
    let index = 0;
    return {
        next: function () {
            return index < array.length ?
                { value: array[index++], done: false } :
                { value: '倒计时结束', done: true }
        }
    };
}
const iter = makeIterator([3, 2, 1]); // 可以将任何类型的值放入数组
iter.next() // {value: 3, done: false}
iter.next() // {value: 2, done: false}
iter.next() // {value: 1, done: false}
iter.next() // {value: '倒计时已结束', done: true}

生成器函数:生成器函数使用 function*语法编写。调用时返回一个称为 Generator 的迭代器对象

这个迭代器对象每次调用next方法时,将yield后面的值作为value返回一个IteratorResult 对象。

function* makeIterator() {
    for (let i = 3; i >= 1; i--) {
        yield i;
    }
}
var a = makeIterator() // 函数此时已经执行,遇到yield关键字冻结
// 调用返回迭代器的next方法,函数恢复执行
a.next() // {value: 3, done: false}
a.next() // {value: 2, done: false}
a.next() // {value: 1, done: false}
a.next() // {value: undefined, done: true}

说明:生成器函数在遇到yield关键字时会冻结,需要通过其返回的迭代器对象调用next方法才能恢复执行。

生成器传值:生成器函数返回的迭代器对象在第一次调用next方法传参无效。

function* gen() {
    while (true) {
        let value = yield null;
        console.log(value);
    }
}
const g = gen();
g.next(1); // 第一次传参无效,value并无赋值也无输出
g.next(2); // 控制台输出2
g.next(3); // 控制台输出3

可迭代协议:允许 JavaScript 对象定义或定制它们的迭代行为。

可迭代对象必须具备[Symbol.iterator]属性,其值作为方法会不带参数进行调用进而返回一个迭代器。

对象属性[Symbol.iterator]的值是一个函数,可以是以下两种函数:

  1. 普通函数(必须返回一个符合迭代协议的对象即迭代器对象)。

  2. 生成器函数(通过yield提供遍历的元素)。

根据上述可迭代协议,分别通过普通函数生成器函数实现一个可迭代对象。代码如下:

  1. 使用**生成器函数(通过yield提供遍历的元素)**实现可迭代对象如下:
const map = {
    name: '小华',
    age: 24,
    [Symbol.iterator]: function* () {
        let index = 0
        const keys = Object.keys(this) // this指向map
        while (index < keys.length) {
            yield [keys[index], this[keys[index]]];
            index++
        }
    }
};
[...map] // 输出:[['name', '小华'], ['age', 24]]
  1. 使用**普通函数(返回一个迭代器对象)**实现可迭代对象如下:
const map = {
    name: '小华',
    age: 24,
    [Symbol.iterator]: function () {
        let keyIndex = 0, valueIndex = 0
        const keys = Object.keys(this) // this指向map
        return {
            next: () => {
                return keyIndex < keys.length ? {
                    value: [keys[keyIndex++], this[keys[valueIndex++]]],
                } : { done: true }
            }
        }
    }
};
[...map] // 输出:[['name', '小华'], ['age', 24]]
[...map] // 输出:[['name', '小华'], ['age', 24]]

常见的默认迭代场景:JavaScript默认调用Symbol.iterator方法对数据进行迭代。

  1. 解构赋值:[...map]

  2. 扩展运算符:const [name, age] = map

  3. yield*:yield* [1,2,3]

  4. for...of...:for value of map

  5. 内置方法:Array.from(), Map(), Set(), WeakMap(), WeakSet(), Promise.all(), Promise.race()

注意:以上所有默认迭代场景,迭代器对象在迭代完成后必须返回一个{ done: true }终止迭代

async/await关键字

说明:async/await是基于生成器函数的语法糖,一般用于执行异步操作。

生成器函数特点:

  1. 函数返回一个称为 Generator 的迭代器对象。

  2. yield表达式返回 IteratorResult 对象的value值。

  3. 只能手动调用next方法执行下一步。

function* test() {
    const x = yield 1
    console.log(x) // value的值
}
let iter = test() // 返回一个迭代器对象
iter.next()

async/awiat关键字特点:

  1. 函数返回一个Promise对象。

  2. await表达式返回Promise解决的值。

  3. 内置自动执行器,执行下一步。

async function test() {
    const x = await 1
    console.log(x) // resolve的值。若出现reject函数会中断执行
}
test() // 返回一个状态已完成的Promise对象

注意:async函数内的返回值会被隐式地包装在一个 promise 中。

// 函数存在return某个值的情况
async function foo() {
    return 1
}// 等价于以下函数
function foo() {
    return Promise.resolve(1)
}

函数体内有一个 await 表达式,async 函数就一定会异步执行。

async function foo() {
    await 1
}// 等价于以下函数
function foo() {
    return Promise.resolve(1).then(() => undefined)
}

结合前面所学知识,为生成器函数实现一个async/awiat的自动执行器。代码如下:

function async(ite) { // // 接收生成器函数执行后所返回的迭代器对象
    return new Promise((resolve, reject) => {
        function go(nextFn, next = null) {
            try {
                next = nextFn()
            } catch (err) {
                reject(err)
            }
            if (next.done) {
                return resolve(next.value)
            }
            Promise.resolve(next.value).then((value) => {
                go(() => ite.next(value))
            })
        }
        go(() => ite.next())
    })
}

自动执行器的测试用例如下:

function request(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value)
        }, 1000)
    })
}
function* generator(a, b) {
    console.log(`传参测试:${a}, ${b}`)
    const one = yield request('第一个请求成功')
    console.log(one)
    const two = yield request('第二个请求成功')
    console.log(two)
    const three = yield request('第三个请求成功')
    console.log(three)
    return three // 作为promise的结果值
}
let promise = async(generator(1, 2)) // 将模拟async/await的函数调用过程

模块化方法

说明:ES6的模块化可以使得不同的JS文件之间相互传递变量

具名接口:通过export方法导出的模块,import时的模块名称固定,并且需要通过{}花括号接收模块。

// module.js文件
export function foo(){...} // export后可以接声明变量
// index.js文件
import { foo } from './module.js'

默认接口:通过export default导出的模块,import可任意指定名称,无须通过{}花括号接收模块。

// module.js文件
export default foo // export default后不能接声明变量
// index.js文件
import newFoo from './module.js'

模块整体加载:导出JS文件中的所有模块,且必须添加as关键字定义变量名

import * as object from './module.js'

import和export复合写法

export { foo } from './module.js'
export { default } from './module.js'

export { default as foo } from './module.js' // 默认接口改为具名接口
export { foo as default } from './module.js' // 具名接口改为默认接口

export * from './module.js' // 注意:仅导出所有具名接口,不包含默认接口
export * as object from './module.js' // 包含全部接口
export * as default from './module.js' // 包含全部接口

对象方法

方法说明
Object.keys()返回一个包含所有给定对象自身可枚举属性名称的数组。
Object.values()返回给定对象自身可枚举值的数组。
Object.entries()返回给定对象自身可枚举属性的 [key, value] 数组。
Object.assign()通过复制一个或多个对象来创建一个新的对象。
Object.getPrototypeOf()返回指定对象的原型对象。
Object.setPrototypeOf()设置对象的原型(即内部 [[Prototype]] 属性)。
Object.hasOwn()如果指定的对象自身有指定的属性,则返回 true;否则返回 false

class语法

先来看一下ES5和ES6面向对象的不同写法。实现分别如下:

ES5面向对象写法

function inheritPrototype(subType, superType) {
    subType.prototype = Object.create(superType.prototype)
    subType.prototype.constructor = subType
}
function USER(username, password) {
    this.username = username
    this.password = password
    this.weapon = ['水果小刀']
}
inheritPrototype(VIP, USER)
function VIP(username, password) {
    USER.call(this, username, password)
}
VIP.prototype.addWeapon = function (weaponName) {
    this.weapon.push(weaponName)
}

ES6面向对象写法

class USER {
    constructor(username, password) {
        this.username = username
        this.password = password
        this.weapon = ['水果小刀']
    }
}
class VIP extends USER {
    constructor(username, password) {
        super(username, password)
    }
    addWeapon(weaponName) {
        this.weapon.push(weaponName)
    }
}

接下来采用对比式学习的方法,解析ES5和ES6面向对象写法的差异。

类的编写

constructor()构造器

// ES6写法:new USER()后执行constructor()构造器方法
class USER {
    constructor(username, password) {
        this.username = username
        this.password = password
        this.weapon = ['水果小刀']
    }
}
// ES5写法:new USER()后执行USER()构造函数方法
function USER(username, password) {
    this.username = username
    this.password = password
    this.weapon = ['水果小刀']
}

类添加方法

class VIP extends USER {
    constructor(username, password) {
        super(username, password)
    }
    // ES6写法:直接在class内部写入方法即可
    addWeapon(weaponName) {
        this.weapon.push(weaponName)
    }
}斐波那契数列


// ES5写法:在构造函数原型上添加方法
VIP.prototype.addWeapon = function (weaponName) {
    this.weapon.push(weaponName)
}

类的继承

子类继承父类方法(extends关键字)

// ES6写法:通过extends实现父类方法继承
class VIP extends USER {}

// ES5写法:传递原型链__proto__实现继承
function inheritPrototype(subType, superType) {
    subType.prototype = Object.create(superType.prototype)
    subType.prototype.constructor = subType
}
inheritPrototype(VIP, USER)

子类继承父类属性(super关键字)

class VIP extends USER {
    constructor(username, password) {
        // ES6写法:通过super继承父类属性
        super(username, password)
    }
}
function VIP(username, password) {
    // ES5写法:通过call方法调用父类构造函数继承父类属性
    USER.call(this, username, password)
}

getter和setter

getter和setter方法拦截实例对象属性的存取行为,操作如下:

class USER {
    constructor(username, password) {
        this.username = username
        this.password = password
    }
    get username() {
        console.log('getter')
    }
    set username(value) {
        console.log(`setter: ${value}`)
    }
}
new USER('小红').username

静态属性和方法

定义方法:在属性或方法名称的前面加入static关键字表示为静态属性或方法。

class Foo {
    static string = "Hello, World"
    static getClassString() {
        return Foo.string
    }
}
Foo.getClassString() // 输出:"Hello, World"

注意:静态属性和方法定义在类本身,而不是定义在实例对象上。也就是说,只能通过类名调用静态属性和方法。此外,父类的静态属性和方法可以通过extends关键字被子类所继承。

私有属性和方法

定义方法:在属性或方法名称的前面加入#表示为私有属性或方法。

class Count {
    #a = 1
    #b = 1
    #sum() {
        return this.#a + this.#b
    }
    add() {
        return this.#sum()
    }
}
new Count().add() // 输出:2

注意:私有属性和方法只能在类的内部访问或调用。此外,父类私有的属性和方法不会被子类所继承。


参考

MDN Web Docs

ES6标准入门(第3版)