引言
2015 年,ECMAScript 第六版得以发布,这就是众所周知的 ES6 标准,标志着 JavaScript 语言迎来新纪元。ES6 引入了一些新的语法糖,同时弥补了一些在 ES5 中存在的一些缺陷。
本篇笔记是在学习阮一峰老师的《ES6 入门教程》中做的笔记,主要是记录一些自己不太熟悉的或者和 ES5 差异很大的特性。在学习过程中,可以和别的语言(如 Python)进行对比,也可以看到在某些设计思想上,这些语言也是相通的。
简介
- ES5 其实是 ES3 的升级版本,也就是 ES3.1
- ES4 其实并没有发布,但是增加的一些功能在 ES6 中发布了
- 每年 6 月发布新的标准,ES6 泛指下一代 JavaScript 标准
- Babel 转码器:将 ES6 代码转换成 ES5 代码
- Google 的
Traceur
转码器也可以将 ES6 代码转为 ES5 代码
杂记
- 暂时性死区本质:只要一进入当前作用域,所需要使用的变量就已经存在,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
- 块作用域可以任意嵌套
- 块作用域内可以声明函数,但是只能在该作用域下使用
- 浏览器为了兼容历史代码,在块作用域中声明的函数会提升到全局,类似
var
声明的变量了 ES6 支持 6 中声明关键字:
var
function
let
const
import
class
var
,function
命令声明的全局变量,依然是顶层对象的属性;let
,const
,class
声明的全局变量,则不再属于顶层对象的属性。二者需要脱钩- JavaScript 中的顶层对象在各个环境下都是需要存在的,但是在 Node 和浏览器环境中用一套代码想要拿到顶层对象还是有些费劲的。可行的方案如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14(typeof window !== 'undefined'
? window
: (typeof process === 'object' &&
typeof require === 'function' &&
typeof global === 'object')
? global
: this);
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
解构
解构(类似 Python 中的用法,或者 Rust 中的模式匹配):
1
2
3
4
5
6
7
8
9
10let [a, b, c] = [1, 2, 3]
// c 将接收剩余的数组元素
[a, b, ...c] = [1, 2, 3, 4, 5]
[, , c] = [1, 2, 3]
// 不完全解构也是支持的
[a, b] = [1, 2, 3, 4]
// 右值必须要是可迭代的,即实现了 Iterator 接口,否则会出错
[a, b] = NaN // TypeError: undefined is not a function可设置默认值:
let [foo = true] = [];
,只有数组成员严格等于undefined
才会让默认值生效- 解构失败,变量值为
undefined
对象也可以解构:
1
2
3
4
5
6
7
8
9let o = {name: "chris", age: 26}
let { name, age } = o
// 可以将对象赋值给某个已有的变量
const { log } = console
log('hello')
// 可以有别名,name 是模式,n 才是被赋值的变量
({name: n, age: a} = o)对象解构的本质是,先找到同名属性,再赋值给对应的变量
- 解构赋值的原则是,只要右值不是对象或数组,都要先转为对象(如数字会转成 Number);而
undefined
和null
无法转换为对象,所以会报错 - 函数的参数也支持解构赋值
字符串
"\u{xxxx}"
可以表示超过两个字节的 Unicode 字符for (let c of 'foo')
可以直接遍历字符串,并且可以识别大于0xFFFF
的码点String.raw
返回转义字符串:String.includes()
是否存在子串String.startsWith()
+String.endsWith()
String.repeat()
数值
多种进制写法:
- 二进制:
0b010101
- 八进制:
0o123
- 十六进制:
0x1234
- 二进制:
将字符串的多种进制转换成十进制:
Number(target)
- 新增了
Number.isFinite
和Number.isNaN
方法,但这两种方法只对数值有效,其它一律为 false。注意也有一对全局的函数isFinite
和isNaN
,它们则会尝试先将输入的参数转换为 Number 后再进行判断,这是最重要的区别 - ES6 将
parseInt
和parseFloat
挪到Number
上了,这样会更加统一 Number.EPSILON
浮点数计算会有精度损失,这个极小值可以用来指定误差范围- 指数运算符
**
,采用的是右结合的模式 - V8 引擎中,指数运算符和
Math.pow
算法实现是不同的,对于特别大的结果,二者会有细微差别
函数
- 默认参数:
function say(word = 'hi') { console.log(word) }
- 同一作用域中,不能出现同名函数的声明
- 默认参数是惰性计算,每次调用都会重新计算,这点和 Python 非常不同!!
- 函数参数支持解构,且支持默认值
- 指定了参数默认值后,函数的
length
属性只会返回未设置默认值的参数个数 - 如果设置了参数,且不是尾参数,则其之后的参数都不算到
length
了:(function (a = 0, b, c) {}).length // 0
函数如果设置了默认值,则在初始化时会形成一个特殊的作用域:
1
2
3
4
5
6
7const x = 100
// 这里的参数 `x` 和 `y` 处于一个作用域中,故 `y` 的默认值就是指向参数 `x`
function foo(x, y = x) {
console.log(x, y)
}
foo(200) //200, 200ES6 规定,只要函数使用了默认值、解构和扩展运算符,就不能在内部显式指定为严格模式
- 箭头函数(更简洁的匿名函数写法):
- 如果直接返回一个对象,需要将对象用圆括号包围,否则会被解释为代码块:
let getObj = id => ({ id: id })
- 函数体内的
this
是定义时所在的对象,而非使用时所在的对象,也就是在箭头函数中,this
是固定的。本质上是因为在箭头函数中,根本就没有this
,它指向的是外层的this
- 如果直接返回一个对象,需要将对象用圆括号包围,否则会被解释为代码块:
- 不可以作为构造函数,不能对它使用 `new` 命令
- 没有 `arguments` 对象
- 不能使用 `yield`,不可以作为 `Generator`
尾调用:
- 函数式编程中的一个重要概念,指的是函数的最后一步操作(不一定是最后一行,逻辑上是最后一步)是直接调用另一个函数并返回其结果:
1
2
3
4
5
6
7
8function foo(x) { return bar(x) }
// 以下不符合
function foo(x) { let y = bar(x); return y }
function foo(x) { bar(x) }
function foo(x) { return bar(x) + 1 }
- 函数式编程中的一个重要概念,指的是函数的最后一步操作(不一定是最后一行,逻辑上是最后一步)是直接调用另一个函数并返回其结果:
尾调用优化:只保留内层函数的调用帧,节约内存
- ES6 中只要使用了尾递归,就不会出现调用栈溢出,因为只需要维护一个调用帧即可。但其实这个优化只有在严格模式下才会启用。正常模式下,需要通过变量
arguments
,func.caller
来跟踪函数的调用栈
数组
- 数组复制:
const a2 = [...a1]
或者const [...a2] = a1
- 可以将字符串转换为数组:
const a = [...'hello']
- 扩展运算符
...
可以将任何实现了Iterator
接口的对象转换成数组 Array.from
可以接受任意实现了Iterator
接口的对象,转换成数组;也支持 array-like 对象Array.of
将一组值转换为数组,弥补Array
构造函数在参数个数不同时,行为也不同的毛病。完全可以替代Array()
或者new Array()
提供了几个遍历的接口:
keys()
实际就是索引values()
值entries()
键值
includes()
判断是否包含元素flat
用于将嵌套数组展开,拉平,默认只展开一层,可指定多层或者Infinity
flatMap
和flat
类似,但是会对每个元素执行一次回调函数,但它只能展开一层
对象
属性或者方法都可以简写啦:
1
2
3
4
5
6
7
8
9
10
11let [x, y] = [1, 2]
const a = {x, y}
// 等同于
const a = {x: x, y: y}
// 方法的简写
const o = {
doSomething() { return true }
// 常规写法
doSomething: function() { return true }
}允许使用表达式作为对象的属性名:
1
2
3
4
5
6let propKey = 'foo'
let obj = {
[properKey]: true,
['a' + 'bc']: 123
}ES6 中属性遍历的方法:
for...in
:遍历对象自身和继承的可枚举属性(不含 Symbol 属性)Object.keys(obj)
:对象自身可枚举属性(不含 Symbol 属性)Object.getOwnPropertyNames(obj)
:对象自身的所有属性的键名(不含 Symbol 属性)Object.getOwnPropertySymbols(obj)
:对象自身所有Symbol
属性的键名Reflect.ownKeys(obj)
:对象自身所有的键名(无论是 Symbol 属性或者是别的属性,不关心可否枚举)
super
:- 指向对象的原型对象,在这种用法下,只能在对象方法中使用
- 对象方法:必须采用 ES6 方法简写的方式,才会被认定为对象方法
对象解构赋值:
let { x, y, ...z } = { x: 1, y: 2, a: 10, b: 20 }
- 解构赋值必须是最后一个参数
- 右值必须是对象
- 采用的是浅拷贝
Object.is()
:- 更加明确的比较对象是否符合 Same-value equlity
Object.is(NaN, NaN) // true
Object.is(+0, -0) // false
Object.assign()
用于对象合并,就是将源对象所有可枚举的属性复制到目标对象(包括Symbol
属性)。注意点:- 浅拷贝
- 同名属性直接替换,不考虑嵌套
- 由于
Object.assign
只能进行值复制,对于取值函数,则会先取值再复制
Object.assign()
常见用途:- 为对象添加属性:
Object.assign(this, { x, y })
- 为对象添加方法:
Object.assign(Foo.prototype, { barMethod() { ... } })
- 对象克隆:
Object.assign(Object.create(originProto), origin)
- 属性提供默认值:
Object.assign({}, DEFAULTS, options)
- 为对象添加属性:
Symbol
ES6 引入 Symbol 的原因:
- ES5 中,属性名是字符串,容易造成属性名冲突
- Symbol 可以保证每个属性独一无二
Symbol
是新增的原始类型,表示独一无二的值。用于对象的属性。使用Symbol()
函数构造Symbol
接收一个字符串作为参数,表示对Symbol
实例的描述。如果是一个对象参数,则会调用obj.toString()
得到对象的字符串,再生成一个 Symbol 值- 注意点:
- 不可以直接与其它类型值运算,不会隐式转换为 string
- 可以显式转换为 string
symbol.description
可以获取 Symbol 的描述信息作为对象的属性(只能是公开属性):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const name = Symbol('name')
let person = {}
// 第一种定义方式
person[name] = "Foo"
// 获取的方式
person[name]
// 第二种定义方式
let person2 = {
[name]: 'Bar' // 必须要放在方括号中
}
// 第三种定义方式
Object.defineProperty(a, mySymbol, { value: 'Hello!' });可以作为常量,保证值不同
- 可以类似我们在 Python 中那样定义 Enum:
1 | const userType = { |
- 由于不会被常规的方法如
getOwnPropertyNames
等遍历到,但其实又是公开。这种特性可以用来创建一些非私有、仅希望内部调用的方法 Symbol.for
和Symbol.keyFor
:复用相同参数构建的Symbol
,它会在全局注册参数,并在创建前进行搜索,如果存在则返回,否则新建。注意这个全局是全局环境,可以在不同的iframe
和service worker
中取到同样的值:1
2
3
4
5
6let s1 = Symbol.for('s1')
Symbol.for('s1') === s1 // true
Symbol.keyFor(s1) // 's1'
let s2 = Symbol('s2')
Symbol.keyFor(s2) // undefined内置的 Symbol 值,指向语言内部使用的方法:
Symbol.hasInstance
,每个对应可以自定义这个 Symbol 方法,从而让instanceof
运算符按照期望的表现Symbol.isConcatSpreadable
属性,表示在Array.prototype.concat()
时,能否展开Symbol.species
属性,指向一个构造函数。在创建衍生对象时,会使用该属性Symbol.iterator
指向对象的默认遍历器Symbol.toPrimitive
指向转换成原始类型的方法
Set 和 Map 数据结构
Set
构造函数可接受任意Iterable
的对象来看下消除数组中重复元素的方法与 Python 的区别:
1
2
3
4
5
6
7
8// Python 中的写法
list(set([1, 1, 2]))
// JS ES6 中的写法
[...new Set([1, 1, 2])]
// 或者
Array.from(new Set([1, 1, 2)])WeakSet
弱引用计数,只能存放对象。不可遍历Map
的构造:1
2
3
4
5
6// 先看看 Python 中一个类似的构造方式
dict(zip(['name', 'age'], ['chris', 22]))
// 那么在 ES6 中的写法
// 实际上任意 Iterable 的对象,且成员均为双元素的数组的数据结构都可以作为 Map 的构造函数
new Map([['name', 'age'], ['chris', 22]])WeakMap
Proxy
- 属于元编程范畴,可以拦截一些操作,并进行重定义
- 构造函数:
var proxy = new Proxy(target, handler)
要想让
Proxy
起作用,必须要针对其实例进行操作,而非目标对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let handler = {
get (target, name, receiver) {
if (name === "name" || name == "age") {
return Reflect.get(target, name, receiver)
} else {
throw ReferenceError(`property '${name}' not found`)
}
}
}
let user = {
name: "Chris",
age: 26,
}
let proxy = new Proxy(user, handler)
console.log(proxy)
console.log(proxy.age)
console.log(proxy.name)
console.log(proxy.foo)Proxy.revocable
可以返回一个可取消的 Proxy 实例
Reflect
Reflect
也是为了操作对象而提供的新的 API,设计目标:- 将
Object
对象中明显属于内部的方法剥离出来,放到Reflect
对象上 - 修改某些
Object
方法的返回结果,更加方便使用 - 将某些命令式的操作,变成函数,如:
name in obj => Reflect.has(obj, name)
,delete obj[name] => Reflect.deleteProperty(obj, name)
- 与
Proxy
中拦截方法一样,可以替代执行对象的默认行为 Reflect.apply(Math.floor, undefined, [1])
- 将
Promise
- 异步编程解决方案。所谓 Promise,就是一个简单的容器,保存着未来才会结束的事情(通常是一个异步结果)
特点:
- 对象的状态不受外界影响(pending, fulfilled, rejected)
- 一旦状态改变,就不会再变,任何时候都可以得到结果
- 以更加同步的方式编程,易于提供统一的接口,便于维护
缺点:
- 无法取消
- 若不设置回调,Promise 内部抛出错误不会反馈到外部
- pending 状态时,无法得知具体进展情况
Promise.prototype.then()
可接受两个回调,一个是resolved
时的 回调,还有一个是rejected
时的回调。第二个回调可选Promise.prototype.then()
返回的是一個新的Promise
實例Promise.prototype.catch()
是.then(undefined, rejection)
或.then(null, rejection)
的别名,用于指定发生错误时的回调函数- Promise 对象的错误具有冒泡的性质,会一直向后传递,直到被捕获为止。错误总是会被下一个
catch
捕获 Promise.prototype.finally
用于指定不管Promise
对象最终如何,都会执行的操作,ES2018 引入。无法在 finally 中获取 Promise 的状态,它的执行与状态无关Promise.all
:- 参数必须是 Iterable 的对象,元素应当是 Promise 实例
- 状态由成员决定,只有都是
fulfilled
时,整体的状态才会变成fulfilled
- 如果任何一个
rejcted
,则整体整体就是rejected
,并且返回第一个被reject
的实例的返回值
Promise.race
,类似Promise.any
的感觉,就是只要有一个状态改变,整体状态就变化;率先变化的 Promise 实例返回值会传递给p
的回调函数
Iterator & for…of
Iterator 遍历过程:
- 创建一个指向可迭代对象的指针对象
- 调用
next
,可得到第一个成员(返回结果包括两个属性:value
+done
,可分别省略) - 不断调用
next
直到消费完
实现了
Iterator
接口的数据结构,就是可遍历的(Iterable)。示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class RangeIterator {
constructor(start = 0, stop = 100) {
this._value = start
this._stop = stop
}
[Symbol.iterator]() {
return this
}
next() {
let val = this._value
if (val < this._stop) {
this._value++
return {done: false, value: val}
}
return {done: true, value: undefined}
}
}
for (const x of new RangeIterator()) {
console.log(x)
}何时触发 Iterator (即
Symbol.iterator
)接口调用?- 解构赋值
- 扩展运算符
yield*
for...of
Array.from()
Map()
,Set()
,WeakMap()
,WeakSet()
Promise.all()
Promise.race()
可以使用 Generator 函数实现可迭代:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class RangeIterator {
constructor(start = 0, stop = 10) {
this._value = start
this._stop = stop
}
*[Symbol.iterator]() {
while (this._value < this._stop) {
this._value++
yield this._value
}
}
}
for (const x of new RangeIterator()) {
console.log(x)
}遍历器对象除了有
next
方法外,还可以添加一个return
方法,用于处理for...of
提前退出(如出错,或者break
)时要做的事情。而throw
方法一般是配合 Generator 函数使用
Generator 函数
- 定义方式:
function* funcName() { yield 1; return "end" }
- 调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数内部指针
yield
只能用于 Generator 函数中(这个和 Python 不同。在 Python 中,Generator 函数从表明上看和普通函数定义是一样的,只是实现不同)yield
表达式本身无返回值,或者总返回undefined
。next
方法可以带参数,作为yield
表达式的返回值- 第一次带参数调用
next
方法时,参数是无效的。因为next
方法的参数表示上一个yield
表达式的返回值 - 语义上,第一次调用
next
是启动遍历器对象 Generator.prototype.throw()
可以在函数体外向 Generator 函数抛错。如果生成器中没有try...catch
块,错误会传递到外部- 如果一个 Generator 执行过程中出错,且没有内部捕获,则会认为它已经执行结束了
Generator.prototype.return()
可以终止 Generator,如果return
带参数,则会作为最后的返回值。如果 Generator 中含有try...finally
语句块,则会优先执行finally
,延迟返回yield*
表达式可以参考 Python 中的yield from
- 异步编程模式:
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
async 函数
async
是 Generator 语法糖,写异步执行的函数更加便捷改进点:
- 内置执行器,无需
co
之类的模块控制 Generator 执行流程了 - 更好的语义
- 更广的适用性。
await
可以是 Promise 对象,如果是原始类型值,则会自动转换为 resolved Promise 对象 - 返回值是 Promise
- 内置执行器,无需
定义方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 函数声明
async function foo() { }
// 函数表达式
const foo = async function () { }
// 对象的方法
let o = { async foo() { } }
// 类中定义
class Storage {
constructor() {
this.cachePromise = caches.open("avatars")
}
async getAvatar(name) {
const cache = await this.cachePromise
return cache.match(`/avatars/${name}.jpg`)
}
}
const store = new Storage()
store.getAvatar("jake").then().cach()
// 箭头函数
const foo = async () => {}错误处理:如果
await
后面的异步操作出错,等同于async
函数返回的 Promise 对象被 reject。捕获错误的两种方式:1
2
3
4
5
6
7
8
9
10
11
12async function foo() {
try {
await doSomething()
} catch (e) {
console.error(e)
}
}
// 另外一种
async function bar() {
await doSomething().catch(e => { console.error(e) })
}对于不相互依赖的独立操作,不建议分别 await,那样和同步有毛线区别。建议用下面的方式:
1
2
3
4
5
6
7let [foo, bar] = await Promise.all([foo(), bar()])
// 另外一种写法
let fooPromise = foo()
let bar Promise = bar()
let fooResult = await fooPromise
let barResult = await barPromise实现原理:就是将 Generator 函数和自动执行器包装在一起:
1
2
3
4
5
6
7
8
9
10async function fn(args) {
// ...
}
// 等价于
function fn(args) {
return spaw(function *() {
//...
})
}
Class
- ES6 中的 class 本质上还是
Function
对象,所有定义的新方法都是在prototype
上。但是,类内部定义的所有方法都是不可枚举的,这点和 ES5 中直接给prototype
挂载方法不同 - 每个类都必要有
constructor
,如果没有会有一个空的constructor
存在 - 类必须使用
new
调用,否则会报错 this 指向:
- 类的方法内部如果含有
this
,默认指向类的实例。但是如果单独使用,可能出错 - 解决方法有两种,固定
this
:- 采用
bind
- 使用箭头函数
- 采用
- 类的方法内部如果含有
静态方法:
- 使用
static
关键字修饰 - 不会被实例继承和调用(这点和 Python 不同,在 Python 中 static 方法可以被实例调用,而不需要通过类名)
- 如果静态方法中包含
this
,其指向的不是实例,而是类 - 静态方法可以和非静态方法重名
- 父类的静态方法可以被子类继承
- 使用
继承:
- 使用
extends
关键字 - 单继承
- 子类必须要在自定义的
constructor
方法中调用super
,触发父类构造函数调用后,才可以使用this
- 使用
判断一个类是否继承自某个类实例:
Reflect.getPrototypeOf(ColoredPoint) === Point
super
关键字:- 当作函数使用,代表调用父类的构造函数(注意,此时
super
调用后返回的是基类的实例),只能用于构造函数 - 当作对象,在普通方法中指向父类的原型对象;在静态方法中,指向父类
- ES6 规定,在子类普通方法中调用
super.xxx()
时,方法中的this
指向的是当前的子类实例 - 在子类静态方法中通过
super
调用父类方法时,内部的this
指向当前的子类,而非子类的实例
- 当作函数使用,代表调用父类的构造函数(注意,此时
ES5 原生构造函数无法继承(子类无法获得原生构造函数的内部属性)。具体原因是 ES5 中,是新建子类的实例对象
this
,再将父类的属性添加都子类上;而父类的内部属性无法获取,故无法继承:- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
ES6 则允许继承上述原生构造函数了。原因是 ES6 中,是先新建父类的实例对象
this
,再用子类的构造函数修饰this
,使得父类的行为可以继承
模块
- 早期 JS 无模块管理机制,不利于开发大型项目
- 社区提供了 CommonJS 和 AMD 两种方案,分别用于服务端和浏览器端;二者都是运行时确定模块依赖
- ES6 模块的设计思想是尽可能静态化,编译时确定模块依赖关系及输入输出变量
import
使用注意:import { foo } from 'mod'
- 导入的变量都是只读的
- 导入的路径既可以是相对路径,也可以是绝对路径
.js
后缀可省略- 如果只是模块名称,无路径则需要配置文件告知 JS 引擎具体的模块位置
- 具有提升的效果
- 静态执行,无法使用表达式和变量
- Single 模式,有缓存机制
- 整体加载带别名:
import * as mod from 'longNameMod'
export default
本质上是输出一个叫做default
的变量和方法,然后系统允许重名- 在一条语句中同时输入默认方法和其它接口:
import _, { each } from 'lodash'
- 复合写法:
export { foo, bar } from 'my_mod'
,注意不会在当前模块导入这两个变量,而只是转发到外面了
模块加载规则
浏览器中可使用的方式:
<script src="path/to/foo.js" [async/defer]/>
- ES6 模块加载(默认就是 defer 模式):
<script type="module" src="./mod.js"/>
<script type="model">
/可以正常写 import xx from xxx 这种,模块作用域/`
ES6 与 CommonJS
- 前者输出是值引用,后者输出的是值拷贝
- 前者是编译时输出接口,后者是运行时加载
- 前者是动态引用,后者会缓存值
- 在 Node 中
import
是异步加载 - ES6 模块中,顶层
this
指向undefined
;后者则指向当前模块
二进制数组(类数组)
二进制数组组成对象:
ArrayBuffer
对象:代表内存中的一段二进制数据,可通过「视图」操作TypedArray
视图:可用于读写简单的二进制数据,包括 9 种类型视图:Int8
Uint8
Uint8C
:自动过滤溢出,DataView
视图不支持Int16
Uint16
Int32
Uint32
Float32
Float64
DateView
视图:可自定义复合格式的视图,读写复杂类型的二进制数据