探索 Reflect.apply 与 Function.prototype.apply 的区别
探索 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 non-strict mode code,null
andundefined
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
而如果传递了正确可调用的函数,才会去校验第三个参数数组的参数;这也说明 reflect.apply()
的参数校验是有顺序的:
reflect.apply(console.log) // thrown: // typeerror: createlistfromarraylike called on non-object
实际使用
虽然目前没有在 proxy
以外的场景看到更多的使用案例,但相信在兼容性问题逐渐变得不是问题的时候,使用率会得到逐渐上升。
我们可以发现 es6 reflect.apply()
的形式相较于传统 es5 的用法,会显得更直观、易读了,让人更容易看出,一行代码希望使用哪个函数,执行预期的行为。
// es5 function.prototype.apply.call(<function>, undefined, [...]) <function>.apply(undefined, [...]) // es6 reflect.apply(<function>, undefined, [...])
我们选择常用的 object.prototype.tostring
比较看看:
object.prototype.tostring.apply(/ /) // '[object regexp]' reflect.apply(object.prototype.tostring, / /, []) // '[object regexp]'
可能有人会不同意,这不是写得更长、更麻烦了吗?关于这点,见仁见智,对于单一函数的重复调用,确实是打的代码更多了;对于需要灵活使用的场景,会更符合函数式的风格,只需指定函数对象、传递参数,即可获得预期的结果。
但是对于这个案例来说,可能还会有一点小问题:每次调用都需要创建一个新的空数组!尽管现在多数设备性能足够好,程序员不需额外考虑这点损耗,但是对于高性能、引擎又没有优化的场景,先创建一个可重复使用的空数组可能会更好:
const emptyargs = [] function gettype(obj) { return reflect.apply( object.prototype.tostring, obj, emptyargs ) }
另一个调用 string.fromcharcode()
的场景可以做代码中字符串的混淆:
reflect.apply( string.fromcharcode, undefined, [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33] ) // 'hello world!'
对于可传多个参数的函数如 math.max()
等可能会更有用,如:
const arr = [1, 1, 2, 3, 5, 8] reflect.apply(math.max, undefined, arr) // 8 function.prototype.apply.call(math.max, undefined, arr) // 8 math.max.apply(undefined, arr) // 8
但由于语言标准规范没有指定最大参数个数,如果传入太大的数组的话也可能报超过栈大小的错误。这个大小因平台和引擎而异,如 pc 端 node.js 可以达到很大的大小,而手机端的 jsc 可能就会限制到 65536 等。
const arr = new array(math.floor(2**18)).fill(0) // [ // 0, 0, 0, 0, // ... 262140 more items // ] reflect.apply(math.max, null, arr) // thrown: // rangeerror: maximum call stack size exceeded
总结
es6 新标准提供的 reflect.apply()
更规整易用,它有如下特点:
- 直观易读,将被调用函数放在参数中,贴近函数式风格;
- 异常处理具有一致性,无歧义;
- 所有参数必传,编译期错误检查和类型推断更友好。
如今 vue.js 3 也在其响应式系统中大量使用 proxy 和 reflect 了,期待不久的将来 reflect 会在前端世界中大放异彩!
推荐阅读
-
探索 Reflect.apply 与 Function.prototype.apply 的区别
-
IE6与IE7中,innerHTML获取param的区别_javascript技巧
-
批处理中Copy与Xcopy命令的区别小结
-
php 转换字符串编码 iconv与mb_convert_encoding的区别说明_PHP
-
php中const与define的使用区别 详解_PHP教程
-
如何检查您的Win11版本?Win11各版本区别与查看Win11版本方法
-
EXEC的使用与sp_executesql命令的区别
-
JavaScript 常量与变量,var、let、const的区别
-
英语单词state与status的区别
-
display:none;与visibility:hidden;的区别_html/css_WEB-ITnose