luobotang

ES6 元编程:Part 2 - Reflect

原文链接: www.keithcirkel.co.uk

2015/8/26

归类在:JavaScriptES6元编程

ES6 元编程:第2部分 Reflect

我的上一篇文章中,我们讨论了符号(Symbols),以及符号给 JavaScript 带来的新的元编程特性。这一次,我们(终于!)来聊一聊反射。如果你还没有看过 Part 1: Symbols,推荐你先看一下。上一篇文章中,我一再重申了以下关键点:

  • 符号(symbol)是指包含在实现中的反射 - 通过向已有的类和对象中添加符号来改变其行为。
  • 反射(refect)是指通过内省来实现反射 - 用于从代码中发现更低层次的信息。
  • 代理(proxy)是通过拦截实现反射 - 包装对象,然后通过设置陷阱来拦截它们的行为。

反射(Reflect) 是新的全局对象(类似 JSONMath),提供了一系列有用的内省方法(内省其实就是“找东西”的一种高级说法)。JavaScript 中已经有内省工具了,如 Object.keysObject.getOwnPropertyNames 等等。既然可以向 Object 增加方法,为什么还需要一个新的 API 呢?

“内部方法”

所有的 JavaScript 规范,以及实现引擎,都提供了一些“内部方法”。这样使得 JavaScript 引擎在处理代码时能够更有效地处理对象。如果你读过规范,你会发现到处都有类似 [[Get]][[Set]][[HasOwnProperty]] 这样的东西(如果你失眠的话,可以看看完整的内部方法列表,在 ES5 8.12ES6 9.1 中)。

这些“内部方法”中,有一些隐藏在 JavaScript 代码里面,有些在一些方法中被部分应用到。而且即便它们可用,也被隐藏在各种角落里面。例如 Object.prototype.hasOwnProperty[[HasOwnProperty]] 的一种实现,但并非所有对象都继承自 Object,所以你得用很绕的方式来调用它,例如:

var myObject = Object.create(null); // 比你预想的还要经常使用(特别是在新的 ES6 class 中)
assert(myObject.hasOwnProperty === undefined);
// 如果你想在 `myObject` 上使用 hasOwnProperty:
Object.prototype.hasOwnProperty.call(myObject, 'foo');

另一个例子,内部方法 [[OwnPropertyKeys]] 将对象上所有的字符串类型 key 和符号类型 key 作为一个数组返回。唯一能够获取到这些(不使用 Reflect)的方式是将 Object.getOwnPropertyNamesObject.getOwnPropertySymbols 的结果组合。

var s = Symbol('foo');
var k = 'bar';
var o = { [s]: 1, [k]: 1 };
// 模拟 [[OwnPropertyKeys]]
var keys = Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o));
assert.deepEqual(keys, [k, s]);

Reflect 方法

Reflect 是所有那些原本只能在 JavaScript 引擎内部访问的 “内部方法” 的集合,作为单个、便于使用的对象提供。你可以会想:为什么不像 Object.keysObject.getOwnPropertyNames 等一样,把这些方法添加到 Object 上呢?原因如下:

  1. Reflect 有不是用于 Object 的方法,例如 Reflect.apply,作用于函数。如果是 Object.apply(myFunction) 这样调用的话,看起来会很奇怪。

  2. 使用一个对象来包含这些方法,可以让 JavaScript 其余的部分保持简洁。这比通过构造函数和原型对象来使用反射方法(或者更糟,通过全局对象),要好一些。

  3. typeofinstanceofdelete 已经作为反射运算符存在了。如果像这样增加新的关键字,对于开发者而言,不仅会觉得麻烦,对于向后兼容性来说也会是噩梦,而且会使得保留字数目暴增。

Reflect.apply ( target, thisArgument [, argumentsList] )

Reflect.applyFunction#apply 很像,它接收一个函数,使用一个上下文对象和参数数组来调用该函数。从这一点来说,你 _可以_ 认为 Function#callFunction#apply 是过时的版本。这没什么大不了,不过是更合理的说法。可以这样来使用该方法:

var ages = [11, 33, 12, 54, 18, 96];

// Function.prototype 方式:
var youngest = Math.min.apply(Math, ages);
var oldest = Math.max.apply(Math, ages);
var type = Object.prototype.toString.call(youngest);

// Reflect 方式:
var youngest = Reflect.apply(Math.min, Math, ages);
var oldest = Reflect.apply(Math.max, Math, ages);
var type = Reflect.apply(Object.prototype.toString, youngest);

Reflect.apply 相比 Function.prototype.apply 真正的好处在于其防御性:任何代码都可以简单地修改函数的 callapply 方法,这使你因为崩溃的代码的可怕的变通方法而卡住。这在一般情况下并不是大问题,但下面的代码可能真的存在:

function totalNumbers() {
  return Array.prototype.reduce.call(arguments, function (total, next) {
    return total + next;
  }, 0);
}
totalNumbers.apply = function () {
  throw new Error('Aha got you!');
}

totalNumbers.apply(null, [1, 2, 3, 4]); // throws Error('Aha got you!');

// ES5 唯一能够 防御这种情况的方法很可怕:
Function.prototype.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;

// 也可以这样做,仍然不是很简洁:
Function.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;

// Reflect.apply 前来救援!
Reflect.apply(totalNumbers, null, [1, 2, 3, 4]) === 10;

Reflect.construct ( target, argumentsList [, constructorToCreateThis] )

Reflect.apply 类似,这个方法用于以一组参数来调用构造函数。这对于类也适用,并且能够正确设置对象,从而让构造函数有匹配原型的 this 对象。在 ES5 中,你得使用 Object.create(Constructor.prototype) 的方式,然后将对象传给 Constructor.callConstructor.applyReflect.construct 的不同之处在于,并非是传入对象,只需传入构造函数,然后 Reflect.construct 会处理这些细节(或者,省略该参数,会缺省使用 target 参数作为构造函数)。实现这种方式的老办法就太麻烦了,新方法则简单得多,只需要一行:

class Greeting {

    constructor(name) {
        this.name = name;
    }

    greet() {
      return `Hello ${name}`;
    }

}

// ES5 方式的工厂方法:
function greetingFactory(name) {
    var instance = Object.create(Greeting.prototype);
    Greeting.call(instance, name);
    return instance;
}

// ES6 方式的工厂方法:
function greetingFactory(name) {
    return Reflect.construct(Greeting, [name], Greeting);
}

// 或者,省略第三个参数,会缺省使用第一个参数。
function greetingFactory(name) {
  return Reflect.construct(Greeting, [name]);
}

// 超级简单的 ES6 一行工厂函数!
const greetingFactory = (name) => Reflect.construct(Greeting, [name]);

Reflect.defineProperty ( target, propertyKey, attributes )

Reflect.definePropertyObject.defineProperty 很像,用于定义属性的元数据(metadata)。这个方法更适合,因为 Object.* 隐含着表示方法作用于对象字面量(其实是对象字面量构造函数),而 Reflect.defineProperty 只表示现在做的与反射有关,更具语义化。

特别需要注意的是,和 Object.defineProperty 一样, 对于非法的 targetReflect.defineProperty 会抛出 TypeError 异常,例如 Number 或 String 类型(Reflect.defineProperty(1, 'foo'))。这是好事,对于错误的参数类型抛出异常而不是安静地失败,可以提醒你出现了问题。

再一次,你可以认为 Object.defineProperty 是过时的版本了,改用 Reflect.defineProperty 吧。

function MyDate() {
  /*…*/
}

// 奇怪的老方式,因为这里使用 Object.defineProperty 为 Function 定义属性
// (为什么没有 Function.defineProperty ?)
Object.defineProperty(MyDate, 'now', {
  value: () => currentms
});

// 新方式,并不奇怪,因为 Reflect 做的是反射.
Reflect.defineProperty(MyDate, 'now', {
  value: () => currentms
});

Reflect.getOwnPropertyDescriptor ( target, propertyKey )

这个接口,又可以视为 Object.getOwnPropertyDescriptor 的替代,用于获取属性的描述元数据。主要区别在于,Object.getOwnPropertyDescriptor(1, 'foo') 只会静静地失败,返回 undefined,而 Reflect.getOwnPropertyDescriptor(1, 'foo') 则会抛出 TypeError —— 和 Reflect.defineProperty 一样,对于非法参数抛出异常。你大概明白这是什么意思了,而 Reflect.getOwnPropertyDescriptor 则废弃了 Object.getOwnPropertyDescriptor

var myObject = {};
Object.defineProperty(myObject, 'hidden', {
  value: true,
  enumerable: false,
});
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });

// 老方式:
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });

assert(Object.getOwnPropertyDescriptor(1, 'foo') === undefined)
Reflect.getOwnPropertyDescriptor(1, 'foo'); // 抛出 TypeError

Reflect.deleteProperty ( target, propertyKey )

Reflect.deleteProperty 会删除对象上的属性。在 ES6 之前,你可能会写 delete obj.foo,现在你可以用 Reflect.deleteProperty(obj, 'foo')。这有点啰嗦,而且与 delete 关键字的语义稍有不同,但对于对象而言基本作用是一样的。两者都调用内部的 target[[Delete]](propertyKey) 方法 —— 但 delete 操作符还可以“用于”非对象的引用(例如,变量),所以这个接口会做对操作对象进行更多检查,也更可能会抛出异常:

var myObj = { foo: 'bar' };
delete myObj.foo;
assert(myObj.hasOwnProperty('foo') === false);

myObj = { foo: 'bar' };
Reflect.deleteProperty(myObj, 'foo');
assert(myObj.hasOwnProperty('foo') === false);

再一次,你可以任务这个接口是删除属性的“新方法” —— 如果你想的话。它的意图显然是是非常明确的。

Reflect.getPrototypeOf ( target )

关于替换、废弃 Object 方法的主题继续 —— 这次是 Object.getPrototypeOf。和它的同胞类似,新的 Reflect.getPrototypeOf 对于非法的 target,会抛出 TypeError,例如 Number、String 字面量,nullundefined。而 Object.getPrototypeOf 强制要求 target 是对象,所以 'a' 会变成 Object('a')。语法上两种完全相同。

var myObj = new FancyThing();
assert(Reflect.getPrototypeOf(myObj) === FancyThing.prototype);

// 老方式
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);

Object.getPrototypeOf(1); // undefined
Reflect.getPrototypeOf(1); // TypeError

Reflect.setPrototypeOf ( target, proto )

当然,如果没有 getPrototypeOf 是没法使用 getPrototypeOf 的。Object.setPrototypeOf 对于非对象会抛出异常,但会尝试将传入的参数转为对象,不过如果内部 [[SetPrototype]] 方法失败,会抛出 TypeError,成功则返回参数 targetReflect.setPrototypeOf 则更基本些,如果接收了非对象参数,则抛出 TypeError,但如果不是这样,则会返回 [[SetPrototypeOf]] 的结果,表示操作是否成功的 Boolean 值。这很有用,因为只需要处理返回值,而不需要用 try/catch,因为这会在接收到错误参数时捕获到 TypeError 异常。

var myObj = new FancyThing();
assert(Reflect.setPrototypeOf(myObj, OtherThing.prototype) === true);
assert(Reflect.getPrototypeOf(myObj) === OtherThing.prototype);

// 老方式
assert(Object.setPrototypeOf(myObj, OtherThing.prototype) === myObj);
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);

Object.setPrototypeOf(1); // TypeError
Reflect.setPrototypeOf(1); // TypeError

var myFrozenObj = new FancyThing();
Object.freeze(myFrozenObj);

Object.setPrototypeOf(myFrozenObj); // TypeError
assert(Reflect.setPrototypeOf(myFrozenObj) === false);

Reflect.isExtensible (target)

好的,这个接口只是 Object.isExtensible 的替代,但是要稍微复杂一点。在 ES6 之前(也就是… ES5),如果你传入非对象(typeof target !== 'object'),Object.isExtensible 会抛出 TypeError。ES6 修改了这个接口的语义(啊!改了现有的 API!),所以传入非对象参数给出 Object.isExtensible 会返回 false,因为非对象都不能扩展。所以像 Object.isExtensible(1) === false 这样的代码会报错,而 ES6 下语句才会和期望一样执行(返回 true)。

上面简单的历史课的重点在于,Reflect.isExtensible 使用了 _老_ 的行为,也就是说对非对象报错。我不清楚为什么,但就是这样。所以技术上讲,Reflect.isExtensible 的语义和 Object.isExtensible 不一样,不过 Object.isExtensible 也改了。接下来看下代码:

var myObject = {};
var myNonExtensibleObject = Object.preventExtensions({});

assert(Reflect.isExtensible(myObject) === true);
assert(Reflect.isExtensible(myNonExtensibleObject) === false);
Reflect.isExtensible(1); // 抛出 TypeError
Reflect.isExtensible(false);  // 抛出 TypeError

// 使用 Object.isExtensible
assert(Object.isExtensible(myObject) === true);
assert(Object.isExtensible(myNonExtensibleObject) === false);

// ES5 Object.isExtensible 语义
Object.isExtensible(1); // 在老的浏览器抛出 TypeError
Object.isExtensible(false);  // 在老的浏览器抛出 TypeError

// ES6 Object.isExtensible 语义
assert(Object.isExtensible(1) === false); // 只在新的浏览器上通过
assert(Object.isExtensible(false) === false); // 只在新的浏览器上通过

Reflect.preventExtensions ( target )

这是从 Object 上借来的最后一个反射相关方法。和 Reflect.isExtensible 的故事类似,Object.preventExtensions 对于非对象会报错,但在 ES6 上会将值返回。而 Reflect.preventExtensions 和老的 ES5 行为相同,对于非对象报错。同时,Object.preventExtensions 可能会报错异常,而 Reflect.preventExtensions 只是简单返回 truefalse,基于操作是否成功,从而可以优化处理失败的场景。

var myObject = {};
var myObjectWhichCantPreventExtensions = magicalVoodooProxyCode({});

assert(Reflect.preventExtensions(myObject) === true);
assert(Reflect.preventExtensions(myObjectWhichCantPreventExtensions) === false);
Reflect.preventExtensions(1); // 抛出 TypeError
Reflect.preventExtensions(false);  // 抛出 TypeError

// 使用 Object.isExtensible
assert(Object.isExtensible(myObject) === true);
Object.isExtensible(myObjectWhichCantPreventExtensions); // 抛出 TypeError

// ES5 Object.isExtensible 语义
Object.isExtensible(1); // 抛出 TypeError
Object.isExtensible(false);  // 抛出 TypeError

// ES6 Object.isExtensible 语义
assert(Object.isExtensible(1) === false);
assert(Object.isExtensible(false) === false);

Reflect.enumerate ( target )

更新:这个接口在 ES2016(也就是 ES7)中移除了。myObject[Symbol.iterator]() 是唯一用于枚举对象的 key 或 value 的方法。

终于有一个全新的反射方法了!Reflect.enumerateSymbol.iterator 函数有相同的语义(上一篇文章有讨论),都使用了隐藏的 [[Enumerate]] 方法。也就是说,Reflect.enumerate 的唯一替代是 myObject[Symbol.iterator](),处理 Symbol.iterator 可以被隐藏,而 Reflect.enumerate 不能。可以这样使用:

var myArray = [1, 2, 3];
myArray[Symbol.enumerate] = function () {
  throw new Error('Nope!');
}
for (let item of myArray) { // 报错:Nope!
}
for (let item of Reflect.enumerate(myArray)) {
  // 1 然后 2 然后 3
}

Reflect.get ( target, propertyKey [ , receiver ])

Reflect.get 也是全新的方法。它是个很简单的方法,用来调用 target[propertyKey]。如果 target 不是对象,函数报错。这很有帮助,因为目前如果执行 1['foo'] 这样的代码,只会静静返回 undefined,而 Reflect.get(1, 'foo') 会抛出 TypeError!一个有趣的部分是 Reflect.get 的参数,在 target[propertyKey] 是一个 getter 函数时,会作为 this 参数应用,例如:

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
}

assert(Reflect.get(myObject, 'foo') === 1);
assert(Reflect.get(myObject, 'bar') === 2);
assert(Reflect.get(myObject, 'baz') === 3);
assert(Reflect.get(myObject, 'baz', myObject) === 3);

var myReceiverObject = {
  foo: 4,
  bar: 4,
};
assert(Reflect.get(myObject, 'baz', myReceiverObject) === 8);

// 非对象报错:
Reflect.get(1, 'foo'); // throws TypeError
Reflect.get(false, 'foo'); // throws TypeError

// 老方式并不会报错:
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);

Reflect.set ( target, propertyKey, V [ , receiver ] )

你大概猜到这个方法的用途了。它是 Reflect.get 的同胞,接收额外的参数,也就是用于设置的值。和 Reflect.get 相同,Reflect.set 对于非对象也会报错,也会在 target[propertyKey]setter 函数时将 receiver 参数作为 this 使用。代码示例:

var myObject = {
  foo: 1,
  set bar(value) {
    return this.foo = value;
  },
}

assert(myObject.foo === 1);
assert(Reflect.set(myObject, 'foo', 2));
assert(myObject.foo === 2);
assert(Reflect.set(myObject, 'bar', 3));
assert(myObject.foo === 3);
assert(Reflect.set(myObject, 'bar', myObject) === 4);
assert(myObject.foo === 4);

var myReceiverObject = {
  foo: 0,
};
assert(Reflect.set(myObject, 'bar', 1, myReceiverObject));
assert(myObject.foo === 4);
assert(myReceiverObject.foo === 1);

// 非对象报错:
Reflect.set(1, 'foo', {}); // 抛出 TypeError
Reflect.set(false, 'foo', {}); // 抛出 TypeError

// 老方式不报错:
1['foo'] = {};
false['foo'] = {};
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);

Reflect.has ( target, propertyKey )

Reflect.has 是一个有趣的方法,因为它和 in 操作符(不在循环中使用)有相同的功能。两者都是用 [[HasProperty]] 内部方法,并且在 target 不是对象时报错。所以好像只在更喜欢函数调用方式的时候,才会选择 Reflect.has 而非 in,不过下一批文章中,你会看到在其他地方会有更重要的应用。来看使用:

myObject = {
  foo: 1,
};
Object.setPrototypeOf(myObject, {
  get bar() {
    return 2;
  },
  baz: 3,
});

// 没有 Reflect.has
assert(('foo' in myObject) === true);
assert(('bar' in myObject) === true);
assert(('baz' in myObject) === true);
assert(('bing' in myObject) === false);

// 使用 Reflect.has:
assert(Reflect.has(myObject, 'foo') === true);
assert(Reflect.has(myObject, 'bar') === true);
assert(Reflect.has(myObject, 'baz') === true);
assert(Reflect.has(myObject, 'bing') === false);

Reflect.ownKeys ( target )

这篇文章前面已经讨论过这个接口了,Reflect.ownKeys 实现了 [[OwnPropertyKeys]],而后者是 Object.getOwnPropertyNamesObject.getOwnPropertySymbols 的结合。这使得 Reflect.ownKeys 特别有用。我们来看下:

var myObject = {
  foo: 1,
  bar: 2,
  [Symbol.for('baz')]: 3,
  [Symbol.for('bing')]: 4,
};

assert.deepEqual(Object.getOwnPropertyNames(myObject), ['foo', 'bar']);
assert.deepEqual(Object.getOwnPropertySymbols(myObject), [Symbol.for('baz'), Symbol.for('bing')]);

// 不使用 Reflect.ownKeys:
var keys = Object.getOwnPropertyNames(myObject).concat(Object.getOwnPropertySymbols(myObject));
assert.deepEqual(keys, ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

// 使用 Reflect.ownKeys:
assert.deepEqual(Reflect.ownKeys(myObject), ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

结论

我们审视了每一个 Reflect 方法。我们发现有些方法是已存在的方法的新版本,或许稍有变化,有些则是全新的方法,开放了 JavaScript 新层次的反射。如果你愿意,可以完全丢开 Object.*Function.* 方法,改用 Reflect 对应方法,如果不愿意,没关系,也不会出问题。

现在,我不想你两手空空地离开。如果你想使用 Reflect,那么听我说,作为这篇文章的部分工作,我为 ESlint 提交了一个 pull request,作为 v1.0.0,ESlint 加入了一条 prefer-reflect 规则,使用它 ESlint 会告诉在哪里用了老版本的 Reflect 方法。你可以看下我的 eslint-config-strict 配置,已经打开了 prefer-reflect(以及一些其他的规则)。当然,如果你打算使用 Reflect,你可能会需要 polyfill,还好现在已经有比较好的 polyfill 库,例如 core-jsharmony-reflect

你怎么看新的 Reflect API 呢?打算在你的项目中使用了吗?可以通过本文后面的评论告诉我,或者在 Twitter 上联系我 @keithamus

哦,别忘了,本系列的第三篇也是最后一篇文章,“Part 3 Proxies”,马上要发布了,我尽量不再花 2 个月来发布了!

最后,感谢 @mttshw@WebReflection 对本文的审阅,使得这篇文章变得更好。