乐趣区

关于javascript:ES6-元编程Reflect

咱们来聊一聊反射:

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

反射(Reflect) 是新的全局对象(相似 JSONMath),提供了一系列有用的内省办法(内省其实就是“找货色”的一种高级说法)。JavaScript 中曾经有内省工具了,如 Object.keysObject.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.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-js 和 harmony-reflect。

退出移动版