我们来聊一聊反射:
- 符号(symbol)是指包含在实现中的反射 - 通过向已有的类和对象中添加符号来改变其行为。
- 反射(refect)是指通过内省来实现反射 - 用于从代码中发现更低层次的信息。
- 代理(proxy)是通过拦截实现反射 - 包装对象,然后通过设置陷阱来拦截它们的行为。
反射(Reflect)
是新的全局对象(类似 JSON
、Math
),提供了一系列有用的内省方法(内省其实就是“找东西”的一种高级说法)。JavaScript 中已经有内省工具了,如 Object.keys
、Object.getOwnPropertyNames
等等。既然可以向 Object
增加方法,为什么还需要一个新的 API 呢?
“内部方法”
所有的 JavaScript 规范,以及实现引擎,都提供了一些“内部方法”。这样使得 JavaScript 引擎在处理代码时能够更有效地处理对象。如果你读过规范,你会发现到处都有类似 [[Get]]
、[[Set]]
、[[HasOwnProperty]]
这样的东西(如果你失眠的话,可以看看完整的内部方法列表,在 ES5 8.12 和 ES6 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.getOwnPropertyNames
和 Object.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.keys
、Object.getOwnPropertyNames
等一样,把这些方法添加到 Object 上呢?原因如下:
- Reflect 有不是用于 Object 的方法,例如
Reflect.apply
,作用于函数。如果是Object.apply(myFunction)
这样调用的话,看起来会很奇怪。 - 使用一个对象来包含这些方法,可以让 JavaScript 其余的部分保持简洁。这比通过构造函数和原型对象来使用反射方法(或者更糟,通过全局对象),要好一些。
typeof
、instanceof
和delete
已经作为反射运算符存在了。如果像这样增加新的关键字,对于开发者而言,不仅会觉得麻烦,对于向后兼容性来说也会是噩梦,而且会使得保留字数目暴增。
Reflect.apply ( target, thisArgument [, argumentsList] )
Reflect.apply
和 Function#apply
很像,它接收一个函数,使用一个上下文对象和参数数组来调用该函数。从这一点来说,你 _可以_ 认为 Function#call
和 Function#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 真正的好处在于其防御性:任何代码都可以简单地修改函数的 call
和 apply
方法,这使你因为崩溃的代码的可怕的变通方法而卡住。这在一般情况下并不是大问题,但下面的代码可能真的存在:
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.call
或 Constructor.apply
。Reflect.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.defineProperty
和 Object.defineProperty
很像,用于定义属性的元数据(metadata)。这个方法更适合,因为 Object.* 隐含着表示方法作用于对象字面量(其实是对象字面量构造函数),而 Reflect.defineProperty 只表示现在做的与反射有关,更具语义化。
特别需要注意的是,和 Object.defineProperty
一样, 对于非法的 target
,Reflect.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 字面量,null
或 undefined
。而 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
,成功则返回参数 target
。Reflect.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
只是简单返回 true
或 false
,基于操作是否成功,从而可以优化处理失败的场景。
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.enumerate
和 Symbol.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.getOwnPropertyNames
和 Object.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-js 和 harmony-reflect。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。