摘要:抽象操作是在調用函數對象的內部的方法。指的是調用函數,指的是的值,然后是傳入到內部方法相應參數的值。切換上下文執行,為函數調用棧在尾部調用函數做準備,切換運行中執行上下文,實現上下文的動態改變。萬事具備,執行,調用函數即可。
今天在學習前端工程化的過程中,遇到一個是實驗中的css屬性:fullscreen,有這樣一個例子:fullscreen偽元素官方demo
:fullscreen Demo
This text will become big and red when the browser is in fullscreen mode.
其中有一段代碼:
function enterFullscreen() { fullscreenFunc.call(fullscreenDiv); }?
雖然結合上下文能看出來是為了兼容瀏覽器的fullscreen API,但是其中的Function.prototype.call()我自己其實沒有特別深究過。
為什么不直接fullscreenFunc(),這樣不能使得fullscreenDiv全屏嗎?
大家都說call與apply都是為了動態改變this的,僅僅是傳入參數的方式不同,call傳入(this,foo,bar,baz),而apply傳入(this,[foo,bar,baz])那么事實真如大家所說的那樣嗎?既然apply能動態改變this,那么為什么還要多此一舉開放一個call?
這其中肯定隱藏著一些秘密,那就是有些事情是apply做不到,而call可以勝任的。
繼續我們的啃規范之旅,去深入到Function.prototype.call()的內部,徹底把它搞清楚。
When the?call?method is called on an object?func with argument,?thisArg?and zero or more?args, the following steps are taken:
If?IsCallable(func) is?false, throw a?TypeError?exception.
Let?argList?be an empty?List.
If this method was called with more than one argument then in left to right order, starting with the second argument, append each argument as the last element of?argList.
Perform?PrepareForTailCall().
Return?Call(func,?thisArg,?argList).
The?length?property of the?call?method is?1.
當call方法在帶參數的對象的方法上調用時,thisArg和零個或者對個參數,會進行如下的步驟:
如果IsCallable(func)返回false,拋出TypeError異常。
定義argList為一個空的列表。
如果方法按照從左到右傳入的參數個數不止一個,從第二個參數開始,依次將每個參數從尾部添加到argList數組。
執行PrepareForTailCall()
返回Call(func,thisArg,argList)
有3個點看不懂:
IsCallable(func)
PrepareForTailCall()
Call(func,thisArg,argList)
這些同樣在規范中有對應描述:
7.2.3IsCallable ( argument )The abstract operation IsCallable determines if?argument, which must be an?ECMAScript language valueor a?Completion Record, is a callable function with a [[Call]] internal method.
重點在于is a callable function with a [[Call]] internal method.,也就是說執行isCallable(func)運算的func,如果函數內部有一個內在的[[Call]]方法,那么運算結果為true,也就是說這個函數是可調用的的。(callable)
14.6.3Runtime Semantics: PrepareForTailCall ( )The abstract operation PrepareForTailCall performs the following steps:
Let?leafContext?be?the running execution context.
Suspend?leafContext.
Pop?leafContext?from?the execution context stack. The?execution context?now on the top of the stack becomes?the running execution context.
Assert:?leafContext?has no further use. It will never be activated as?the running execution context.
A tail position call must either release any transient internal resources associated with the currently executing function?execution context?before invoking the target function or reuse those resources in support of the target function.
ReturnIfAbrupt(argument).
If?Type(argument) is not Object, return?false.
If?argument?has a [[Call]] internal method, return?true.
Return?false.
雖然看不懂,但還是得硬著頭皮學習一波。
抽象操作PrepareForTailCall執行以下幾個步驟:
讓葉子上下文成為運行中的執行上下文
暫停葉子上下文
頂葉子上下文來自執行上下文的堆。當前的在堆頂部的執行上下文成為運行中的執行上下文
斷言:葉子上下文沒有其他作用。它再也不會作為運行中執行上下文被激活。
在調用目標函數或者重用這些資源去支持目標函數之前,尾部位置調用必須釋放與當前執行函數上下文相關的瞬態內部資源。
ReturnIfAbrupt(argument).
如果Type(argument)不是對象,返回false。
如果argument含有[[call]]內部方法,返回true。
返回 false
看懂一個大概,是為了在函數調用棧的尾部調用當前函數做準備,其中的運行中執行上下文,正是我們所說的this動態改變的原因,因為本質上this改變并不僅僅是指向的對象發生變化,而是連帶著與其相關的上下文都發生了變化。
所以說,這一步是this動態改變的真正原因。
7.3.12Call(F, V, [argumentsList])The abstract operation Call is used to call the [[Call]] internal method of a function object. The operation is called with arguments?F,?V?, and optionally?argumentsList?where?F?is the function object,?V?is an?ECMAScript language value?that is the?this?value of the [[Call]], and?argumentsList?is the value passed to the corresponding argument of the internal method. If argumentsList?is not present, an empty?List?is used as its value. This abstract operation performs the following steps:
ReturnIfAbrupt(F).
If?argumentsList?was not passed, let?argumentsList?be a new empty?List.
If?IsCallable(F) is?false, throw a?TypeError?exception.
Return?F.[[Call]](V,?argumentsList).
Call抽象操作是在調用函數對象的內部的[[Call]]方法。這個操作參數類型包括F,V以及可選的argumentList。F指的是調用函數,V指的是[[Call]]的this值,然后argumentsList是傳入到[[Call]]內部方法相應參數的值。如果argumentList不存在,那么argumentList將被置為一個空數組。這個方法按照下列幾步執行:
ReturnIfAbrupt(F)
如果沒傳入argumentList,那么argumentList將會被置為一個空數組。
如果IsCallable(F)是false,返回TypeError異常。
返回 F.[[call]](V,argumentsList).
所以Function.prototype.call(this,...args)執行過程現在很明了:
判斷傳入的func是否有[[call]]屬性,有[[call]]才意味著函數能被調用,否則拋出TypeError異常。
定義argList為一個空的列表。
傳參:如果方法按照從左到右傳入的參數個數不止一個,從第二個參數開始,依次將每個參數從尾部添加到argList數組。
切換this上下文:執行PrepareForTailCall(),為函數調用棧在尾部調用函數做準備,切換運行中執行上下文,實現this上下文的動態改變。
萬事具備,執行Call(func,thisArg,argList),調用函數即可。
回到我們的例子:
fullscreenFunc.call(fullscreenDiv);
func為fullscreenDiv DOM 節點的方法:"requestFullscreen" || "mozRequestFullScreen" || "msRequestFullscreen"
|| "webkitRequestFullScreen",由于是fullscreen API,所以isCallable(func)返回true。
定義一個argList空數組用來傳參。
傳參:由于fullscreenFunc.call(fullscreenDiv);只有一個參數,所以直接傳入argList空數組。
切換this上下文:停止當前的this葉子上下文,也就是window,切換到fullscreenDiv的執行上下文。
由于當前瀏覽器為chrome,因此執行 fullscreenDiv.webkitRequestFullscreen.[[call]](this,[])。
因此我們之前提的那個為什么不直接fullscreenFunc(),這樣不能使得fullscreenDiv全屏嗎?,答案就很清楚了?不能。
為什么呢?
var fullscreenFunc = fullscreenDiv.requestFullscreen; if (!fullscreenFunc) { ["mozRequestFullScreen", "msRequestFullscreen","webkitRequestFullScreen"].forEach(function (req) { fullscreenFunc = fullscreenFunc || fullscreenDiv[req]; }); }
下面的代碼,僅僅是獲得了fullscreenDiv對象的fullscreen request API的引用,而fullscreenFunc的作用域是全局的window對象,也就是this的當前指向為window。
而我們是想觸發window的子對象fullscreenDiv的全屏方法,所以需要將this上下文切換為fullscreenDiv,這就是不直接調用fullscreenFunc(),需要fullscreenFunc.call(fullscreenDiv)的原因。
最近在看龍書,第一章講到動態語言與靜態語言的區別,龍書中講到"運行時決定作用域的語言是動態語言,在編譯時指定作用域的預言是靜態語言"。例子中的以function關鍵字定義的類,this運行中執行上下文的切換,恰恰證明了javascript是一門動態語言;再舉個形象的靜態語言的例子,java會使用class關鍵字構建類,在類內部使用private,public等關鍵字去指定作用域,編譯時就會去約束其作用域,具有非常強的約束性,this始終指向當前類。
剛才和一個java后端同事確認,java也有this關鍵字,但是僅能使用當前類中的方法,B類可以調用A類中的方法,比如通過super實現對父類的繼承,但是當前類中的this指向是不會變的。
js中的this,是可以通過call或者apply進行動態切換從而去調用其他類中的方法的,B類不能調用A類中的方法。(注意:我們這里的類指的是以function關鍵字進行定義的類,暫時不考慮es6的class關鍵字構造類的方式。)
說了這么多,我們再來強調下重點:
加粗的部分是重點!
加粗的部分是重點!
加粗的部分是重點!
拋開V8引擎內部執行call和apply的原理不說,二者最終實現的都是this上下文的動態切換,所以就像大家所說的那樣,都是動態改變this。我們只要心里知道,其實二者在背后實現動態切換this的操作部分有很大的不同就可以了,當出現由于內部實現細節引起的問題時,我們可以快速定位。
That"s it !
2019.8.20更新
js忍者秘籍給出的精簡解釋是:“js可以通過apply和call顯示指定任意對象作為其函數上下文。”強烈建議閱讀P52~P55。言簡意賅,通俗易懂。
主要有兩個用途:
普通函數中指定函數上下文
回調函數中強制指定函數上下文
回調函數強制指定函數上下文很好地體現了函數式編程的思想,創建一個函數接收每個元素,并且對每個元素做處理。
本質上,apply和call都是為了增強代碼的可擴展性,提升編程的效率。
我想這也是js中每一個方法或者api的初衷,提供更加便利的操作,解放初更多的生產力。不斷加入新方法的es規范也是這個初衷。
由于我使用vue比較多,所以根據以上的應用場景出1個單文件組件示例和1個普通示例供參考:
// 普通函數中指定函數上下文 // 通過Math.max()獲得數組中的最大項
// 回調函數中強制指定函數上下文 // 手動實現一個Array.prototype.filter const numbers = [1, 2, 3, 4]; function arrayFilter(array, callback) { const result = []; for (let i = 0; i < array.length; i++) { const validate = callback.call(array[i], array[i]); if (validate) { result.push(array[i]); } } return result; } const evenArrays = arrayFilter(numbers, (n) => n % 2 === 0); console.log(evenArrays);// [2, 4]
期待和大家交流,共同進步,歡迎大家加入我創建的與前端開發密切相關的技術討論小組:
SegmentFault專欄:趁你還年輕,做個優秀的前端工程師
Github博客: 趁你還年輕233的個人博客
掘金主頁:趁你還年輕233
SegmentFault技術圈: ES新規范語法糖
知乎專欄:趁你還年輕,做個優秀的前端工程師
前端開發交流群:660634678
努力成為優秀前端工程師!
加油,前端同學們!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94785.html
摘要:如果是或者,會將作為值。否則,被調用的函數,進行轉換后,作為值。又怎么操作這個很神奇。能轉換它的參數為到總共個整數中的一個,這個函數遵循以下規則。不斷加入新方法的規范也是這個初衷。 showImg(https://segmentfault.com/img/remote/1460000012563719); 今天看element-react源碼的時候,又看到了這張似曾相識卻又異常陌生的老...
摘要:搞懂的譯可能是初學的人最不關心的函數,當你意識到需要保持在其他函數中的上下文,實際上你需要的是。這就是問題所在。整合事件綁定和一個重大提高就是,和等等。然而,并沒有原生添加事件到多個節點的方式。能力有限,如有疑問,紕漏,速指出,感謝你 搞懂JavaScript的Function.prototype.bind[譯] Ben Howdle binding可能是初學Javascript的人最...
摘要:等價與注意如果構造函數有自己的返回,那么情況有所不同。,定義了的屬性,默認是聲明的函數名,匿名函數是。匿名函數表達式和函數聲明都不會創建匿名作用域。 ECMAScript規范中對Function的文檔描述,我認為是ECMAScript規范中最復雜也是最不好理解的一部分,它涉及到了各方面。光對Function就分了Function Definitions、Arrow Function D...
摘要:眾所周知,這三個函數都是改變執行上下文的,那么我們來捋一捋,這些函數內部到底做了什么。 前言 稍微翻了一下call,apply, bind 的各種論壇上的文章, 發現講的都太淺了,大部分都只講了個用法, 對于實現的原理卻都沒有提,因此,在這里,我寫下這篇文章, 希望能讓大家認識到原理所在。 眾所周知, 這三個函數都是改變執行上下文的 , 那么我們來捋一捋,這些函數內部到底做了什么。 c...
閱讀 3274·2021-11-23 09:51
閱讀 939·2021-09-03 10:30
閱讀 3212·2021-08-31 09:40
閱讀 3278·2019-08-30 14:22
閱讀 902·2019-08-30 14:09
閱讀 2900·2019-08-30 13:21
閱讀 3232·2019-08-28 18:03
閱讀 2859·2019-08-26 13:44