探索 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 and undefined will be replaced with the global object, and primitive values will be boxed. This argument is not optional
  
  (function (www.moyouyul.cn  ) { console.log(this) }).apply(null)
  
  // Window {...}
  
  (function (www.chaoyul.com) { console.log(this) }).apply(1)
  
  // Number { [[PrimitiveValue]]: 1 }
  
  (function (www.jinyang3.xyz) { console.log(this) }).apply(true)
  
  // Boolean {  www.huanhua2zhuc.cn[[PrimitiveValue]]: true }
  
  'use strict';
  
  (function () { console.log(this) }).apply(null)
  
  // null
  
  (function (www.wujiu5zhuce.cn) { console.log(this) www.hdptzc.cn}).apply(1)
  
  // 1
  
  (function (www.jiuyueguojizc.cn) { 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 会在前端世界中大放异彩!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值