前端戰五渣學JavaScript——call、apply以及bind
寫這篇部落格之前,我想先說下今天(2019年3月28日)一直關注的一件事吧(出於湊熱鬧的心情——尷尬)。在昨天,全球最大交友網站Github上悄然出現一個名為996.ICU 的文件專案,整個專案沒有程式碼,只是列了一些《勞動法》的條款和最近表明實行996工作制的公司。本來以為是一個小打小鬧 的抱怨,結果今天中午再看的時候star數已經有30k 以上,並且issues達到5000+ 。下午更是勢如破竹 ,在Github的star排行榜上,一路過五關斬六將,截止目前,這個出現不到24小時 的專案,坐擁63k 的star,並且排行榜第21名 。為什麼一個這麼簡單的專案會異軍突起,伴著屠榜 的架勢,一發不可收拾。也許這只是觸動了被強行996工作的朋友們,以及無休止的加班沒有回報的程式設計師們心中那最敏感的神經,可能迫於生計問題,現實生活中只能忍氣吞聲,但當出現一個虛擬的世界可以讓你盡情發洩的時候,心中的苦水傾瀉而出,造就了這個怪異 的專案。我們不是不能接受996,是要實行996工作制公司得付的出相應的報酬,這讓員工感覺自己的付出是有回報的,既沒有相應的酬勞,又沒有自己的時間,怨氣只會越攢越多。我們現在能做什麼:一、儘量不去996的公司,讓996的公司無人可招;二、提高自己的技術水平,讓自己擁有議價的主導權,非要實行996,能談出你可以接受的薪酬 。以上是我個人看法,不喜勿噴。(還是那句。。。錢給到位,住公司都行)
What is this?
What is this?這是什麼?this是什麼?(黑人問號臉)
今天的主題(:heart_eyes:?)是call、apply
以及bind
,這裡這個以及
我覺得用的很好,後面我會解釋為什麼不把bind
和call、apply
歸為一類。
this
物件是在執行時基於函式的執行環境繫結的(拋開箭頭函式)
當函式被作為某個物件的方法呼叫時,this
等於那個物件
this
等於最後呼叫函式的物件
讓我們來for example :arrow_down:
var name = 'Jack Sparrow'; function sayWhoAmI() { console.log(this.name) } sayWhoAmI(); // Jack Sparrow var onePiece = { name: 'Monkey·D·Luffy', sayWhoAmI: function () { console.log(this.name) } }; onePiece.sayWhoAmI(); // Monkey·D·Luffy 複製程式碼
上面的程式碼我們可以看出,不管定義在哪的sayWhoAmI()
方法,函式體是一樣的,onePiece.sayWhoAmI()
根據上面說的可以理解:
∵(因為,下同)呼叫方法的最後那個物件就是onePiece
∴(所以,下同)this
是onePiece
,this.name
就是onePiece.name
但是為什麼全域性定義的sayWhoAmI
方法輸出的是Jack Sparrow,那我換種寫法可能大家就明白了 :arrow_down:
var name = 'Jack Sparrow'; function sayWhoAmI() { console.log(this.name) } - sayWhoAmI(); // Jack Sparrow + window.sayWhoAmI(); // Jack Sparrow 複製程式碼
這樣是不是清晰明瞭了
∵ 在全域性宣告的變數或者函式,都是在window
或者globle
這個物件裡的
∴ 在window
全域性下宣告的sayWhoAmI
可以輸出同是window
全域性下宣告的name
小進階
簡單的我們已經明白了,現在我們來看看加入return
的方法,我覺得算是有點難度的了,大佬請飄過 :arrow_down:
var area = 'East Ocean'; var onePiece = { area: 'New World', tellMeWhereAreYou: function () { return function () { console.log(this.area); } } }; onePiece.tellMeWhereAreYou()(); // East Ocean // 如果看不懂這裡為什麼執行兩次,或者不明白為什麼輸出的全域性變數 // 那我引入一箇中間變數,讓過程多一步就能看懂了 var grandLine = onePiece.tellMeWhereAreYou(); // 這時候的 grandLine = function() { console.log(this.area); },等於onePiece.tellMeWhereAreYou();返回的函式 // 因為grandLine是一個全域性變數,所以this.area返回的是East Ocean grandLine(); // East Ocean 複製程式碼
上面我覺得用了言簡意賅的方法解釋了一下這個問題,因為這個涉及到閉包的知識,以及函式的活動物件,不明白的可以看我的另一篇部落格《前端戰五渣學JavaScript——閉包》 ,如果還不懂,還想更深入的瞭解可以自行翻閱《JavaScript高階程式設計》有關閉包的7.2章節,弄明白7.2章節中的兩張圖。
那麼現在問題來了,我怎麼才能讓這個函式輸出我物件內部的area: 'New World'
:arrow_down:
var area = 'East Ocean'; var onePiece = { area: 'New World', tellMeWhereAreYou: function () { var that = this; // 我們通過宣告一個變數來儲存this所指向的物件,然後再閉包中,就是返回的函式中使用 // 一個典型的閉包結構就完成了 return function () { console.log(that.area); } } }; onePiece.tellMeWhereAreYou()(); // New World 複製程式碼
可能大家之前工作中會用到中間變數來儲存this
的這種方法,而且我感覺也不難,那我就跳過了。
我們現在應該大體搞明白了this
指向的問題了。但是我們就是變態,我們有病,我們終於搞明白了this
的指向問題,那我們現在又想改變this
指向,:stuck_out_tongue_winking_eye:人生處處是艱難啊
這時候我們就需要用到標題中提到的call
和apply
Apply nothing and just call me
call()
方法與apply()
方法的作用相同
,它們的區別僅在於接收引數的方式不同。————————《JavaScript高階程式設計》
書裡面說的很清楚,它們兩個的作用是一樣的,只是接收引數的方式不同,那到底有什麼區別呢,聽我我細細道來
瘋狂打call
call()
方法可以指定一個this
的值(第一個引數),並且分別傳入引數(第一個引數後面的就是需要傳入函式的引數,需要一個一個傳)
call()
方法到底有什麼用呢,自然是解決我們剛才提出來的改變this
指向,怎麼用呢???:arrow_down:
var first = '大黑刀·夜', second = '二代鬼徹', third = '初代鬼徹', fourth = '時雨'; var zoro = { first: '和道一文字', second: '三代鬼徹', third: '雪走', fourth: '秋水' }; function sayYourWeapon(num, num2) { console.log(`這是我${num}得到的刀"${this[num]}"`) console.log(`這是我${num2}得到的刀"${this[num2]}"`) } sayYourWeapon('first', 'third'); // 這是我first得到的刀"大黑刀·夜";這是我third得到的刀"初代鬼徹" sayYourWeapon.call(zoro, 'first', 'fourth'); // 這是我first得到的刀"和道一文字";這是我fourth得到的刀"秋水" 複製程式碼
上面這段程式碼很明顯的改變了this
的指向,如果我直接呼叫sayYourWeapon()
必然輸出的是全域性全域性變數first
和third
的值,而我後面通過sayYourWeapon.call(zoro, 'first', 'fourth')
中的call()
方法
∵ 改變了函式中的this
值,就是傳入的zoro
,把this
值從全域性物件改成了zoro
物件
∴ 後面輸出的也都是物件zoro
中的'first', 'fourth'
的值
apply所有配置
apply()
方法可以指定一個this
的值(第一個引數),並且傳入引數陣列(引數需要在一個數組或者類陣列中)
我們應該已經是知道了call()
方法怎麼用了,那我們熟悉apply()
就簡單多了,我們可以把上面的例子改一下:arrow_down:
var first = '大黑刀·夜', second = '二代鬼徹', third = '初代鬼徹', fourth = '時雨'; var zoro = { first: '和道一文字', second: '三代鬼徹', third: '雪走', fourth: '秋水' }; function sayYourWeapon(num, num2) { console.log(`這是我${num}得到的刀"${this[num]}"`) console.log(`這是我${num2}得到的刀"${this[num2]}"`) } sayYourWeapon('first', 'third'); // 這是我first得到的刀"大黑刀·夜";這是我third得到的刀"初代鬼徹" - sayYourWeapon.call(zoro, 'first', 'fourth'); // 這是我first得到的刀"和道一文字";這是我fourth得到的刀"秋水" + sayYourWeapon.apply(zoro, ['first', 'fourth']); // 這是我first得到的刀"和道一文字";這是我fourth得到的刀"秋水" 複製程式碼
可以看到,我全篇就只是把call
改成了apply
,並且把之前'first', 'fourth'
這麼傳進去的引數改成了['first', 'fourth']
一個數組。如果我們是在一個函式當中使用,那我們還可以直接使用arguments
這個類陣列物件:arrow_down:
var first = '大黑刀·夜', second = '二代鬼徹', third = '初代鬼徹', fourth = '時雨'; var zoro = { first: '和道一文字', second: '三代鬼徹', third: '雪走', fourth: '秋水' }; function sayYourWeapon(num, num2) { console.log(`這是我${num}得到的刀"${this[num]}"`) console.log(`這是我${num2}得到的刀"${this[num2]}"`) } function mySayYourWeapon(num, num2) { sayYourWeapon.apply(zoro, arguments) // 我們自己宣告一個函式,並且在裡面呼叫apply,這是我們只需要傳入arguments這個引數,而不需要想call那樣一個一個傳進去了 } sayYourWeapon('first', 'fourth'); // 這是我first得到的刀"大黑刀·夜";這是我fourth得到的刀"時雨" mySayYourWeapon('first', 'fourth'); // 這是我first得到的刀"和道一文字";這是我fourth得到的刀"秋水" 複製程式碼
羈bind祕密
文章開頭我說過這樣一句話:arrow_down:
call、apply
以及bind
,這裡這個以及
我覺得用的很好
現在我們就來聊聊這個‘以及’的內涵
我為什麼說‘以及’呢,因為bind
和call、apply
這兩個方法的使用有一丟丟的不一樣。上面我們一個函式呼叫.call()
或者.apply()
方法,方法會立即執行,如果函式有返回值會獲得返回值,但是bind
不一樣
bind()方法不會立即執行目標函式,而是返回一個原函式的拷貝,並且擁有指定this
值和初始函式(為什麼是指定的,當然是我們自己傳進去的啦)
什麼叫原函式的拷貝呢,那我們先來看一下:arrow_down:
function a() {} console.log(typeof a.bind() === 'function'); // 返回是true,先證明a.bind()是一個函式 console.log(a.bind()); // 輸出function a() {},跟原函式一樣 console.log(a.bind() == a); // false console.log(a.bind() === a); // false 不管是 === 還是 == 都是false,證明是拷貝出來一份而不是原先的那個函式 複製程式碼
上面解釋了‘原函式的拷貝’這個問題,那接下來我們看看bind()
怎麼使用
結印準備
bind()
方法在傳參上跟call
是一樣的,第一個引數是需要繫結的物件,後面一次傳入函式需要的引數,如下:arrow_down:
var name = 'Jack Sparrow'; var onePiece = { name: 'Monkey·D·Luffy' }; function sayWhoAmI() { console.log(this.name) } var mySayWhoAmI = sayWhoAmI.bind(onePiece) sayWhoAmI(); // Jack Sparrow mySayWhoAmI(); // Monkey·D·Luffy 複製程式碼
一個簡單的實現,本來輸出的是全域性變數'Jack Sparrow',後來經過bind
以後繫結上了物件onePiece
,所以輸出的就是物件onePiece
中的node
Monkey·D·Luffy。
那我們需要傳參 的時候怎麼辦 :arrow_down:
var first = '大黑刀·夜', second = '二代鬼徹', third = '初代鬼徹', fourth = '時雨'; var zoro = { first: '和道一文字', second: '三代鬼徹', third: '雪走', fourth: '秋水' }; function sayYourWeapon(num, num2) { console.log(`這是我${num}得到的刀"${this[num]}"`) console.log(`這是我${num2}得到的刀"${this[num2]}"`) } // 既然我們知道bind是返回一個函式,那我們宣告一個變數來接這個函式會看的直觀一些 var mySayYourWeapon = sayYourWeapon.bind(zoro, 'first', 'fourth'); // 傳入初始引數 var hisSayYourWeapon = sayYourWeapon.bind(zoro); // 只傳入目標物件 sayYourWeapon('first', 'third'); mySayYourWeapon(); // 因為我們當時bind繫結函式的時候已經傳入了目標物件zoro和指定的引數,所以這裡就不需要傳引數了 hisSayYourWeapon( 'first', 'fourth'); // 當然我們開始bind繫結函式的時候不傳入,在呼叫的時候再傳入引數也是可以的 複製程式碼
上面的程式碼我們可以發現mySayYourWeapon
和hisSayYourWeapon
在bind
的時候一個傳入了初始的引數,一個沒有傳入,但是後續呼叫的時候可以再傳
既然是初始化引數,那我們就可以預設引數一個,然後再傳一個——————偏函式(不知道自己理解的對不對,但是肯定是有這麼個功能,不懂的可以移步MDN webdocs的Function.prototype.bind中的偏函式 )
印結完了,該出招了
影子模仿術
預設大家到這裡已經知道怎麼使用bind
了,那我們接下來需要挑戰的就是,自己手寫一個bind
方法,這個可以幫助我們更清楚的理解bind
方法是怎麼運作的,並且面試的時候也可能會被問到哦~
下面我們來看從MDN web docs 的Function.prototype.bind 中複製過來的實現,添加了我自己的理解和註釋,希望大家能看懂:arrow_down:
// 判斷當前環境的Function物件的原型上有沒有bind這個方法,如果沒有,那我們就自己新增一個 if (!Function.prototype.bind) { /** * 新增bind方法 * @param oThis 目標物件 * @returns {function(): *} 返回的拷貝函式 */ Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { // closest thing possible to the ECMAScript 5 // 最接近ECMAScript 5的實現(貌似是這個意思) // internal IsCallable function // 內部IsCallable函式(什麼鬼) // 如果當前this物件不是function,就丟擲錯誤,因為只有function才需要實現bind這個方法。。。畢竟是返回函式 throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); } // 宣告變數aArgs儲存arguments中除了第一個引數的其他引數的陣列,因為第一個引數不是函式需要的引數,而是需要繫結的目標物件 // 這塊就用到了call的方法,因為arguments是類陣列物件,沒有slice這個方法,所以只能從Array那call過來一個使用 var aArgs = Array.prototype.slice.call(arguments, 1); // 儲存原先的this物件,是在呼叫bind的時候沒有傳入目標物件,那就使用原先的this物件 var fToBind = this; // 宣告空函式,在下面的原型中可以使用 var fNOP = function() {}; // 需要放回的拷貝函式的本體,從最後的return也知道,最後是返回的fBound這個方法 var fBound= function() { // this instanceof fBound === true時,說明返回的fBound被當做new的建構函式呼叫 // 下面就涉及到剛才說的是bind時初始化引數,還是bind以後呼叫的時候再傳入引數 return fToBind.apply( // 判斷原始this物件是不是fBound的例項,或者說this的原型鏈上有沒有fBound this instanceof fBound // 如果有,就使用原始的this ? this // 如果沒有,就使用現在的傳入的this物件 : oThis, // 獲取呼叫時(fBound)的傳參.bind 返回的函式入參往往是這麼傳遞的 // 這一步就是為了保障在bind時候沒有傳入引數的時候,呼叫時候傳入的引數能使用上 aArgs.concat(Array.prototype.slice.call(arguments))); }; // 維護原型關係 // 判斷原始this物件上有沒有prototype if (this.prototype) { // Function.prototype doesn't have a prototype property // 如果原始this物件上有prototype 就把fNOP的prototype改成this.prototype,fNOP就繼承自原始this了 fNOP.prototype = this.prototype; } // 下行的程式碼使fBound.prototype是fNOP的例項,因此 // 返回的fBound若作為new的建構函式,new生成的新物件作為this傳入fBound,新物件的__proto__就是fNOP的例項 // 既然fNOP是繼承自原始this物件的,那這裡的這一步就是讓拷貝函式也擁有原始this物件的prototype,繼承自同一個地方,師出同門 fBound.prototype = new fNOP(); // 最後返回被拷貝出來的函式 return fBound; }; } 複製程式碼
上面的程式碼中有我新增的註釋,方便大家能更好的理解,理解了上面的程式碼以後,bind
方法算是瞭解的差不多了,其他實現原理上摸清楚了
可能上面的程式碼註釋有點多,看著很費勁,下面貼出沒有註釋的程式碼,方便大家複製貼上除錯
if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); } var aArgs = Array.prototype.slice.call(arguments, 1); var fToBind = this; var fNOP = function() {}; var fBound= function() { return fToBind.apply(this instanceof fBound ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; if (this.prototype) { fNOP.prototype = this.prototype; } fBound.prototype = new fNOP(); return fBound; }; } 複製程式碼
這麼看來程式碼還不算很多就實現了bind
方法
人的夢想,是不會完結的,沒錯吧?
可能996.ICU 起不到本質上的作用,但是讓我們知道有一群可愛的人跟我們一樣在為生計奔波勞累著,讓我們知道我們的圈子不小,只是沒到團結的時候,敢折騰就不賴,人一定要夢想,趁著年輕,萬一實現了呢。
年輕嘛,就是幹!