寫這篇部落格之前,我想先說下今天(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是什麼?(黑人問號臉)
今天的主題(??)是call、apply
以及bind
,這裡這個以及我覺得用的很好,後面我會解釋為什麼不把bind
和call、apply
歸為一類。
this
物件是在執行時基於函式的執行環境繫結的(拋開箭頭函式)
當函式被作為某個物件的方法呼叫時,this
等於那個物件
this
等於最後呼叫函式的物件
讓我們來for example ⬇️
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,那我換種寫法可能大家就明白了 ⬇️
var name = 'Jack Sparrow';
function sayWhoAmI() {
console.log(this.name)
}
- sayWhoAmI(); // Jack Sparrow
+ window.sayWhoAmI(); // Jack Sparrow
複製程式碼
這樣是不是清晰明瞭了
∵ 在全域性宣告的變數或者函式,都是在window
或者globle
這個物件裡的
∴ 在window
全域性下宣告的sayWhoAmI
可以輸出同是window
全域性下宣告的name
小進階
簡單的我們已經明白了,現在我們來看看加入return
的方法,我覺得算是有點難度的了,大佬請飄過 ⬇️
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'
⬇️
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
指向,?人生處處是艱難啊
這時候我們就需要用到標題中提到的call
和apply
Apply nothing and just call me
call()
方法與apply()
方法的作用相同,它們的區別僅在於接收引數的方式不同。————————《JavaScript高階程式設計》
書裡面說的很清楚,它們兩個的作用是一樣的,只是接收引數的方式不同,那到底有什麼區別呢,聽我我細細道來
瘋狂打call
call()
方法可以指定一個this
的值(第一個引數),並且分別傳入引數(第一個引數後面的就是需要傳入函式的引數,需要一個一個傳)
call()
方法到底有什麼用呢,自然是解決我們剛才提出來的改變this
指向,怎麼用呢???⬇️
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()
就簡單多了,我們可以把上面的例子改一下⬇️
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
這個類陣列物件⬇️
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祕密
文章開頭我說過這樣一句話⬇️
call、apply
以及bind
,這裡這個以及我覺得用的很好
現在我們就來聊聊這個‘以及’的內涵
我為什麼說‘以及’呢,因為bind
和call、apply
這兩個方法的使用有一丟丟的不一樣。上面我們一個函式呼叫.call()
或者.apply()
方法,方法會立即執行,如果函式有返回值會獲得返回值,但是bind
不一樣
bind()方法不會立即執行目標函式,而是返回一個原函式的拷貝,並且擁有指定this
值和初始函式(為什麼是指定的,當然是我們自己傳進去的啦)
什麼叫原函式的拷貝呢,那我們先來看一下⬇️
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
是一樣的,第一個引數是需要繫結的物件,後面一次傳入函式需要的引數,如下⬇️
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。
那我們需要傳參的時候怎麼辦 ⬇️
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 web docs的Function.prototype.bind中的偏函式)
印結完了,該出招了
影子模仿術
預設大家到這裡已經知道怎麼使用bind
了,那我們接下來需要挑戰的就是,自己手寫一個bind
方法,這個可以幫助我們更清楚的理解bind
方法是怎麼運作的,並且面試的時候也可能會被問到哦~
下面我們來看從MDN web docs 的Function.prototype.bind中複製過來的實現,新增了我自己的理解和註釋,希望大家能看懂⬇️
// 判斷當前環境的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 起不到本質上的作用,但是讓我們知道有一群可愛的人跟我們一樣在為生計奔波勞累著,讓我們知道我們的圈子不小,只是沒到團結的時候,敢折騰就不賴,人一定要夢想,趁著年輕,萬一實現了呢。
帶病寫部落格。。。
年輕嘛,就是幹!ps:部落格可以技術分享,也當記錄生活了,以後看見的話,沒準會說“當時是不是傻”,但是現在感覺perfect
我是前端戰五渣,一個前端界的小學生。