今天在學習前端工程化的過程當中,遇到一個是實驗中的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
When the
call
method is called on an object func with argument, thisArg and zero or more args, the following steps are taken:
The length
property of the call
method is 1.git
當call方法在帶參數的對象的方法上調用時,thisArg和零個或者對個參數,會進行以下的步驟:es6
有3個點看不懂:github
這些一樣在規範中有對應描述:web
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)
The abstract operation PrepareForTailCall performs the following steps:
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.
雖然看不懂,但仍是得硬着頭皮學習一波。
抽象操做PrepareForTailCall執行如下幾個步驟:
在調用目標函數或者重用這些資源去支持目標函數以前,尾部位置調用必須釋放與當前執行函數上下文相關的瞬態內部資源。
看懂一個大概,是爲了在函數調用棧的尾部調用當前函數作準備,其中的運行中執行上下文,正是咱們所說的this動態改變的緣由,由於本質上this改變並不只僅是指向的對象發生變化,而是連帶着與其相關的上下文都發生了變化。
因此說,這一步是this動態改變的真正緣由。
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:
Call抽象操做是在調用函數對象的內部的[[Call]]方法。這個操做參數類型包括F,V以及可選的argumentList。F指的是調用函數,V指的是[[Call]]的this值,而後argumentsList是傳入到[[Call]]內部方法相應參數的值。若是argumentList不存在,那麼argumentList將被置爲一個空數組。這個方法按照下列幾步執行:
F.[[call]](V,argumentsList)
.因此Function.prototype.call(this,...args)執行過程如今很明瞭:
回到咱們的例子:
fullscreenFunc.call(fullscreenDiv);
|| 'webkitRequestFullScreen',因爲是fullscreen API,因此isCallable(func)返回true。
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()得到數組中的最大項 <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實例。
期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:
- SegmentFault專欄:趁你還年輕,作個優秀的前端工程師
- Github博客: 趁你還年輕233的我的博客
- 掘金主頁:趁你還年輕233
- SegmentFault技術圈: ES新規範語法糖
- 知乎專欄:趁你還年輕,作個優秀的前端工程師
- 前端開發交流羣:660634678
努力成爲優秀前端工程師!
加油,前端同窗們!