分析ES5和ES6的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(u程式設計客棧ndefined,[]) // 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') },{}) // test3 Reflect.apply(function (text) { console.log(text) },['test4']) // test4
非嚴格模式
由文件可知,function.apply()在非嚴格模式下thisArg引數變現會有所不同,若它的值是null或undefined,則會被自動替換為全域性物件(瀏覽器下為window),而基本資料型別值則會被自動包裝(如字面量1的包裝值等價於Number(1))。
(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引數傳遞不可呼叫的物件,如果補齊了第二、第三個引數,則丟擲的異常描述與上述完全不同:
不過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.ap程式設計客棧ply(console.log)
// Thrown:
// TypeError: CreateListFromArrayLike called on non-object
實際使用
雖然目前沒有在Proxy以外的場景看到更多的使用案例,但相信在相容性問題逐漸變得不是問題的時候,使用率會得到逐漸上升。
我們可以發現 ES6Reflect.apply()的形式相較於傳統 ES5 的用法,會顯得更直觀、易讀了,讓人更容易看出,一行程式碼希望使用哪個函式,執行預期的行為。
// ES5 Function.prototype.apply.call(<Function>,[...]) <Function>.apply(undefined,[...]) // ES6 Reflect.apply(<Function>,[...])
我們選擇常用的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,[104,101,108,111,32,119,114,100,33] ) // 'hello world!'
對於可傳多個引數的函式如Math.max()等可能會更有用,如:
const arr = [1,1,2,3,5,8] Reflect.apply(Math.max,arr) // 8 Function.prototype.apply.call(Math.max,arr) // 8 Math.max.apply(undefined,arr) // 8
但由於語言標準規範沒有指定最大引數個數,如果傳入太大的陣列的話也可能報超過棧大小的錯誤。這個大小因平臺和引擎而異,如 PC 端 node.js可以達到很大的大小,而手機端的jsC 可能就會限制到 65536 等。
const arr = new Array(Math.floor(2**18)).fill(0) // [ // 0,// ... 262140 more items // ] Reflect.apply(Math.max,null,arr) // Thrown: // RangeError: Maximum call stack size exceeded
總結
ES6 新標準提供的Reflect.apply()更規整易用,它有如下特點:
1.直觀易讀,將被呼叫函式放在引數中,貼近函式式風格;
2.異常處理具有一致性,無歧義;
3.所有引數必傳,編譯期錯誤檢查和型別推斷更友好。
如今vue.js 3 也在其響應式系統中大量使用 Proxy 和 Reflect 了,期待不久的將來 Reflect 會在前端世界中大放異彩!
以上就是分析ES5和ES6的apply區別的詳細內容,更多關於ES5和ES6區別的資料請關注我們其它相關文章!