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(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性

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)的基础上,新增MapSet

  • 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的显式绑定(callapplybind)。

::双冒号运算符(函数绑定运算符),可以用来取代callapplybind调用。

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 - ruanyifeng

以上,使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函数返回异步遍历器对象。

 

posted @ 2018-07-25 01:12  万箭穿心,习惯就好。  阅读(1033)  评论(0编辑  收藏  举报