探索 Reflect.apply 與 Function.prototype.apply 的區別 眾所周知, ES6 新增了一個全局、內建、不可構造的 對象,並提供了其下一系列可被攔截的操作方法。其中一個便是 了。下麵探究下它與傳統 ES5 的 之間有什麼異同。 函數簽名 MDN 上兩者的函數簽名分別 ...
探索 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 會在前端世界中大放異彩!