原文:zhehuaxuan.github.io/2019/02/26/…
作者:zhehuaxuan
目的
在JavaScript中有三種方式來改變this
的作用域call
,apply
和bind
。它們在前端開發中很有用。比如:繼承,React的事件繫結等,本文先講用法,再講原理,最後自己模擬,旨在對這塊內容有系統性掌握。
Function.prototype.call()
在MDN中對call()
解釋如下:
call()
允許為不同的物件分配和呼叫屬於一個物件的函式/方法。
也就是說:一個函式,只要呼叫call()
方法,就可以把物件以引數傳遞給函式。
如果還是不明白,不急!我們先來寫一個call()
函式最簡單的用法:
function source(){
console.log(this.name); //列印 xuan
}
let destination = {
name:"xuan"
};
console.log(source.call(destination));
複製程式碼
上述程式碼會列印出destination
的name
屬性,也就是說source()
函式通過呼叫call()
,source()
函式中的this
<=>destination
對應起來。類似於實現destination.source()的效果。
好,明白基本用法,再來看下面的例子:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
}
let destination = {
name:"xuan"
};
console.log(source.call(destination,18,"male"));//call本身沒有返回任何值,故undefined
複製程式碼
列印效果如下:
我們可以看到call()
支援傳參,而且是以arg1,arg2,...
的形式傳入。我們看到最後還還輸出一個undefined
,說明現在呼叫source.call(…args)
沒有返回值。
我們現在給source
函式新增返回值:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//新增一個返回值物件
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
console.log(source.call(destination,18,"male"));
複製程式碼
列印結果:
果不其然!call()
函式的返回值就是source
函式的返回值。
所以call()
函式的作用總結如下:
- 改變this的指向
- 支援對函式傳參
- 呼叫call的函式返回什麼,call返回什麼。
模擬Function.prototype.call()
根據call()
的作用,我們一步一步進行模擬。我們先把上面的部分程式碼摘抄下來:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//新增一個返回值物件
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
複製程式碼
現在只要實現一個函式call1()
並使用下面方式
console.log(source.call1(destination));
複製程式碼
如果得出的結果和call()
函式一樣,那就沒問題了。
現在我們來模擬第一步:
改變this的指向。
假設我們destination的結構是這樣的:
let destination = {
name:"xuan",
source:function(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//新增一個返回值物件
return {
age:age,
gender:gender,
name:this.name
}
}
}
複製程式碼
我們執行destination.source(18,"male");
就可以在source()
函式中把正確的結果列印出來並且返回我們想要的值。
現在我們的目的就是:給destination物件新增一個source屬性,然後新增引數執行它。
我們定義如下:
Function.prototype.call1 = function(ctx){
ctx.fn = this; //ctx為destination this指向source 那麼就是destination.fn = source;
ctx.fn(); // 執行函式
delete ctx.fn; //在刪除這個屬性
}
console.log(source.call1(destination,18,"male"));
複製程式碼
列印效果如下:
我們發現this的指向已經改變了,但是我們傳入的引數還沒有處理。
第二步:
支援對函式傳參。
我們使用ES6語法修改如下:
Function.prototype.call1 =function(ctx,...args){
ctx.fn = this;
ctx.fn(...args);
delete ctx.fn;
}
console.log(source.call1(destination,18,"male"));
複製程式碼
列印效果如下:
引數出現了。 第三步:呼叫call的函式返回什麼,call返回什麼
我們再修改一下:
Function.prototype.call1 =function(ctx,...args){
ctx.fn = this || window; //防止ctx為null的情況
let res = ctx.fn(...args);
delete ctx.fn;
return res;
}
console.log(source.call1(destination,18,"male"));
複製程式碼
列印效果如下:
現在我們實現了call
的效果!
模擬Function.prototype.apply()
apply()
函式的作用和call()
函式一樣,只是傳參的方式不一樣。apply
的用法可以檢視MDN,MDN這麼說的:apply() 方法呼叫一個具有給定this
值的函式,以及作為一個陣列(或類似陣列物件)提供的引數。
apply()
函式的第二個引數是一個陣列,陣列是呼叫apply()
的函式的引數。
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
console.log(source.apply(destination,[18,"male"]));
複製程式碼
效果和call()
是一樣的。既然只是傳參不一樣,我們把模擬call()
函式的程式碼稍微改改:
Function.prototype.apply1 =function(ctx,args){
ctx.fn = this || window;
args = args || [];
let res = ctx.fn(...args);
delete ctx.fn;
return res;
}
console.log(source.apply1(destination,[18,'male']));
複製程式碼
執行效果如下:
apply()
函式的模擬完成。
Function.prototype.bind()
bind()
的作用,我們引用MDN:
bind()
方法會建立一個新函式。當這個新函式被呼叫時,bind()
的第一個引數將作為它執行時的 this
物件,之後的一序列引數將會在傳遞的實參前傳入作為它的引數。
我們看下述程式碼:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
var res = source.bind(destination,18,"male");
console.log(res());
console.log("==========================")
var res1 = source.bind(destination,18);
console.log(res1("male"));
console.log("==========================")
var res2 = source.bind(destination);
console.log(res2(18,"male"));
複製程式碼
列印效果如下:
我們發現bind
函式跟apply
和call
有兩個區別:
1.bind返回的是函式,雖然也有call和apply的作用,但是需要在呼叫函式時生效
2.bind中也可以新增引數
注:bind還支援new語法,下面會展開。
我們先根據上述2點區別來模擬bind
函式。
模擬Function.prototype.bind()
和模擬call一樣,現摘抄下面的程式碼:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//新增一個返回值物件
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
複製程式碼
然後我們定義一個函式bind1
,如果執行下面的程式碼能夠返回和bind
函式一樣的值,就達到我們的目的。
var res = source.bind1(destination,18);
console.log(res("male"));
複製程式碼
首先我們定義一個bind1函式,因為返回值是一個函式,所以我們可以這麼寫:
Function.prototype.bind1 = function(ctx,...args){
var that = this;//外層的this通過閉包傳入內部函式中
return function(){
//將外層函式的引數和內層函式的引數合併
var all_args = [...args].concat([...arguments]);
//apply改變ctx的指向
return that.apply(ctx,all_args);
}
}
複製程式碼
列印效果如下:
這裡我們利用閉包,把外層函式的ctx
和引數args
傳到內層函式,再將內外傳遞的引數合併,然後使用apply()
或call()
函式,將其返回。
當我們呼叫res("male")
時,因為外層ctx
和args
還是會存在記憶體當中,所以呼叫時,前面的ctx
也就是source
,args
也就是18,再將傳入的"male"跟18合併[18,'male']
,執行source.apply(destination,[18,'male']);
返回函式結果即可。bind()
的模擬完成!
但是bind
除了上述用法,還可以有如下用法:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//新增一個返回值物件
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
var res = source.bind(destination,18);
var person = new res("male");
console.log(person);
複製程式碼
列印效果如下:
我們發現bind
函式支援new
關鍵字,呼叫的時候this
的繫結失效了,那麼new
之後,this
指向哪裡呢?我們來試一下,程式碼如下:
function source(age,gender){
console.log(this);
}
let destination = {
name:"xuan"
};
var res = source.bind(destination,18);
console.log(new res("male"));
console.log(res("male"));
複製程式碼
執行new
的時候,我們發現雖然bind
的第一個引數是destination
,但是this
是指向source
的。
如上所示,不用new
的話,this
指向destination
。
好,現在再來回顧一下我們的bind1
實現:
Function.prototype.bind1 = function(ctx,...args){
var that = this;
return function(){
//將外層函式的引數和內層函式的引數合併
var all_args = [...args].concat([...arguments]);
//因為ctx是外層的this指標,在外層我們使用一個變數that引用進來
return that.apply(ctx,all_args);
}
}
複製程式碼
如果我們使用:
var res = source.bind(destination,18);
console.log(new res("male"));
複製程式碼
如果執行上述程式碼,我們的ctx
還是destination
,也就是說這個時候下面的source
函式中的ctx
還是指向destination
。而根據Function.prototype.bind
的用法,這時this
應該是指向source
自身。
我們先把部分程式碼抄下來:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//新增一個返回值物件
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
複製程式碼
我們改一下bind1函式:
Function.prototype.bind1 = function (ctx, ...args) {
var that = this;//that肯定是source
//定義了一個函式
let f = function () {
//將外層函式的引數和內層函式的引數合併
var all_args = [...args].concat([...arguments]);
//因為ctx是外層的this指標,在外層我們使用一個變數that引用進來
var real_ctx = this instanceof f ? this : ctx;
return that.apply(real_ctx, all_args);
}
//函式的原型指向source的原型,這樣執行new f()的時候this就會通過原型鏈指向source
f.prototype = this.prototype;
//返回函式
return f;
}
複製程式碼
我們執行
var res = source.bind1(destination,18);
console.log(new res("male"));
複製程式碼
效果如下:
已經達到我們的效果!
現在分析一下上述實現的程式碼:
//呼叫var res = source.bind1(destination,18)時的程式碼分析
Function.prototype.bind1 = function (ctx, ...args) {
var that = this;//that肯定是source
//定義了一個函式
let f = function () {
... //內部先不管
}
//函式的原型指向source的原型,這樣執行new f()的時候this就會指向一個新的物件,這個物件通過原型鏈指向source,這正是我們上面執行apply的時候需要傳入的引數
f.prototype = this.prototype;
//返回函式
return f;
}
複製程式碼
f()函式的內部實現分析:
//new res("male")相當於執行new f("male");下面進行函式的執行態分析
let f = function () {
console.log(this);//這個時候列印this就是一個_proto_指向f.prototype的物件,因為f.prototype==>source.prototype,所以this._proto_==>source.prototype
//將外層函式的引數和內層函式的引數合併
var all_args = [...args].concat([...arguments]);
//正常不用new的時候this指向當前呼叫處的this指標(在全域性環境中執行,this就是window物件);使用new的話這個this物件的原型鏈上有一個型別是f的原型物件。
//那麼判斷一下,如果this instanceof f,那麼real_ctx=this,否則real_ctx=ctx;
var real_ctx = this instanceof f ? this : ctx;
//現在把真正分配給source函式的物件傳入
return that.apply(real_ctx, all_args);
}
複製程式碼
至此bind()
函式的模擬實現完畢!如有不對之處,歡迎拍磚!您的寶貴意見是我寫作的動力,謝謝大家。