JS - ECMAScript2015(ES6)新特性
友情提示:本文仅mark几个常用的新特性,详细请参见:ES6入门 - ryf;
碎片
var VS let VS const
- var:声明全局变量,
- let:声明块级变量,即局部变量
- const:声明常量,块级作用域,不可修改且必须初始化
将一个对象彻底冻结为常量的方法
var constantize = (obj) => { // 冻结对象本身 Object.freeze(obj); // 冻结对象的属性 Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); };
ES6声明变量的方法,除上述外,还支持:class、import、function。
Number.isNaN() & Number.isFinite()
该两者仅对数值有效:
- Number.isNaN():判断一个数值是否为NaN,利用NaN是唯一不等于自身的值,用于isNaN()判断
- Number.isFinite():表示某个数值是否为正常的数值(即,非infinity),Infinity、-Infinity、NaN和undefined返回false,其余均返回true
注意与传统的全局方法isFinite()和isNaN()的区别,传统方法先调用Number()
将非数值的值转为数值,再进行判断。
同理,Number.parseInt(), Number.parseFloat()优先使用。
新增Number.EPSILON表示极小量,表示 1 与大于 1 的最小浮点数之间的差。
function isTrueWithinErrorMargin (left, right) { return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2); }
新增Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER分别表示安全的极大值和极小值。
Symbol
JS 的第7种数据类型,独一无二的特性:
- 用于扩展对象属性名
- 定义常量
提供几个常用方法
Symbol()
Symbol.for()
Symbol.keyfor
()
${}
模版字符串(template string)语法,配合反引号``使用
- 换行
- 表达式嵌入:占位符(使用
<%...%>
放置 JavaScript 代码,使用<%= ... %>
输出 JavaScript 表达式。) - 支持嵌套
标签模版
函数调用的一种特殊形式:fun`xxx`。
可过滤 HTML 字符串,防止用户输入恶意内容(特殊字符转义)
function toSaferHTML(templateData) { let s = templateData[0]; for (let i = 1; i < arguments.length; i++) { let arg = String(arguments[i]); // Escape special characters in the substitution. s += arg.replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">"); // Don't escape special characters in the template. s += templateData[i]; } return s; } let sender = '<script>alert("abc")</script>'; // 恶意代码 let message = toSaferHTML`<p>${sender} has sent you a message.</p>`; // <p><script>alert("abc")</script> has sent you a message.</p>
注:模板处理函数的第一个参数(模板字符串数组),还有一个 raw
属性,用于保存转义后的原字符串。
支持多语言处理。
...
扩展运算符,基于 for...of,将一个数组转为参数序列,或将实现了 Iterator 接口的对象转化为真正的数组。
- 取代apply()方法
- 复制数组(深拷贝)或合并数组(浅拷贝)
- 配合解构赋值:扩展运算符可以识别四字节的Unicode字符的长度
注意,没有实现 Iterator 接口的对象可以使用Array.from
- 类似数组的对象:(1)DOM 操作返回的 NodeList 集合;(2)函数内部的
arguments
对象 - 可遍历的对象
提供2种字符串长度方法
[1]. [...str].length [2]. Array.from(str).length
Object.assign
将源对象(source)的所有可枚举属性,复制到目标对象(target)。(浅拷贝)
- undefined和null不能作为第一个参数
- 只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)
使用场景
- 为对象添加属性/方法
- 克隆/合并对象
- 为属性指定默认值
Object.getOwnPropertyDescriptor
获取对象属性的描述对象,其中属性enumerable
表示可枚举性。以下只对enumerable=true
的对象有效
for...in
循环:只遍历对象自身的和继承的可枚举的属性Object.keys()
:返回对象自身的所有可枚举的属性的键名JSON.stringify()
:只串行化对象自身的可枚举的属性Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性
Object.getOwnPropertyDescriptors
基于Object.getOwnPropertyDescriptor实现,返回指定对象所有自身属性(非继承属性)的描述对象。
- 解决
Object.assign()
无法正确拷贝get
属性和set
属性的问题 - 配合
Object.create
方法,将对象属性克隆到一个新对象(浅拷贝) - 实现 Mixin(混入)模式
//克隆 方法1 const clone = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) ); //方法2 const clone2 = Object.assign( Object.create(Object.getPrototypeOf(obj)), obj );
属性遍历
- for...in
- Object.keys()
- Object.getOwnPropertyNames():返回一个数组,包含对象自身的所有属性的键名
- Object.getOwnPropertySymbols():返回一个数组,包含对象自身的所有 Symbol 属性的键名
- Reflect.ownKeys():返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举
建议,尽量不要用for...in循环,而用Object.keys()代替。
此外,for...in只能获得对象的键名,不能直接获取键值,而for...of允许遍历获得键值。
Iterator & for...of
为不同的数据结构提供统一的访问机制,任何数据结构只要部署了Iterator接口:
- 支持遍历(for...of)操作
- 使用扩展运算符,将其转为数组
本质是:数据结构部署Symbol.iterator属性
遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的模版描述如下
interface Iterable { [Symbol.iterator]() : Iterator, } interface Iterator { next(value?: any) : IterationResult, } interface IterationResult { value: any, done: boolean, }
原生具备 Iterator 接口的数据结构:
Array Map Set String TypedArray 函数的 arguments 对象 NodeList 对象
默认调用遍历器的场景
- for...of
- 解构赋值
- ...
- yield*:其后面跟一个可遍历的结构,默认调用该结构的遍历器接口
- Array.from(),Promise.all/race()
扩展应用
- String
将遍历器转换为数组,提供2种方法:
[...str.matchAll(regex)] Array.from(str.matchAll(regex));
其中,matchAll() 用于一次性取出所有匹配结果,返回一个遍历器。
Set & Map
ES6在原有的集合数据结构(数组Array
和对象Object
)的基础上,新增Map
和Set
- Set:类似数组,值唯一
- Map:类似(键值对集合的)对象,将"字符串-值"结构的Object扩展到"值-值"结构的Map
- WeakSet:不可遍历,成员只能是对象
- WeakMap:不可遍历
很重要:遍历顺序就是插入顺序。支持遍历 for...of 和 forEach。
其中,forEach 方法的
- 第一个参数回调函数的参数依次为:
(value, key, map)
- 第二个参数用于绑定this,指向某个对象
Set
[1]. 数组去重
function dedupe(array) { return Array.from(new Set(array)); }
[2]. 交并差集运算
若想改变Set本身,提供如下2种方法
// 方法一:利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构 let set = new Set([1, 2, 3]); set = new Set([...set].map(val => val * 2)); // 方法二:利用Array.from方法 let set = new Set([1, 2, 3]); set = new Set(Array.from(set, val => val * 2));
Map
关于Map与其他数据结构的转换,可参见:http://es6.ruanyifeng.com/#docs/set-map
解构赋值
从数组和对象中提取值,对变量进行赋值:(模式匹配)
- 只要等号右边的值不是对象或数组,就先将其转为对象
- 实现了Iterator接口的数据结构,可以采用数组形式的解构赋值
- 由于undefined和null无法转为对象,所以对其解构赋值,会报错
- 解构赋值尽量不适用圆括号()
支持默认值,前提是对象的属性值/数组成员严格等于undefined。若数组成员是null,默认值不会生效。
除数组和对象,字符串也支持解构赋值。
应用场景
- 交换变量的值
- 从函数返回多个值
- 函数参数的定义、默认值
- 提取json数据
- 利用for...of遍历map结构
[1]. 在函数形参使用解构赋值
// 写法一 function m1({x = 0, y = 0} = {}) { return [x, y]; } // 写法二 function m2({x, y} = { x: 0, y: 0 }) { return [x, y]; }
- 写法一:函数参数的默认值是空对象,但是设置了对象解构赋值的默认值
- 写法二:函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值
推荐写法一,因为在函数体中实际使用的左边的x和y。
箭头函数
箭头函数可以让this
指向固定化,总是指向函数定义生效时所在的作用域,而不是指向运行时所在的作用域,这种特性很有利于封装回调函数。
s1 = 0; s2 = 0; function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3200); // 3 setTimeout(() => console.log('s2: ', timer.s2), 3200); // 0 setTimeout(() => console.log('s11: ', this.s1), 3200); // 0 setTimeout(() => console.log('s22: ', this.s2), 3200); // 3
箭头函数自动绑定this,可以减少对this的显式绑定(call
、apply
、bind
)。
::双冒号运算符(函数绑定运算符),可以用来取代call
、apply
、bind
调用。
foo::bar(...arguments); 等同于 bar.apply(foo, arguments);
注意点
- 函数体内的
this
对象,就是定义时所在的对象,而不是使用时所在的对象 - 不可以当作构造函数,即:不可以使用
new
命令 - 不可以使用
yield
命令,即:箭头函数不能用作 Generator 函数 - 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
尾调用 & 尾递归
尾调用:某个函数的最后一步是调用另一个函数
function f(x){ return g(x); //(1)return (2)无其他计算 }
尾递归基于尾调用,相对节省内存,不会发生栈溢出。
相关示例,可参考:阶乘或Fibonacci 数列.
注意,尾调用优化,仅在严格模式下有效。
Proxy & Reflect
Proxy:修改某些操作的默认行为,可以进行数据验证
关于 Proxy 支持的拦截操作,具体参见:http://es6.ruanyifeng.com/#docs/proxy
其中,apply用于拦截如下操作:
- 函数调用
- call、apply
- Reflect.apply
Reflect:将Object对象上的方法迁移到Reflect对象上
关于Reflect对象的方法与Proxy对象的方法一一对应,具体参见:http://es6.ruanyifeng.com/#docs/reflect
建议用 Reflect.xxx 代替 Object.xxx
综上,Proxy 对象和 Reflect 对象联合使用,前者拦截操作,后者完成默认行为。
Promise对象
引出
- 回调地狱(callback hell)
- 多个异步回调难以维护和控制的问题
设计思想:所有异步任务都返回一个 Promise 实例。(异步操作同步化)
Promise 实质上是一个构造函数。
var p = new Promise(f1); p.then(f2);
回调函数f1完成后,执行回调函数f2。(添加状态改变时的回调函数通过then()方法)
Promise 对象通过自身的状态,来控制异步操作。
- 异步操作未完成(pending)
- 异步操作成功(fulfilled)
- 异步操作失败(rejected)
同时,只有异步操作的结果才会改变其状态,Promise 实例的状态变化只可能发生一次:
- 异步操作成功,Promise 实例传回一个值(value),状态变为
fulfilled
- 异步操作失败,Promise 实例抛出一个错误(error),状态变为
rejected
// resolve 和 reject 均由 JavaScript 引擎提供,无需自己实现 var p = new Promise(function (resolve, reject) { // ... if (/* 异步操作成功 */){ resolve(value); } else { /* 异步操作失败 */ reject(new Error()); } });
- resolve:将Promise实例的状态从“未完成”变为“成功”(
pending-->
fulfilled
),在异步操作成功时调用,并将异步操作的结果作为参数传出 - reject:将Promise实例的状态从“未完成”变为“失败”(
pending-->
rejected
),在异步操作失败时调用,并将异步操作的错误作为参数传出
注意:
- Promise 的回调函数属于异步任务,会在同步任务之后执行。
- Promise 的回调函数不是正常的异步任务,而是微任务(microtask)
正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。所以,微任务的执行时间一定早于正常任务。
setTimeout(function() { console.log(1); }, 0); new Promise(function (resolve, reject) { resolve(2); }).then(console.log); console.log(3); // 3 2 1
Promise对象还有其他特性(可以看作是缺点):
- Promise 对象新建后就会立即执行,无法取消Promise
- Promise 内部的错误不会影响到 Promise 外部的代码(Promise 会吃掉错误),也可以通过设置回调函数将错误信息抛出
- 代码冗余,all then()...
静态方法
- Promise.all():与
- Promise.race():或
- Promise.resolve():将现有对象转为立即resolved的Promise对象
- Promise.reject():返回一个新的Promise实例,该实例的状态为rejected,回调立即执行
Promise.resolve()与Promise.reject()略有不同,Promise.reject()会将参数原封不动地传出。
新的Promise.try()用于统一管理同步和异步代码,统一用promise.catch()捕获所有同步和异步的错误
Promise.try(database.users.get({id: userId})) .then(...) .catch(...)
参考:Promise对象;ES6 - Promise - ryfeng;
Generator函数
遍历器对象生成函数,可以暂停函数执行,返回任意表达式的值
- function*:
- yield:产出,暂停标志
调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针,可以通过next()依次遍历Generator函数内部的每一个状态(异步操作的容器)。
- 异步操作同步化表达
- 为任意对象部署 Iterator 接口
- 作为数据结构,提供类似数组的接口
- 控制流管理:项目拆分成任务,任务拆分成步骤,依次执行
- 状态机(容器)
- 协程(coroutine)
注意,返回的遍历器对象,其Symbol.iterator属性是其自身
function* gen(){...} var g = gen(); g[Symbol.iterator] === g
遍历器对象是Generator函数的实例,继承其原型上的方法,但是this对象无法访问。若想访问:
// 将遍历器对象绑定到Generator函数的原型 var gen = Gen.call(Gen.prototype);
若想应用new命令,对外封装一层即可
function F() { return Gen.call(Gen.prototype); } // f即遍历器对象 F f = new F();
for...of
支持自动遍历Generator函数时生成的Iterator对象。
注意,不会遍历到return语句,扩展运算符、解构赋值和Array.from()亦是。
利用for...of循环,可以写出遍历任意对象(object)的方法。通过Generator函数为对象加上这个接口
function* objectEntries() { let propKeys = Object.keys(this); for (let propKey of propKeys) { yield [propKey, this[propKey]]; } } let obj= { first: 'Jane', last: 'Doe' }; obj[Symbol.iterator] = objectEntries; for (let [key, value] of objectEntries(obj)) { console.log(`${key}: ${value}`); }
重点理解下述3个原型方法:
Generator.prototype.next()
- next方法可以带一个参数,该参数重写上一个yield表达式的返回值(yield表达式默认无返回值或undefined)
- 第一次执行next方法,等同于启动执行Generator函数的内部代码
Generator.prototype.throw()
- Generator函数体内或外抛出的错误gen.throw(),会优先被Generator体内的try...catch捕获
- 注意遍历器对象的throw()方法和throw命令的不同
- throw方法抛出的错误要被内部try...catch捕获,前提是必须至少执行过一次next方法,,否则只能被外部try...catch捕获
- throw方法被捕获后,会附带执行一次next方法,返回下一条yield表达式
- Generator函数体内的错误未捕获到,会中断Generator函数体内的后续代码
Generator.prototype.return()
- 返回给定的值,终结执行Generator函数
- 优先级低于finally代码块
next(): 将yield表达式替换成一个值 throw(): 将yield表达式替换成一个throw语句 return(): 将yield表达式替换成一个return语句
yield*
在一个Generator函数A里面执行另一个Generator函数B。
场景:递归
若B中有return语句,通过以下形式获取返回值
var value = yield* B()
异步应用
异步调用方式
- 发布/订阅(事件监听)
- 回调函数
- Promise对象
- Generator函数:协程
协程
Generator函数是协程在ES6的实现,可以理解为Generator函数是协程的实例
最大特点就是可以交出函数的执行权(暂停函数执行和恢复执行)
- 函数体内、外的数据交换
- 错误处理机制
自动执行机制:接收和交还程序的执行权(当异步操作有了结果,自动交回执行权)
- 回调函数:将异步操作包装成 Thunk 函数,在回调函数里面交回执行权
- Promise对象:将异步操作包装成 Promise 对象,用then方法交回执行权
Thunk函数
自动执行Generator函数的一种方法。
- yield:将程序的执行权移出 Generator 函数
- thunk:将执行权交还给 Generator 函数
懒执行,传名调用的实现策略,用临时函数(Thunk函数)替换某个表达式。在JavaScript中,是将多参数(某个参数是回调函数)函数fn,替换成一个只接受回调函数作为参数的单参数函数。
// Thunk函数转换器 const Thunk = function(fn) { return function (...args) { return function (callback) { return fn.call(this, ...args, callback); } }; };
提供Thunk函数转换工具:Thunkify 模块
无需编写Generator函数的自动执行器,而且其检查机制,确保回调函数只运行一次。使用前提是Generator函数的yield命令后面,只能是Thunk函数。
// 引入 var thunkify = require('thunkify'); // 转换 var thunkFun= thunkify(fn);
co模块
自动执行Generator函数的另一种方法,基于Promise对象的自动执行器。
co函数接收Generator函数作为参数,返回Promise对象,支持then方法执行回调函数。
// 引入 var co = require('co'); // 自动执行 co(gen).then(...);
本质上封装了两种自动执行器(Thunk 函数和 Promise 对象),使用 co 的前提条件是,Generator函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。
而且,co函数支持并发操作:把并发的操作放在数组或对象里,跟在yield命令后面
以上,使Generator支持异步操作,即yield命令后是异步方法,则:该方法只能返回一个Thunk函数或者一个Promise对象。
async函数
- async:表示函数里有异步操作
- await:表示紧跟在后面的表达式需要等待结果
Generator函数的语法糖:对Generator函数和自动执行器的封装
相比Generator函数:( *和yield --> async和await )
- 内置(自动)执行器
- 立即返回Promise对象,支持then方法执行回调函数。
- 语义清晰,适应性广
async函数可以看作是将多个异步操作封装成一个Promise对象,await命令是内部then()方法的语法糖。
关于3者的比较,可参见:async函数-5
务必注意,不能在普通函数中使用await。但是 esm 模块加载器支持顶层await,即await命令可以不放在async函数里面,直接使用
// 顶层 await 的写法 const res = await fetch('google.com'); console.log(await res.text());
常见形式:
// 函数声明 async function foo() {} // 函数表达式 const foo = async function () {}; // 箭头函数 const foo = async () => {}; // 对象的方法 let obj = { async foo() {} }; obj.foo().then(...) // Class 的方法 class Storage { constructor() { this.cachePromise = caches.open('avatars'); } async getAvatar(name) { const cache = await this.cachePromise; return cache.match(`/avatars/${name}.jpg`); } } const storage = new Storage(); storage.getAvatar('jake').then(…);
错误处理机制
async函数内部抛出错误,会导致返回的Promise对象变为reject状态,同时中断async函数的执行,若不中断:
- try...catch
- await Promise(...).catch()
抛出的错误对象会被catch方法回调函数接收到。
并发
互不影响相互独立的2个异步操作同时执行
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
场景:并发拉数据
async function pullDataFeomUrl(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); } }
异步遍历器(Async Iterator)
es2018引入,为异步操作提供原生的遍历器接口:异步返回value和done
特点:异步遍历器的next()方法返回一个Promise对象。
类似对象的同步遍历器部署Symbol.iterator属性,支持for...of,对象的异步遍历器部署Symbol.asynciterator属性,支持for await...of。
即,可遍历对象的Symbol.asynciterator属性返回一个异步Generator函数
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
注意,for await...of循环也可以用于同步遍历器。
异步遍历器重要的是:可以接近相同的方式处理同步操作和异步操作。
// 同步Generator函数 function* map(iterable, func) { const iter = iterable[Symbol.iterator](); while (true) { const {value, done} = iter.next(); if (done) break; yield func(value); } } // 异步Generator函数 async function* map(iterable, func) { const iter = iterable[Symbol.asyncIterator](); while (true) { const {value, done} = await iter.next(); if (done) break; yield func(value); } }
异步Generator函数
async函数与Generator函数的结合
- await:用于将外部操作产生的值输入函数内部
- yield:用于将函数内部的值输出
Generator函数返回同步遍历器对象,异步Generator函数返回异步遍历器对象。