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

时间:2022-06-21 08:46:31

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命令具有提升效果,会提升到整个模块的头部,首先执行。因为不是运行时加载,不支持条件加载、按需加载等