【前端学习】ECMAScript 6入门
ECMAScript 6入门
let和const命令
- let命令声明的变量只在let命令所在的代码块内有效
- let所声明的变量一定要在声明后使用,否则报错
- 块级作用域的出现,实际上使得广泛应用的匿名立即执行函数表达式(匿名IIFE)不再必要了
- const只能保证指向实际数据的指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了
- ES6规定,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性
变量的解构赋值
- 对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,而对象的属性没有次序
- 对象的解构赋值可以取到继承的属性
- 字符串也可以解构赋值,类似数组的对象都有一个length属性,还可以对这个属性解构赋值
- 解构赋值的规则是:只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错
- 函数的参数也可以使用解构赋值,下面是一个使用默认值的函数参数解构例子:
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
- 可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号
- 变量的解构赋值可以用于从函数返回多个值:
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
字符串的扩展
- ES6为字符串添加了遍历器接口,使得字符串可以被
for...of
循环遍历
for (let codePoint of 'foo') {
console.log(codePoint)
}
- 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中
- 模板字符串中,可以使用
<%...%>
放置JavaScript代码,使用<%= ... %>
输出JavaScript表达式
let template = `
<ul>
<% for (let i = 0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`
- 模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。如果模板字符里面有变量,就会先将模板字符串处理成多个参数,再调用函数
let a = 5;
let b = 10;
function tag(s, v1, v2) {
console.log(s[0]);
console.log(s[1]);
console.log(s[2]);
console.log(v1);
console.log(v2);
return 'OK';
}
tag`Hello ${ a + b } world ${ a * b }`;
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"
字符串的新增方法
codePointAt()
方法是测试一个字符由两个字节还是由四个字节组成的最简单方法- ES6提供了新的字符串方法
includes()
、startsWith()
和endsWith()
- ES2017引入了字符串补全长度的功能,
padStart()
用于头部补全,padEnd()
用于尾部补全 - ES2019对字符串实例新增了
trimStart()
和trimEnd()
两个方法,用于消除空格
正则的扩展
- ES6可以使用第二个参数为正则对象指定修饰符,但返回的正则表达式会忽略原有正则表达式的修饰符,只使用新指定的修饰符
- ES6对正则表达式添加了
u
修饰符,含义为“Unicode模式”,用来正确处理大于\uFFFF
的Unicode字符。利用这一点,可以写出一个正确返回字符串长度的函数
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
var s = '𠮷𠮷';
s.length // 4
codePointLength(s) // 2
- ES6对正则表达式添加了
y
修饰符,它的设计本意就是让头部匹配的标志^
在全局匹配中都有效 - ES6新增了
dotAll
模式,即点(dot)代表一切字符 - 先行断言指的是,
x
只有在y
前面才匹配,必须写成/x(?=y)/
,先行否定断言指的是,x
只有不在y
前面才匹配,必须写成/x(?!y)/
。后行断言正好相反,x
只有在y
后面才匹配,必须写成/(?<=y)x/
。后行否定断言指的是x
只有不在y
后面才匹配,必须写成/(?<!y)x/
,下面是几个例子:
/\d+(?=%)/.exec('100% of US presidents have been male') // ['100']
/\d+(?!%)/.exec('that\'s all 44 of them') // ['44']
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ['100']
/(?<!\$)\d+/.exec('it\'s worth about €90') // ['90']
- 后行断言的组匹配,与正常情况下结果是不一样的,其反斜杠引用也与通常的顺序相反,必须放在对应的那个括号之前
- ES2018引入了一种新的类的写法
\p{...}
和\P{...}
,允许正则表达式匹配符合Unicode某种属性的所有字符 - ES2018引入了具名组匹配,允许为每一个组匹配指定一个名字。字符串替换时,使用
$<组名>
引用具名组
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31
数值的扩展
- ES6在
Number
对象上,新提供了Number.isFinite()
和Number.isNaN()
两个方法 Number.EPSILON
可以用来设置“能够接受的误差范围”,它的实质是一个可以接受的最小误差范围
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true
1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true
- ES2016新增了一个指数运算符(
**
),它的一个特点是右结合,而不是常见的左结合 - ES2020引入了一种新的数据类型BigInt(大整数),只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示,但它不能与普通数值进行混合运算
函数的扩展
- ES6允许为函数的参数设置默认值,即直接写在参数定义的后面
- 参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
- 参数默认值可以与解构赋值的默认值结合起来使用
// 写法一
// 函数参数默认值是空对象,但是设置了对象解构赋值的默认值
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
// 函数参数默认值是有具体属性的对象,但是没有设置对象解构赋值的默认值
function m2({x, y} = {x: 0, y: 0}) {
return [x, y];
}
// 函数没有参数的时候
m1() // [0, 0]
m2() // [0, 0]
// x和y都有值的时候
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x有值,y无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x和y都无值的情况
m1({}) // [0, 0]
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
- 通常情况下,定义了默认值的参数,应该是函数的尾参数
- ES6引入rest参数(形式为
...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中 - 箭头函数体内的
this
对象,就是定义时所在的对象,而不是使用时所在的对象。this
对象的指向是可变的,但是在箭头函数中,它是固定的 - 以下两个场合不应该使用箭头函数:
- 定义对象的方法,且该方法内部包括
this
- 需要动态this的时候
数组的扩展
- 扩展运算符(spread)是三个点(
...
)。它好比rest参数的逆运算,将一个数组转为用逗号分割的参数序列。它主要用于函数调用
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
add(...numbers) // 42
- 扩展运算符还可以将字符串转为真正的数组,如
[...'hello']
。这有一个重要的好处,那就是能够正确识别四个字节的Unicode字符 Array.from
方法支持类似数组的对象和没有部署遍历器接口(Symbol.iterator
)的对象。这些对象都可以通过Array.from
方法转为数组,而此时扩展运算符就无法转换indexOf
方法无法识别数组的NaN
成员,但是findIndex
方法可以借助Object.is
方法左到:
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
- ES6明确将空位转为
undefined
- ES2019明确规定,
Array.prototype.sort()
的默认排序算法必须稳定。这个规定已经做到了,现在JavaScript各个主要实现的默认排序算法都是稳定的
对象的扩展
- ES6又新增了一个类似的关键字
super
,指向当前对象的原型对象 - 对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }
x // 1
y // 2
z // { a:3, b: 4 }
- 对象的扩展运算符等同于使用
Object.assign()
方法 - 如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法:
// 写法一
const clone1 = {
__proto__: Object.getPrototypeOf(obj),
...obj
}
// 写法二
const clone2 = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
obj
);
// 写法三
const clone3 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
- 扩展运算符的参数对象之中,如果有取值函数
get
,这个函数是会执行的 - ES2020引入了链判断运算符
?.
,简化层层运算
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value;
对象的新增方法
Object.assign()
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)Object.getOwnPropertyDescriptors()
方法的引入目的,主要是为了解决Object.assign()
无法正确拷贝get
属性和set
属性的问题。它配合Object.defineProperties()
方法就可以实现正确拷贝
const shallowMerge = (target, source) => Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);
Object.getOwnPropertyDescriptors()
方法的另一个用途,是配合Object.create()
方法,将对象属性克隆到一个新对象,这属于浅拷贝
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
Object.getOwnPropertyDescriptors()
方法可以实现一个对象继承另一个对象
const obj = Object.create(
prot,
Object.getOwnPropertyDescriptors({
foo: 123,
})
);
Symbol
- 每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块组成的情况非常有用,能防止某一个键被不小心改写或覆盖
- Symbol作为属性名,遍历对象时不会出现在
for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。但它也不是私有属性,有一个Object.getOwnPropertySymbols()
方法可以获得指定对象所有Symbol属性名 Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和Symbol键名
Set和Map数据结构
- 向Set加入值时认为
NaN
等于自身,而精确相等运算符认为NaN
不等于自身 - Set结构的键名就是键值(两者是同一个值)
- WeakSet的成员会随时消失,不适合引用。同时,WeakSet不可遍历
- Map结构提供了“值——值”的对应,是一种更完善的Hash结构实现
Proxy
- Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程
- 要使得
Proxy
起作用,必须针对Proxy
实例进行操作,而不是针对目标对象进行操作。下面是一个用Proxy
进行拦截的实例:
var 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.count = 1
// setting count!
++obj.count
// getting count
// setting count
// 2
has
方法用来拦截HasProperty
操作,即判断对象是否具有某个属性时,这个方法会生效,典型的操作就是in
运算符。下面的例子使用has
方法隐藏某些属性,不被in
运算符发现:
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
- 虽然
for...in
循环也用到了in
运算符,但是has
拦截对for...in
循环不生效 Proxy.revocable()
方法返回一个可取消的Proxy实例。它的一个使用场景是目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
- 在Proxy代理的情况下,目标对象内部的this关键字会指向Proxy代理
Reflect
Reflect
对象得设计目的有这样几个:
(1) 将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
)放到Reflect
对象上
(2) 修改某些Object
方法的返回结果,让其变得更合理
(3) 让Object
操作都变成函数行为
(4)Reflect
对象的方法与Proxy
对象的方法一一对应
- 用Proxy可以写一个观察者模式的最简单实现,即实现
observable
和observe
这两个函数。思路是observable
函数返回一个原始对象的Proxy代理,拦截赋值操作,触发充当观察者的各个函数
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result
}
const person = observable({
name: '张三',
age: 20
});
function print() {
console.log(`${person.name}, ${person.age}`)
}
observe(print);
person.name = '李四';
// 输出
// 李四, 20
Promise对象
- 一般来说,调用
resolve
或reject
以后,Promise的使命就完成了,后续操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。所以,最好在它们前面加上return
语句防止意外 - 采用链式的
then
,可以指定一组按照次序调用的回调函数 Promise.race()
方法是将多个Promise实例包装成一个新的Promise实例。这些Promise实例中,只要有一个实例率先改变状态,新实例的状态就会跟着改变。那个率先改变的Promise实例返回值,就传递给新实例的回调函数- 由于
Promise.try
为所有操作提供了统一的处理机制,所以如果想用then
方法管理流程,最好都用Promise.try
包装一下。这样有许多好处,其中一点就是可以更好地管理异常
Iterator 和 for...of 循环
- Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令
for...of
循环,Iterator接口主要供for...of
消费 yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口for...of
获得的是对象的键值,而for...in
获得的是对象的键名for...of
直接遍历普通的对象会出错
Generator 函数的语法
- 一个生成Hello World的Generator函数
helloWorldGenerator
的例子如下:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: false }
hw.next()
// { value: undefined, done: true }
- 由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的
Symbol.iterator
属性,从而使得该对象具有Iterator接口 yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值for...of
循环可以自动遍历 Generator 函数运行时生成的Iterator
对象,且此时不再需要调用next
方法- Generator 函数返回的遍历器对象,还有一个
return
方法,可以返回给定的值,并且终结遍历 Generator 函数 - ES6提供了
yield*
表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数 - 可以并行执行、交换执行权的线程(或函数),就称为协程。它是以多占用内存为代价,实现多任务的并行
Generator 函数的异步应用
- JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
- Thunk函数是Generator函数自动执行的一种方案,生产环境可使用thunkify。下面是一个读取文件的例子:
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var gen = function* () {
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
}
var run = function (fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(gen);
async 函数
- 前文的
gen
函数改写成async
函数如下:
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
}
async
函数的执行与普通函数一样,只需要一行,不需要调用next
方法或者使用co
模块async
函数完全可以看作多个异步操作包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖- 有时,我们希望即使一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个
await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行 - 只有
async
函数内部是继发执行,外部不受影响。一个并发读取远程 URL 并按顺序输出的例子如下:
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
Class 的基本语法
- 基本上,ES6 的
class
可以看作只是一个语法糖,它的绝大部分功能 ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已 - 与 ES5 一样,实例的属性除非定义在其本身(即定义在
this
对象上),否则都是定义在原型上(即定义在class
上) - 与 ES5 一样,类的所有实例共享一个原型对象
- 类的内部方法如果含有
this
,它默认指向类的实例,但是,必须非常小心,一旦单独使用该方法,很可能报错。一个比较简单的解决方法是,在构造方法中绑定this
class Logger {
constructor() {
this.printName = this.printName.bind(this);
// 也可以使用箭头函数:this.getThis = () => this
}
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName();
- Class 内部调用
new.target
,返回当前 Class
Class 的继承
- 由于
super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的 - ES6 规定,在子类普通方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类实例
Module 的语法
- 模块功能主要由两个功能组成:
export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能 - 模块整体加载所在的那个对象应该是可以静态分析的,所以不允许运行时改变,下面的写法都是不允许的
import * as circle from './circle';
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
export default
就是输出一个叫做default
的变量或方法,然后系统允许你为它取任何名字require
是运行时加载模块,import
命令无法取代require
的动态加载功能,但 ES2020 提案引入import()
函数,支持动态加载模块
Module 的加载实现
defer
是“渲染完再执行”,async
是“下载完就执行”。如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的- 浏览器加载 ES6 模块,也使用
<script>
标签,但是要加入type="module"
属性 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
- ES6 模块与 CommonJS 模块有两个重大差异:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值
- ES6 模块是动态引用,如果使用
import
从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载对象的引用,需要开发者自己保证,真正取值的时候能够取到值
编程风格
- 使用数组成员对变量赋值时,优先使用解构赋值
- 函数的参数如果是对象的成员,优先使用解构赋值
- 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值
- 如果对象添加属性不可避免,要使用
Object.assign
方法
异步遍历器
- 异步遍历器最大的语法特点,就是调用遍历器的
next
方法,返回的是一个 Promise 对象 - Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异:
// 传统写法
function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
readStream.on('data', (chunk) => {
console.log('>>> ' + chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}
// 异步遍历器写法
async function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
for await (const chunk of readStream) {
console.log('>>> ' + chunk)
}
console.log('### DONE ###')
}
- 异步 Generator 函数内部,能够同时使用
await
和yield
命令,可以这样理解,await
命令用于将外部操作产生的值输入函数内部,yield
命令用于将函数内部的值输出