探索 Reflect.apply 与 Function.prototype.apply 的区别
众所周知, ES6 新增了一个全局、内建、不可构造的 Reflect 对象,并提供了其下一系列可被拦截的操作方法。其中一个便是 Reflect.apply() 了。下面探究下它与传统 ES5 的 Function.prototype.apply() 之间有什么异同。
函数签名MDN 上两者的函数签名分别如下:
Reflect.apply(target, thisArgument, argumentsList) function.apply(thisArg, [argsArray])而 TypeScript 定义的函数签名则分别如下:
declare namespace Reflect { function apply(target: Function, thisArgument: any, argumentsList: ArrayLike<any>): any; } interface Function { apply(this: Function, thisArg: any, argArray?: any): any; }它们都接受一个提供给被调用函数的 this 参数和一个参数数组(或一个类数组对象, array-like object )。
可选参数可以最直观看到的是, function.apply() 给函数的第二个传参「参数数组」是可选的,当不需要传递参数给被调用的函数时,可以不传或传递 null 、 undefined 值。而由于 function.apply() 只有两个参数,所以实践中连第一个参数也可以一起不传,原理上可以在实现中获得 undefined 值。
(function () { console.log('test1') }).apply() // test1 (function () { console.log('test2') }).apply(undefined, []) // test2 (function () { console.log('test3') }).apply(undefined, {}) // test3 (function (text) { console.log(text) }).apply(undefined, ['test4']) // test4而 Reflect.apply() 则要求所有参数都必传,如果希望不传参数给被调用的函数,则必须填一个空数组或者空的类数组对象(纯 JavaScript 下空对象也可以,若是 TypeScript 则需带上 length: 0 的键值对以通过类型检查)。
Reflect.apply(function () { console.log('test1') }, undefined) // Thrown: // TypeError: CreateListFromArrayLike called on non-object Reflect.apply(function () { console.log('test2') }, undefined, []) // test2 Reflect.apply(function () { console.log('test3') }, undefined, {}) // test3 Reflect.apply(function (text) { console.log(text) }, undefined, ['test4']) // test4 非严格模式由文档可知, function.apply() 在非严格模式下 thisArg 参数变现会有所不同,若它的值是 null 或 undefined ,则会被自动替换为全局对象(浏览器下为 window ),而基本数据类型值则会被自动包装(如字面量 1 的包装值等价于 Number(1) )。
Note that this may not be the actual value seen by the method: if the method is a function in code, and will be replaced with the global object, and primitive values will be boxed. This argument is not optional
(function () { console.log(this) }).apply(null) // Window {...} (function () { console.log(this) }).apply(1) // Number { [[PrimitiveValue]]: 1 } (function () { console.log(this) }).apply(true) // Boolean { [[PrimitiveValue]]: true } 'use strict'; (function () { console.log(this) }).apply(null) // null (function () { console.log(this) }).apply(1) // 1 (function () { console.log(this) }).apply(true) // true但经过测试,发现上述该非严格模式下的行为对于 Reflect.apply() 也是有效的,只是 MDN 文档没有同样写明这一点。
异常处理Reflect.apply 可视作对 Function.prototype.apply 的封装,一些异常判断是一样的。如传递的目标函数 target 实际上不可调用、不是一个函数等等,都会触发异常。但异常的表现却可能是不一样的。
如我们向 target 参数传递一个对象而非函数,应当触发异常。
而 Function.prototype.apply() 抛出的异常语义不明,直译是 .call 不是一个函数,但如果我们传递一个正确可调用的函数对象,则不会报错,让人迷惑 Function.prototype.apply 下到底有没有 call 属性?
Function.prototype.apply.call() // Thrown: // TypeError: Function.prototype.apply.call is not a function Function.prototype.apply.call(console) // Thrown: // TypeError: Function.prototype.apply.call is not a function Function.prototype.apply.call(console.log) ///- 输出为空,符合预期Function.prototype.apply() 抛出的异常具有歧义,同样是给 target 参数传递不可调用的对象,如果补齐了第二、第三个参数,则抛出的异常描述与上述完全不同:
Function.prototype.apply.call(console, null, []) // Thrown: // TypeError: Function.prototype.apply was called on #<Object>, which is a object and not a function Function.prototype.apply.call([], null, []) // Thrown: // TypeError: Function.prototype.apply was called on [object Array], which is a object and not a function Function.prototype.apply.call('', null, []) // Thrown: // TypeError: Function.prototype.apply was called on , which is a string and not a function不过 Reflect.apply() 对于只传递一个不可调用对象的异常,是与 Function.prototype.apply() 全参数的异常是一样的:
Reflect.apply(console) // Thrown: // TypeError: Function.prototype.apply was called on #<Object>, which is a object and not a function