ES6核心内容精讲--快速实践ES6(二)


Iterator 和 for…of

是什么:

Iterator(迭代器)是专门用来控制如何遍历的对象,具有特殊的接口。

Iterator 接口是一种数据遍历的协议,只要调用迭代器对象对象的 next 方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息,这个包含 done 和 value 两个属性。

迭代器对象创建后,可以反复调用 next()使用。

怎么用:

Iterator 对象带有 next 方法,每一次调用 next 方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value 和 done 两个属性的对象。其中,value 属性是当前成员的值,done 属性是一个布尔值,表示遍历是否结束。
ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。

let obj = {
  data: ['hello', 'world'],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false,
          };
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  },
};

for (let item of obj) {
  console.log(item);
}
// hello
// world

如上,for-of 循环首先调用 obj 对象的 Symbol.iterator 方法,紧接着返回一个新的迭代器对象。迭代器对象可以是任意具有.next()方法的对象,for-of 循环将重复调用这个方法,每次循环调用一次。return 的对象中 value 表示当前的值,done 表示是否完成迭代。

Iterator 的作用有三个:

  1. 为各种数据结构,提供一个统一的、简便的访问接口;

  2. 使得数据结构的成员能够按某种次序排列;

  3. ES6 创造了一种新的遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费。

一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for…of 循环遍历它的成员。也就是说,for…of 循环内部调用的是数据结构的 Symbol.iterator 方法。

for…of 循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如 arguments 对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

Symbol

是什么

ES6 引入了一种第六种基本类型的数据:Symbol。Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用。

怎么用

调用 Symbol()创建一个新的 symbol,它的值与其它任何值皆不相等。

var sym = new Symbol(); // TypeError,阻止创建一个显式的Symbol包装器对象而不是一个Symbol值
var s1 = Symbol('foo');
var s2 = Symbol('foo');
s1 === s2; // false

常用使用场景:

由于每一个 Symbol 值都是不相等的,因此常作为对象的属性名来防止某一个键被不小心改写或覆盖,这个以 symbol 为键的属性可以保证不与任何其它属性产生冲突。

作为对象属性名时的遍历:参见对象的遍历那节

内置的 Symbol 值:

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。其中一个很重要的就是 Iterator 中提到的 Symbol.iterator

Reflect(反射)

是什么

Reflect 是一个内置的对象,它提供可拦截 JavaScript 操作的方法。

为什么要增加 Reflect 对象

参考链接

1)更有用的返回值

比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc)则会返回 false。

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

2)函数操作。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name)和 Reflect.deleteProperty(obj, name)让它们变成了函数行为

3)更加可靠的函数调用方式

在 ES5 中,当我们想传一个参数数组 args 来调用函数 f,并且将 this 绑定为 this,可以这样写:

f.apply(obj, args);

但是,f 可能是一个故意或者不小心定义了它自己的 apply 方法的对象。当你想确保你调用的是内置的 apply 方法时,一种典型的方法是这样写的:

Function.prototype.apply.call(f, obj, args);

但是这种方法不仅冗长而且难以理解。通过使用 Reflect,你可以以一种更简单、容易的方式来可靠地进行函数调用

Reflect.apply(f, obj, args);

4)可变参数的构造函数

假设你想调用一个参数是可变的构造函数。在 ES6 中,由于新的扩展运算符,你可能可以这样写:

var obj = new F(...args);

在 ES5 中,这更加难写,因为只有通过 F.apply 或者 F.call 传递可变参数来调用函数,但是没有 F.contruct 来传递可变参数实例化一个构造函数。通过 Reflect,在 ES5 中可以这样写(内容翻译自参考链接,链接的项目是 ES6 Reflect 和 Proxy 的一个 ES5 shim,所以会这么说):

var obj = Reflect.construct(F, args);

5)为 Proxy(代理,见下一章)的 traps 提供默认行为

当使用 Proxy 对象去包裹存在的对象时,拦截一个操作是很常见的。执行一些行为,然后去“做默认的事情”,这是对包裹的对象进行拦截操作的典型形式。例如,我只是想在获取对象 obj 的属性时 log 出所有的属性:

var loggedObj = new Proxy(obj, {
  get: function (target, name) {
    console.log('get', target, name);
    // now do the default thing
  },
});

Reflect 和 Proxy 的 API 被设计为互相联系、协同的,因此每个 Proxy trap 都有一个对应的 Reflect 去“做默认的事情”。因此当你发现你想在 Proxy 的 handler 中“做默认的事情”是,正确的事情永远都是去调用 Reflect 对象对应的方法:

var loggedObj = new Proxy(obj, {
  get: function (target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
  },
});

Reflect 方法的返回类型已经被确保了能和 Proxy traps 的返回类型兼容。

6)控制访问或者读取时的 this

var name = ... // get property name as a string
Reflect.get(obj, name, wrapper) // if obj[name] is an accessor, it gets run with `this === wrapper`
Reflect.set(obj, name, value, wrapper)

静态方法

Reflect 对象一共有 14 个静态方法(其中 Reflect.enumerate 被废弃)

与大多数全局对象不同,Reflect 没有构造函数。不能将其与一个 new 运算符一起使用,或者将 Reflect 对象作为一个函数来调用。

Reflect 对象提供以下静态函数,它们与代理处理程序方法(Proxy 的 handler)有相同的名称。这些方法中的一些与 Object 上的对应方法基本相同,有些遍历操作稍有不同,见对象扩展遍历那节。

Reflect.apply()

对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply()功能类似。

Reflect.construct()

对构造函数进行 new 操作,相当于执行 new target(…args)。

Reflect.defineProperty()

和 Object.defineProperty()类似。

Reflect.deleteProperty()

删除对象的某个属性,相当于执行 delete target[name]。

Reflect.enumerate()

该方法会返回一个包含有目标对象身上所有可枚举的自身字符串属性以及继承字符串属性的迭代器,for…in 操作遍历到的正是这些属性。

Reflect.get()

获取对象身上某个属性的值,类似于 target[name]。

Reflect.getOwnPropertyDescriptor()

类似于 Object.getOwnPropertyDescriptor()。

Reflect.getPrototypeOf()

类似于 Object.getPrototypeOf()。

Reflect.has()

判断一个对象是否存在某个属性,和 in 运算符的功能完全相同。

Reflect.isExtensible()

类似于 Object.isExtensible().

Reflect.ownKeys()

返回一个包含所有自身属性(不包含继承属性)的数组。

Reflect.preventExtensions()

类似于 Object.preventExtensions()。

Reflect.set()

设置对象身上某个属性的值,类似于 target[name] = val。

Reflect.setPrototypeOf()

类似于 Object.setPrototypeOf()。

Proxy(代理)

是什么

Proxy 对象用于定义基本操作的自定义行为 (例如属性查找,赋值,枚举,函数调用等)。

一些术语:

  • handler:包含 traps 的对象。
  • traps:提供访问属性的方法,与操作系统中的 traps 定义相似。
  • target:被代理虚拟化的对象,这个对象常常用作代理的存储后端。

用法

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是 handler 参数的写法。其中,new Proxy()表示生成一个 Proxy 实例,target 参数表示所要代理的目标对象,handler 参数也是一个对象,用来定制代理行为。

下面代码对一个空对象进行了代理,重定义了属性的读取(get)和设置(set)行为。

var obj = new Proxy(
  {},
  {
    get: function (target, key, receiver) {
      console.log(`getting ${key}!`);
      return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
      console.log(`setting ${key}!`);
      return Reflect.set(target, key, value, receiver);
    },
  }
);

obj.count = 1;
//  setting count!
++obj.count;
//  getting count!
//  setting count!
//  2

handler 对象的方法

handler 是一个包含了 Proxy 的 traps 的占位符对象。

所有的 trap 都是可选的,如果某个 trap 没有定义,将会对 target 进行默认操作。这些 trap 和 Reflect 的静态方法是对应的,可以使用 Reflect 对应的静态方法提供默认行为。上面的例子中,handler 定义了 get 和 set 两个 trap,每个 trap 都是一个方法,接收一些参数。返回了对应的 Reflect 方法来执行默认方法。

handler 的每个方法可以理解为对相应的某个方法进行代理拦截。

handler.getPrototypeOf(target):Object.getPrototypeOf 的一个 trap

handler.setPrototypeOf(target, proto):Object.setPrototypeOf 的一个 trap

handler.isExtensible(target):Object.isExtensible 的一个 trap

handler.preventExtensions(target):Object.preventExtensions 的一个 trap

handler.getOwnPropertyDescriptor(target, propKey):Object.getOwnPropertyDescriptor 的一个 trap

handler.defineProperty(target, propKey, propDesc):Object.defineProperty 的一个 trap

handler.has(target, propKey):in 操作的一个 trap

handler.get(target, propKey, receiver):获取属性值的一个 trap

handler.set(target, propKey, value, receiver):设置属性值的一个 trap

handler.deleteProperty(target, propKey):delete 操作的一个 trap

handler.ownKeys(target):Object.getOwnPropertyNames 和 Object.getOwnPropertySymbols 的一个 trap

handler.apply(target, object, args):函数调用的一个 trap

handler.construct(target, args):new 操作的一个 trap

Proxy.revocable()

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.revocable 方法返回一个对象,该对象的 proxy 属性是 Proxy 实例,revoke 属性是一个函数,可以取消 Proxy 实例。上面代码中,当执行 revoke 函数之后,再访问 Proxy 实例,就会抛出一个错误。

Proxy.revocable 的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

使用场景

上面说的那些可能都比较虚,去看一下 w3cplus 上翻译的实例解析 ES6 Proxy 使用场景,可能就会更清楚地明白该怎么用。

如实例解析 ES6 Proxy 使用场景中所说,Proxy 其功能非常类似于设计模式中的代理模式,该模式常用于三个方面:

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理

有以下 5 个常见使用场景:

  1. 抽离校验模块

  2. 私有属性

  3. 访问日志

  4. 预警和拦截

  5. 过滤操作

类与继承

类:

将原先 JavaScript 中传统的通过构造函数生成新对象的方式变为类的方式,contructor 内是构造函数执行的代码,外面的方法为原型上的方法

// ES5
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  // 静态方法,static关键字,就表示该方法不会被实例继承(但是会被子类继承),而是直接通过类来调用
  static classMethod() {
    return 'hello';
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

继承:

通过 extends 关键字来实现。super 关键字则是用来调用父类

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。理解了这句话,下面 1,2 两点也就顺其自然了:

1)子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。

2)在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

Object.getPrototypeOf(ColorPoint) === Point; // true

3)mixin: 继承多个类

function mix(...mixins) {
  class Mix {}

  for (let mixin of mixins) {
    copyProperties(Mix, mixin);
    copyProperties(Mix.prototype, mixin.prototype);
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}

4)new.target 属性:通过检查 new.target 对象是否是 undefined,可以判断函数是否通过 new 进行调用。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用new生成实例');
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用new生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错

Decorator(装饰器)

是什么

Decorator 是用来修改类(包括类和类的属性)的一个函数。

这是 ES 的一个提案,其实是 ES7 的特性,目前 Babel 转码器已经支持。

怎么用

1)修饰类:在类之前使用@加函数名,装饰器函数的第一个参数,就是所要修饰的目标类

function testable(target) {
  target.prototype.isTestable = true;
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable; // true

装饰器函数也可以是一个工厂方法

function testable(isTestable) {
  return function (target) {
    target.isTestable = isTestable;
  };
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable; // true

@testable(false)
class MyClass {}
MyClass.isTestable; // false

2)修饰类的属性:修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。装饰器在作用于属性的时候,实际上是通过 Object.defineProperty 来进行扩展和封装的。

下面是一个例子,修改属性描述对象的 enumerable 属性,使得该属性不可遍历。

class Person {
  @nonenumerable
  get kidCount() {
    return this.children.length;
  }
}

function nonenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

实践

core-decorators.js这个第三方模块提供了几个常见的修饰器。

在修饰器的基础上,可以实现 Mixin 模式等。

Module(模块)

在 ES6 之前,前端和 nodejs 实践中已经有一些模块加载方案,如 CommonJS、AMD、CMD 等。ES6 在语言标准的层面上,实现了模块功能。

模块功能主要由两个命令构成:export 和 import。export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

export

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。必须使用 export 关键字输出该变量。有以下两种不同的导出方式:

命名导出

命名导出规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

export { myFunction }; // 导出一个函数声明
export const foo = Math.sqrt(2); // 导出一个常量

默认导出 (每个脚本只能有一个),使用 export default 命令:

export default myFunctionOrClass;

本质上,export default 就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字

对于只导出一部分值来说,命名导出的方式很有用。在导入时候,可以使用相同的名称来引用对应导出的值。

关于默认导出方式,每个模块只有一个默认导出。一个默认导出可以是一个函数,一个类,一个对象等。当最简单导入的时候,这个值是将被认为是”入口”导出值。

import

使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。

import { foo, bar } from 'my_module'; // 指定加载某个输出值

import 'lodash'; // 仅执行

import { lastName as surname } from './profile'; // 为输入的模块重命名

import * as circle from './circle'; // 整体加载

/*export和import复合写法*/
export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

ES6 模块与 CommonJS 模块的差异

它们有两个重大差异。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

CommonJS 是运行时加载,ES6 是编译时加载,使得静态分析成为可能

注意事项

  1. ES6 的模块自动采用严格模式。因此 ES6 模块中,顶层的 this 指向 undefined。

  2. export 一般放在两头即开始或者结尾这样更能清晰地明白暴露了什么变量

  3. 注意,import 命令具有提升效果,会提升到整个模块的头部,首先执行。因为不是运行时加载,不支持条件加载、按需加载等


文章作者: Angus
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Angus !
  目录