常用语法
- 模板字符串:允许嵌入表达式的字符串字面量
let a = 'Hello'
let b = `${a}, World`
b === 'Hello, World' // true
- 默认参数:在没有值或
undefined被传入时使用默认形参。
const add = (x = 10, y = 20) => { console.log( x + y ) }
- 剩余参数:接收不定数量的参数合并为一个数组。
const add = (...args, x, y) => { console.log(args) }
- 展开运算符:将数组表达式和
string在语法层面展开,还可以将对象表达式按 key-value 的方式展开。
// 数组展开
let array = [1, 2, 3]
let newArray = [ ...array ]
// 对象展开
let object = { name: '小红', age: 24 }
let newObject = { ...object }
- 可选链运算符:允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。当访问的对象引用不存在时,返回
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
- 解构赋值:将数组中的值或对象的属性值取出,赋值给其他变量。
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)
说明:编码风格不尾随分号:但在以 “(“、”[“ 、”/“、”+”、”-“ 开头的语句前面必须加分号。
- 属性简写:定义的变量可以作为键值对放入构建的对象中,变量名作为键名,变量的值作为键值。
let name = '小华'
let age = '24'
let object = { name, age } // 输出:{name: '小华', age: '24'}
- for…of(可迭代对象)数组、类数组、字符串、Map和Set等等。
// obj[Symbol.iterator] === 'function' 检测对象是否可迭代
for (let value of array) { ... } // let定义可修改value的值,进而改变数组中的元素
for (const value of array) { ... } // 其中value为数组中的元素
- 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属性的数组 |
- 作为对象属性名称
let name = Symbol('1');
let object = {
[name]: '小华'
}
console.log(object[name])
// 注意:Symbol值作为对象属性名时,只能通过方括号访问,不能通过点运算符访问。
- 减少魔法字符串
// 定义红黄绿灯,作为状态记录
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) | 接收两个参数key和value,作为键值对存储在 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) | 接收两个参数key和value,作为键值对存储在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 对象(具有value和done属性或其中之一)
一般情况下,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]的值是一个函数,可以是以下两种函数:
-
普通函数(必须返回一个符合迭代协议的对象即迭代器对象)。
-
生成器函数(通过
yield提供遍历的元素)。
根据上述可迭代协议,分别通过普通函数和生成器函数实现一个可迭代对象。代码如下:
- 使用**生成器函数(通过
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]]
- 使用**普通函数(返回一个迭代器对象)**实现可迭代对象如下:
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方法对数据进行迭代。
-
解构赋值:
[...map] -
扩展运算符:
const [name, age] = map -
yield*:
yield* [1,2,3] -
for...of...:
for value of map -
内置方法:
Array.from(),Map(),Set(),WeakMap(),WeakSet(),Promise.all(),Promise.race()
注意:以上所有默认迭代场景,迭代器对象在迭代完成后必须返回一个{ done: true }以终止迭代。
async/await关键字
说明:async/await是基于生成器函数的语法糖,一般用于执行异步操作。
生成器函数特点:
-
函数返回一个称为 Generator 的迭代器对象。
-
yield表达式返回IteratorResult对象的value值。 -
只能手动调用next方法执行下一步。
function* test() {
const x = yield 1
console.log(x) // value的值
}
let iter = test() // 返回一个迭代器对象
iter.next()
async/awiat关键字特点:
-
函数返回一个Promise对象。
-
await表达式返回Promise解决的值。 -
内置自动执行器,执行下一步。
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
注意:私有属性和方法只能在类的内部访问或调用。此外,父类私有的属性和方法不会被子类所继承。
参考