前言
今天閑着無聊隨便逛了逛MDN,忽而看到一個方法Function.prototype.bind()
,突然發現除了使用這個方法之外都沒有仔細琢磨過這個方法。於是乎,找到了kill time的事情-寫博客。
基礎知識簡介
隨便看看資料發現這玩意其實不簡單,理解起來需要不少基礎知識,在這里羅列一些,也算是一個總結和復習。
函數
下面這段話來自《JavaScript語言精粹》,名副其實地描述了函數的精髓。
調用一個函數會暫停當前函數的執行,傳遞控制權和參數給新函數。除了聲明時定義的形式參數,每個函數還接收兩個附加的參數:this和arguments。參數this在面向對象編程中非常重要,他的值取決於調用的模式。在JavaScript里面一共有四種調用模式:方法調用模式、函數調用模式、構造器調用模式和apply調用模式。這些模式在如何初始化關鍵參數this上面存在差異。
方法調用模式
當一個函數被保存為對象的一個屬性時,我們稱它為一個方法。當一個方法被調用的時候,this被綁定到該對象。
var info = {
name: 'yuanzm',
sayName: function() {
console.log(this.name);
}
}
info.sayName(); //yuanzm
函數調用模式
當一個函數並非為一個對象的屬性的時候,他就是被當做一個函數來調用的。此模式調用函數的時候,this被綁定到全局對象。這是語言設計上的一個錯誤。倘若語言設計正確,this應該是綁定到外部函數的this變量。
var name = "yuanzm"
var sayName = function() {
console.log(this.name);
}
sayName();// yuanzm
構造器調用模式
如果在一個函數前面帶上new來調用,那么背地里將會創建一個連接到該函數的prototype成員的新對象,同時this會被綁定到新對象上。(JavaScript原型相關知識這里不做贅述)
function Info(name) {
this.name = name;
}
Info.prototype.sayName = function() {
console.log(this.name);
}
var info = new Info('yuanzm');
info.sayName();//yuanzm
Apply調用模式
根據MDN的定義
The apply() method calls a function with a given this value and arguments provided as an array (or an array-like object).
var numbers = [5, 6, 2, 3, 7];
var max = Math.max.apply(null, numbers);
類數組
一個類數組被定義為:
- 具有:指向對象元素的數字索引下標以及 length 屬性告訴我們對象的元素個數
- 不具有:諸如 push 、 forEach 以及 indexOf 等數組對象具有的方法
符合上述定義的類數組是長下面這樣子的:
var my_object = {
'0': 'zero',
'1': 'one',
'2': 'two',
'3': 'three',
'4': 'four',
length: 5
};
前面提到的arguments
也是類數組。很多時候,處理類數組最省事的方法就是將它轉化成數組。那么就涉及到一個非常有意思的話題:將類數組轉換成數組。
將類數組轉換成數組非常簡單,調用Array自帶的方法即可:
Array.prototype.slice.call(arguments);
其中call換成apply也是一樣的。
簡單解釋一下,slice方法常規的調用方式為array.slice(start, end)
,會對array中的一段做淺復制
,首先復制array[start], 一直復制到array[end]。前面提到過apply(或call)會切換一個函數調用的上下文,也就是Array.prototype.slice.call(arguments);
手動綁定了需要操作的array為arguments,由於arguments和數組一樣具有下標,所以這個方法也是可行的,因而產生了一個新的數組,這個數組具有普通數組的所有方法,同時具有arguments每一個下標對應的值。
bind
簡介
前面說了這么多,主角終於登場了!不過為了凸顯它的作用,還是需要拋出一段達不到我們需求的代碼。
var name = 'yuan'
var info = {
name: "yuanzm",
sayName: function() {
console.log(this.name);
}
}
var sayName = info.sayName;
// 我們本身是希望得到yuanzm的,但是確得到了yuan這個結果。為什么會得到這個結果,前面的長篇大論起作用了。
// 如果是采用info.sayName()這種調用方式,符合函數的方法調用模式,函數內部的this是info對象;
// 如果令一個變量sayName為info.sayName,這個時候再調用函數,就是普通的函數調用模式了,結果自然為yuan。
sayName(); // yuan
那么我們使用bind就是希望最后得到的結果為yuanzm
。
現在我們可以好好介紹bind了。根據MDN的定義:
The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
翻譯過來就是,bind()方法會創建一個新函數,稱為綁定函數.當調用這個綁定函數時,綁定函數會以創建它時傳入 bind()方法的第一個參數作為 this,傳入 bind() 方法的第二個以及以后的參數加上綁定函數運行時本身的參數按照順序作為原函數的參數來調用原函數。
他的語法是:
fun.bind(thisArg[, arg1[, arg2[, ...]]]);
解決問題
有了bind,上述問題我們就能夠得到想要的結果了:
var name = 'yuan'
var info = {
name: "yuanzm",
sayName: function() {
console.log(this.name);
}
}
var sayName = info.sayName.bind(info);
sayName(); // yuanzm
bind的用法在MDN上面描述得很詳細,本文的目的也不是為了照搬MDN,所以這里有關bind使用的部分就到這兒,接下來我們去看看Javascript庫中bind的實現
Prototype中的bind
很久之前Prototype的bind寫法是下面這樣的(注釋為本人所加):
Function.prototype.bind = function(){
// bind作為Function的prototype屬性,每一個函數都具有bind方法,其中的this指向調用該方法的函數(也即一個函數對象實例)
var fn = this;
// 前面說過,這個方法是為了將類數組轉換成數組
var args = Array.prototype.slice.call(arguments);
// 數組的shift方法會移除數組的第一個元素,在bind方法的參數里面,第一個參數就是需要綁定this的對象
var object = args.shift();
return function(){
return fn.apply(object,
// 現在的args是移除了最初參數中第一個參數后的數組,也就是新函數的預設的初始參數。
// array.concat(items...)方法產生一個新數組,它包含一份array的淺復制,並把一個或者多個參數item附加在其后。
// 如果參數item是一個數組,那么它的元素會被分別添加。
// 於是這一行代碼產生了一個參數數組,這個數組由預設的初始參數(如果存在)和新傳遞的參數組成。
args.concat(Array.prototype.slice.call(arguments)));
};
};
在最新的版本1.7.2(2015.06.23查閱官網)中https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/function.js#L115,bind是下面這樣子的:
function bind(context) {
if (arguments.length < 2 && Object.isUndefined(arguments[0]))
return this;
if (!Object.isFunction(this))
throw new TypeError("The object is not callable.");
var nop = function() {};
var __method = this, args = slice.call(arguments, 1);
var bound = function() {
var a = merge(args, arguments);
// Ignore the supplied context when the bound function is called with
// the "new" keyword.
var c = this instanceof bound ? this : context;
return __method.apply(c, a);
};
nop.prototype = this.prototype;
bound.prototype = new nop();
return bound;
}
可見,除了加了一些異常情況判斷,核心代碼和之前並無大差別。
結語
總得來說apply、call和bind都是為了手動綁定this對象,目的上沒有什么區別。但是bind和另外兩者的明顯的區別是,bind會產生一個新的函數,這個函數還可以有預設的參數,這在很多時候會比apply和call更加好用。合理利用apply、call和bind會使得javaScript代碼更加優雅。
參考資料
Function.prototype.apply()
JavaScript 的怪癖 8:“類數組對象”
how does Array.prototype.slice.call() work?
JavaScript’s Apply, Call, and Bind Methods are Essential for JavaScript Professionals