對於前面2個 apply,call大家應該非常熟悉了,都可以改變this指向,都可以傳參數,但是bind的話很多人可能覺得和它們沒有什麼區別,估計用bind也用的少,下面我來一一分析下各自的實現原理:
1.call方法
1.第一個參數是this指向的對象。
2.使用的單個參數進行傳遞。
3.用於確定了函數的形參有多少個的時候用。
舉個例子:
var name = '李四'
var b = {name:'張三'};
function a(n){
console.log(this.name+n+'歲了')
}
a.call(b,'18');//'張三18歲了'
a.call(null,'18');//'李四18歲了'
我們可以這樣理解:把a方法放到b裏面,然後我在b的環境下執行a(如果b沒有值,那就相當再window環境下執行a,this是指向window的),相當b.a(18)或者a(18),然後我再b裏面刪除a方法。知道原理了,那麼我們按照上面的例子來自己封裝下call方法:
Function.prototype.call = function (obj) {
// 當call的第一個參數沒有或者是null的時候,this的指向是window
var obj= obj || window;
// 把a方法放進裏面
obj.fn = this;
// 用於存儲call後面的參數
var args = [];
var len = arguments.length;
// 這裏是爲了將一個函數的參數傳入到另外一個函數執行
for (var i = 1; i < len; i++) {
args.push('arguments[' + i + ']');
};
// 在eval的環境下 args數組會變成一個一個參數字符串(默認是會調用Array.toString())
var result = eval('obj.fn(' + args + ')');
// 刪除b裏面的a方法
delete obj.fn;
// 因爲函數可能有返回值,所以把結果也返回出去給他們
return result;
};
2.apply方法(和call沒啥區別)
1.第一個參數是this指向的對象。
2.使用的參數是數組進行傳遞。
3.用於確定了函數的形參的個數不確定的情況下使用。
原理我就不累贅了,和call沒有什麼區別,只有傳遞的形參不用,apply傳遞的一定要是是數組,那麼我們就可以在傳參的進行判斷下。我們來模擬下代碼的實現:
Function.prototype.apply = function (obj, arr) {
// 當apply的第一個參數是null的時候,this的默認指向是window
var obj = obj || window;
// 把該函數掛載到對象上
obj.fn = this;
//判斷有沒有傳值
if (!arr) {
result = obj.fn();
} else {
//判斷傳入的是不是數組,不是的話拋出異常
if (!Array.isArray(arr)) {
throw new Error('上傳的必須是數組');
};
var args = [];
// 用於存儲apply後面的參數
for (var i = 0; i < arr.length; i++) {
args.push('arr[' + i + ']');
};
// 這裏的args默認是會調用Array.toString()方法的
var result = eval('obj.fn(' + args + ')');
}
// 刪除函數
delete obj.fn;
// 因爲函數可能有放回值,所以把結果也返回出去給他們
return result;
}
2.bind方法
這裏重要講下bind,bind和call和apply還是有點區別的。
1.bind會創建一個函數(稱爲綁定函數),創建一個新函數而不執行,這是bind和call與apply方法的一個重要差別,call和apply這兩個方法都會立即執行函數,返回的是函數執行後的結果。而bind函數只創建一個新函數而不執行。
2.函數的柯里化(使用一個閉包返回一個函數),柯里化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。
下面我們來慢慢模擬一下bind的實現(首選返回一個函數):
Function.prototype.bind = function(obj){
var self =this;
//第一個參數爲它運行時的this,應該取第二個之後的參數
var args =Array.prototype.slice.call(arguments,1);
//返回一個新函數閉包
var newFn = function(){
self.apply(obj,args);
};
return newFn;
};
//例子:
var name = '李四'
var b = {
name: '張三',
};
function a(age) {
console.log(this.name + age + '歲了')
}
var p =a.bind(b,'18');
p();//"張三18歲了"
和預想的一樣,bind得底部實現還是apply方法,很穩!!,有沒有發現啥問題,我只在返回的新函數裏面傳了第2個參數,調用的時候都沒傳參數,這是個問題!然後補充一下:
Function.prototype.bind = function(obj){
var self =this;
//第一個參數爲它運行時的this,應該取第二個之後的參數
var args =Array.prototype.slice.call(arguments,1);
//返回一個新函數閉包
var newFn = function(){
self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
};
return newFn;
};
//例子改下參數
var name = '李四'
var b = {
name: '張三',
};
function a(age,sex) {
console.log(this.name + age + '歲了,性別'+sex);
}
var p =a.bind(b,'18');
p('男');//張三18歲了,性別男
和預想的一樣!可以接受2個參數,實現了函數的柯里化。是不是還覺得哪裏有問題?對的,函數才能使用bind!這裏需要判斷一下,再改下代碼:
Function.prototype.bind = function(obj){
var self =this;
if(typeof this !=='function'){
throw new Error('只有函數纔可以調用bind');
};
//第一個參數爲它運行時的this,應該取第二個之後的參數
var args =Array.prototype.slice.call(arguments,1);
//返回一個新函數閉包
var newFn = function(){
self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
};
return newFn;
};
好了,到這裏的話,感覺好像代碼實現的差不多,但是這裏有個重點,當我們把創建出來的新函數當做構造函數的時候,官方文檔有這麼一句話:“說明綁定過後的函數被new實例化之後,需要繼承原函數的原型鏈方法,且綁定過程中提供的this被忽略(繼承原函數的this對象),但是參數還是會使用。” 看上去不是很明白,其實主要是說,當new完之後 新創建出來的實例要繼承原函數的原型,並且this指向新創建出來的實例對象(綁定的this將失效)。首先實現下原理繼承:
Function.prototype.bind = function(obj){
var self =this;
if(typeof this !=='function'){
throw new Error('只有函數纔可以調用bind');
};
//第一個參數爲它運行時的this,應該取第二個之後的參數
var args =Array.prototype.slice.call(arguments,1);
//返回一個新函數閉包
var newFn = function(){
self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
};
//繼承原函數的原型
newFn.prototype = this.prototype;
return newFn;
};
如果這樣寫的話是有問題的,如果我改變了新創建出來的函數的原型同樣也修改了原函數的原型,所以這裏需要寫一個過渡函數(如果不懂繼承的同學,可以點擊這裏)。
Function.prototype.bind = function(obj){
var self =this;
if(typeof this !=='function'){
throw new Error('只有函數纔可以調用bind');
};
//第一個參數爲它運行時的this,應該取第二個之後的參數
var args =Array.prototype.slice.call(arguments,1);
//返回一個新函數閉包
var newFn = function(){
self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
};
//過渡函數
var f = function(){};
f.prototype = this.prototype;
newFn.prototype = new f();
return newFn;
};
接下來需要判斷this的值,判斷到底是不是new出來的實例,通過instanceof 判斷。簡單解釋下 instanceof ,假如a instanceof b,意思是a是不是b的實例,或者這樣說:b的prototype是否在a的原型鏈上。再改下代碼:
Function.prototype.bind = function(obj){
var self =this;
if(typeof this !=='function'){
throw new Error('只有函數纔可以調用bind');
};
//第一個參數爲它運行時的this,應該取第二個之後的參數
var args =Array.prototype.slice.call(arguments,1);
//返回一個新函數閉包
var newFn = function(){
self.apply(this instanceof f ? this : obj ,args.concat(Array.prototype.slice.call(arguments)));
};
//過渡函數
var f = function(){};
f.prototype = this.prototype;
newFn.prototype = new f();
return newFn;
};
到這裏代碼就修改的差不多了,最後拿這段代碼“this instanceof f ? this : obj ”分析下,這裏的意思是如果是new出來的,this指向它實例出來的對象,如果不是,那麼this是指向綁定的obj對象。如果obj沒有值,不傳值或者是null,那麼這個this是指向window的,有些同學代碼在這裏做了下個判斷 “this instanceof f ? this : obj || this”,這樣寫的話,this不一定都指向window的,比如看下面這個例子:
var name = '張三';
var foo = {
name:'李四',
fn: fn.bind(null) //如果在這裏執行,this是指向widonw的
};
function fn() {
console.log(this.name);
}
//foo這裏調用fn,所以在這裏把this的指向改變了,this===foo;
foo.fn() //'張三'
所以說這個判斷是不對的。好了,到這裏就差不多已經說完了(如果有不對之處,歡迎指正,不勝感激!!!),本來還想說下bind的幾個難點,怕寫的太多都看不下去了,還是下一篇再說吧,歡樂的時光總是過得特別快,又到時候和大家講拜拜!!