探索 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
thismay not be the actual value seen by the method: if the method is a function in non-strict mode code,nullandundefinedwill 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 的区别的更多相关文章
- Function.prototype.apply.call 理解分析
首先需要了解apply,call的基本用法,其目的是改变调用方法中的this指向,将其指向为传入的对象,改变this的指向,两种方法接收参数的方式不同. 代码:console.log var cons ...
- javascript中 Function.prototype.apply()与Function.prototype.call() 对比详解
Function.prototype.apply()|Function.prototype.call() apply()方法可以在使用一个指定的 this 值和一个参数数组(或类数组对象)的前提下调用 ...
- 关于Function.prototype.apply.call的一些补充
宿主对象,在javascript中有三类对象,本地对象,内置对象和宿主对象.其他两类暂且不提,宿主对象是指什么呢(DOM BOM),控制台对象是文档对象模型的扩展,也被认为是宿主对象.那么,它们有什么 ...
- Function.prototype.apply.call
我们先从一道简单的题目开始,前几天在git上看到的: 定义log方法,它可以代理console.log的方法.log(1,2,3) => 1 2 3 通常,你的答案会是这样的: functi ...
- Function.prototype.apply()
文章地址:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply ...
- Function.prototype.call 和 Function.prototype.apply 的区别
call和apply函数是function函数的基本属性,都可以用于更改函数对象和传递参数,是前端工程师常用的函数.具体使用方法请参考以下案列: 例如: 申明函数: var fn = function ...
- 箭头函数表达式和声名式函数表达式的区别以及 Function.prototype的bind, apply,call方法
箭头函数不能用做构造函数 箭头函数没有arguments参数 箭头函数没有自己的this,是从作用域链上取this,是与箭头函数定义的位置有关的,与执行时谁调用无关,所以用call,apply,bin ...
- Function.prototype.call.apply作用详解
关于call()和apply()基本用法可以参阅如下两篇文章: (1).call方法参阅JavaScript call()一章节. (2).apply方法参阅JavaScript apply()一章节 ...
- Function.prototype.call.apply()方法
在看uncurrying化函数时候,碰到了Function.prototype.call.apply()的用法: 先说说uncurrying()函数: Function.prototype.uncur ...
随机推荐
- join的使用
我们还是以这四张表为例:1.left joinselect * from student a left join score b on a.sid=b.sid; 也就是说A中的元素都会显示,没有值的用 ...
- MacOS 系统 文件夹解析
Mac OS X,基于UNIX核心的系统,增强了系统的稳定性.性能以及响应能力. 通过对称多处理技术充分发挥双处理器的优势,提供无与伦比的2D.3D和多媒体图形性能以及广泛的字体支持和集成的PDA功能 ...
- Java连接MySQL简单步骤及常见问题解析
这篇文章跟读者们介绍Java语言如何连接到MySQL数据库相关操作,及FAQ(Frequently Asked Questions)介绍. 选择工具和准备数据集 IDE:Eclipse 数据库:使用的 ...
- nodejs sql --- 添加事务
添加事务 let transaction; try { transaction = await this.ctx.model.transaction(); await this.ctx.model.C ...
- TICK技术栈(二)Telegraf安装及使用
1.什么是Telegraf? Telegraf是一个用Go语言开发的代理程序,可用于收集和报告指标.Telegraf插件直接从其运行的系统中获取各种指标,从第三方API中提取指标,甚至通过StatsD ...
- Unity调用Android相册
最近有一个项目有这个需求,让用户上传自己的交易凭证的截图,之前因为对调Android原生的东西不太熟悉,就先放了一边 因为项目已经上线,只不过是该功能未开放而已,那么现在为什么要写这篇博客呢,是因为. ...
- 通过Value获取Map中的键值Key的四种方法
1 简介 我们都知道Map是存放键值对<Key,Value>的容器,知道了Key值,使用方法Map.get(key)能快速获取Value值.然而,有的时候我们需要反过来获取,知道Value ...
- 在VMware15中安装虚拟机并使用Xshell连接到此虚拟机(超详细哦)
首先点击创建新的虚拟机. 此处默认, 点击下一步 默认, 点击下一步 此处可以设置你的虚拟机名称和安装位置(强烈建议不要将安装位置放在系统盘). 此处可根据自己的电脑配置来设置(建议2,4),后续可以 ...
- 我的【Java】面试日记
背景 在老东家五年了,总共工作整七年,经历两家公司.2019-10-31日离职.公司规模较小,项目压力不大,非985/211毕业,统招本科,计算机专业.目标:中大型公司,最好是大厂,嘿嘿,不过不抱希望 ...
- 持续集成Gitlab CICD Runner&Jenkins
目录 使用Gitlab Runner实现 再要部署的服务器上安装 gitlab runner 下载可执行文件 设置可执行权限权限 创建用户 运行服务 注册 Runner 到gitlab上找到需要用的U ...