手動實現call() , apply() , bind()
這篇文章簡單的介紹了實現call() , apply() , bind()的思路
實現call(obj,arg,arg....)
將目標函式的this指向傳入的第一個物件,引數為不定長,且立即執行
實現思路
- 改變this指向:可以將目標函式作為這個物件的屬性
- 利用arguments類陣列物件實現引數不定長
- 不能增加物件的屬性,所以在結尾需要delete
那麼怎麼將不定長的引數傳遞給函式呢?有三種辦法:eval,apply,ES6的解構語法
eval('obj.fn('+args+'));
obj.fn.apply(obj,args);
obj.fn(...args);
Function.prototype.mycall = function(obj){ var args = Array.prototype.slice.apply(arguments,[1]); obj.fn = this; obj.fn(...args);//es6的解構語法,也可以使用obj.fn.apply(obj,args); delete obj.fn; }
使用eval時,eval會將一個字串解析為變數,所以如果傳入的引數為字串,會報 xxx is not defined,解決辦法如下:
Function.prototype.mycall = function(obj){ obj = obj||window; var args = []; for(var i = 1 ; i < arguments.length; i++) { args.push('arguments[' + i + ']'); } obj.fn = this; eval('obj.fn('+args+')); delete obj.fn; }
實現apply()
與call()只有一個區別,apply第二個引數為陣列
Function.prototype.myapply = function(obj,arr){ obj.fn = this; if(!arr){ obj.fn(); }else{ var args = []; for(var i = 0; i < arr.length; i++) { args.push('arr[' + i + ']'); } eval('obj.fn('+args+')'); } delete obj.fn; }
實現bind()
返回一個與被調函式具有相同函式體的新函式,且這個新函式也能使用new操作符。
實現思路
- 返回一個新函式,可以使用閉包
- dind()傳入的引數長度不定,使用函式內建的arguments物件陣列,可利用Array.prototype.slice.call(arguments,1 )將其轉化為陣列
- 返回的新函式中,使用apply改變被呼叫函式的this指向,將arguments轉化成的陣列作為apply的第二個引數
- 因為返回的新函式也可以使用new操作符,所以在新函式內部需要判斷是否使用了new操作符(為什麼需要判斷,後面會講解到),如果使用則將apply的第一個引數設定為新建立的物件,如果沒有則設定為在呼叫bind()時所傳入的物件(不傳的話預設為window)。
需要注意的是怎麼去判斷是否使用了new操作符呢?在解決這個問題之前,我們先看使用new操作符時具體幹了些什麼,下面是new操作符的簡單實現過程:
//簡潔版的new操作符實現過程
function newFunc(constructor){
//第一步:建立一個空物件obj
var obj = {};
//第二步:將建構函式 constructor的原型物件賦給obj的原型
obj.__proto__ = constructor.prototype;
//第三步:將建構函式 constructor中的this指向obj,並立即執行建構函式內部的操作
constructor.apply(obj);
//第四步:返回這個物件
return obj;
}
new操作符的一個過程相當於繼承,新建立的建構函式的例項可以訪問建構函式的原型鏈
在new操作符實現過程的第三步中,會將建構函式 constructor中的this指向obj,並立即執行建構函式內部的操作,那麼,當在執行函式內部的操作時,如果不進行判斷是否使用了new,就會導致 " 將建構函式 constructor中的this指向obj " 這一過程失效,具體原因請看下面的模仿實現bind()的程式碼:
Function.prototype.testBind = function(object){
var that = this,
args = Array.prototype.slice.call(arguments,1),
bound = function(){
return that.apply(this instanceof fNOP?this:object||window,
args.concat.apply(Array.from(arguments)));
};
//建立一箇中轉函式fNOP,讓bound間接繼承目標函式的原型
var fNOP = function(){};
fNOP.prototype= that.prototype;
bound.prototype= new fNOP();
return bound;
}
重點:建立一箇中轉函式fNOP,讓bound間接繼承目標函式的原型,一開始我想為什麼不直接讓 bound.prototype = that.prototype ,後來才發現直接賦值後,bound.prototype和that.prototype指向同一塊內容,如果改變bound.prototype就會直接影響that.prototype,使用一箇中轉函式, bound.prototype= new fNOP()將bound.prototype的__poro__指向fNOP.prototype,然後fNOP.prototype = that.prototype,所以此時改變bound.prototype並不會影響that.prototype。
另外:上面實現bind()的程式碼中使用apply的地方可以換成原生實現的程式碼