從規範去看Function.prototype.call究竟是怎麼工做的?

今天在學習前端工程化的過程當中,遇到一個是實驗中的css屬性:fullscreen,有這樣一個例子:fullscreen僞元素官方demojavascript

<div id="fullscreen">
    <h1>:fullscreen Demo</h1>
    <p>This text will become big and red when the browser is in fullscreen mode.</p>
    <button id="fullscreen-button">Enter Fullscreen</button>
</div>
<script>
var fullscreenButton = document.getElementById("fullscreen-button");
var fullscreenDiv    = document.getElementById("fullscreen");
var fullscreenFunc   = fullscreenDiv.requestFullscreen;
if (!fullscreenFunc) {
     ['mozRequestFullScreen', 'msRequestFullscreen','webkitRequestFullScreen'].forEach(function (req) {
        fullscreenFunc = fullscreenFunc || fullscreenDiv[req];
     });
}  
function enterFullscreen() {
    fullscreenFunc.call(fullscreenDiv);
} 
fullscreenButton.addEventListener('click', enterFullscreen);
</script>

其中有一段代碼:css

function enterFullscreen() {
    fullscreenFunc.call(fullscreenDiv);
} 

雖然結合上下文能看出來是爲了兼容瀏覽器的fullscreen API,可是其中的Function.prototype.call()我本身其實沒有特別深究過。前端

爲何不直接fullscreenFunc(),這樣不能使得fullscreenDiv全屏嗎?vue

你們都說call與apply都是爲了動態改變this的,僅僅是傳入參數的方式不一樣,call傳入(this,foo,bar,baz),而apply傳入(this,[foo,bar,baz])那麼事實真如你們所說的那樣嗎?既然apply能動態改變this,那麼爲何還要畫蛇添足開放一個call?
這其中確定隱藏着一些祕密,那就是有些事情是apply作不到,而call能夠勝任的。
繼續咱們的啃規範之旅,去深刻到Function.prototype.call()的內部,完全把它搞清楚。java

19.2.3.4 Function.prototype.call (thisArg , ...args)

When the  call method is called on an object func with argument, thisArg and zero or more args, the following steps are taken:
  1. If IsCallable(func) is false, throw a TypeError exception.
  2. Let argList be an empty List.
  3. 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.
  4. Perform PrepareForTailCall().
  5. Return Call(functhisArgargList).

The length property of the call method is 1.git

當call方法在帶參數的對象的方法上調用時,thisArg和零個或者對個參數,會進行以下的步驟:es6

  1. 若是IsCallable(func)返回false,拋出TypeError異常。
  2. 定義argList爲一個空的列表。
  3. 若是方法按照從左到右傳入的參數個數不止一個,從第二個參數開始,依次將每一個參數從尾部添加到argList數組。
  4. 執行PrepareForTailCall()
  5. 返回Call(func,thisArg,argList)

有3個點看不懂:github

  • IsCallable(func)
  • PrepareForTailCall()
  • Call(func,thisArg,argList)

這些一樣在規範中有對應描述:web

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.chrome

重點在於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:

  1. Let leafContext be the running execution context.
  2. Suspend leafContext.
  3. Pop leafContext from the execution context stack. The execution context now on the top of the stack becomes the running execution context.
  4. AssertleafContext 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.

  1. ReturnIfAbrupt(argument).
  2. If Type(argument) is not Object, return false.
  3. If argument has a [[Call]] internal method, return true.
  4. Return false.

雖然看不懂,但仍是得硬着頭皮學習一波。
抽象操做PrepareForTailCall執行如下幾個步驟:

  1. 讓葉子上下文成爲運行中的執行上下文
  2. 暫停葉子上下文
  3. 頂葉子上下文來自執行上下文的堆。當前的在堆頂部的執行上下文成爲運行中的執行上下文
  4. 斷言:葉子上下文沒有其餘做用。它不再會做爲運行中執行上下文被激活。

在調用目標函數或者重用這些資源去支持目標函數以前,尾部位置調用必須釋放與當前執行函數上下文相關的瞬態內部資源。

  1. ReturnIfAbrupt(argument).
  2. 若是Type(argument)不是對象,返回false。
  3. 若是argument含有[[call]]內部方法,返回true。
  4. 返回 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:

  1. ReturnIfAbrupt(F).
  2. If argumentsList was not passed, let argumentsList be a new empty List.
  3. If IsCallable(F) is false, throw a TypeError exception.
  4. Return F.[[Call]](VargumentsList).

Call抽象操做是在調用函數對象的內部的[[Call]]方法。這個操做參數類型包括F,V以及可選的argumentList。F指的是調用函數,V指的是[[Call]]的this值,而後argumentsList是傳入到[[Call]]內部方法相應參數的值。若是argumentList不存在,那麼argumentList將被置爲一個空數組。這個方法按照下列幾步執行:

  1. ReturnIfAbrupt(F)
  2. 若是沒傳入argumentList,那麼argumentList將會被置爲一個空數組。
  3. 若是IsCallable(F)是false,返回TypeError異常。
  4. 返回 F.[[call]](V,argumentsList).

因此Function.prototype.call(this,...args)執行過程如今很明瞭:

  1. 判斷傳入的func是否有[[call]]屬性,有[[call]]才意味着函數能被調用,不然拋出TypeError異常。
  2. 定義argList爲一個空的列表。
  3. 傳參:若是方法按照從左到右傳入的參數個數不止一個,從第二個參數開始,依次將每一個參數從尾部添加到argList數組。
  4. 切換this上下文:執行PrepareForTailCall(),爲函數調用棧在尾部調用函數作準備,切換運行中執行上下文,實現this上下文的動態改變。
  5. 萬事具有,執行Call(func,thisArg,argList),調用函數便可。

回到咱們的例子:

fullscreenFunc.call(fullscreenDiv);
  1. func爲fullscreenDiv DOM 節點的方法:'requestFullscreen' || 'mozRequestFullScreen' || 'msRequestFullscreen'

|| 'webkitRequestFullScreen',因爲是fullscreen API,因此isCallable(func)返回true。

  1. 定義一個argList空數組用來傳參。
  2. 傳參:因爲fullscreenFunc.call(fullscreenDiv);只有一個參數,因此直接傳入argList空數組。
  3. 切換this上下文:中止當前的this葉子上下文,也就是window,切換到fullscreenDiv的執行上下文。
  4. 因爲當前瀏覽器爲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。
image.png

而咱們是想觸發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()得到數組中的最大項
<script>
function maxNumber(...args) {
  this.maxNumber = Math.max.apply(null, args);
}
export default {
  data() {
    return {
      applyTest: {
        numbers: [1, 2, 4, 5, 3],
      },
    }
  },
  created() {
    maxNumber.apply(this.applyTest, 
    this.applyTest.numbers);
    console.log(this.applyTest); // {"numbers": [1,2,4,5,3],"maxNumber": 5}
  },
}
</script>
// 回調函數中強制指定函數上下文
// 手動實現一個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]

2019.8.23更新

在上面的示例中,本質上是call,apply實現僞造對象繼承。
this.applyTest僞造繼承了MaxNumber類,從而新建出單獨包含maxNumber屬性的實例。
array[i]僞造繼承了callback類,從而新建出每個傳入參數以後的validate實例。

期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:

努力成爲優秀前端工程師!

加油,前端同窗們!

相關文章
相關標籤/搜索