探索 Reflect.apply 與 Function.prototype.apply 的區別

来源:https://www.cnblogs.com/BlackStorm/archive/2019/12/03/11975502.html
-Advertisement-
Play Games

探索 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() 給函數的第二個傳參「參數數組」是可選的,當不需要傳遞參數給被調用的函數時,可以不傳或傳遞 nullundefined 值。而由於 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 參數變現會有所不同,若它的值是 nullundefined ,則會被自動替換為全局對象(瀏覽器下為 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 () { 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() 更規整易用,它有如下特點:

  1. 直觀易讀,將被調用函數放在參數中,貼近函數式風格;
  2. 異常處理具有一致性,無歧義;
  3. 所有參數必傳,編譯期錯誤檢查和類型推斷更友好。

如今 Vue.js 3 也在其響應式系統中大量使用 Proxy 和 Reflect 了,期待不久的將來 Reflect 會在前端世界中大放異彩!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • switch功能:簡單條件判斷,比if判斷標簽少些不等於相同功能,視個人習慣而用。 ...
  • 實例方法 >必須要通過new的方式創建的對象(實例對象)來調用的方法 靜態方法 >直接通過大寫的構造函數的名字調用的方法(直接通過大寫的對象名字調用的) 字元串的常用屬性: .length >字元串的長度 var str = "12345"; console.log(str.length); .ch ...
  • 1、是否合法IP地址 export function validateIP(rule, value,callback) { if(value==''||value==undefined||value==null){ callback(); }else { const reg = /^(\d{1,2} ...
  • 導言 在一個風和日麗的一天,看完了瘋狂HTML 5+CSS 3+JavaScript講義,跟著做了書里最後一章的俄羅斯方塊小游戲,並做了一些改進,作為自己前端學習的第一站。 游戲效果: 製作思路 因為書里的俄羅斯方塊比較普通,太常規了,不是很好看,所以我在網上找了上面那張圖片,打算照著它來做。(請無 ...
  • 話不多說,先上圖 背景: 微信聊天,經常會遇見視頻發不了,嗯,還有聊天不方便的問題,於是我就自己買了伺服器,部署了一套可以直接在微信打開的網頁進行聊天,這樣只需要發送個url給朋友,就能聊天了! 由於自己無聊弄著玩的,代碼比較粗糙,各位多指正! 1、首先安裝SignalR,這步我就不做過多說明瞭 安 ...
  • String >是一個對象 字元串可以看成是字元組成的數組, 但是js中沒有字元類型 字元是一個一個的, 在別的語言中字元用一對單引號括起來 在js中字元串可以使用單引號也可以使用雙引號 因為字元串可以看成是數組, 所以, 可以通過for迴圈進行遍歷 字元串特性: 不可變性, 字元串的值是不能改變 ...
  • 常見 Uncaught TypeError: Cannot read property 'trim' of undefined 報錯原因及解決方案 ...
  • 格式化後的指定格式的日期和時間,封裝一個函數 function getDate() { var dt = new Date(); var year = dt.getFullYear(); var month = dt.getMonth(); var date = dt.getDate(); var ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...