原文:https://zhehuaxuan.github.io/...
作者:zhehuaxuan
目的
本文主要用於理解和掌握call,apply和bind的使用和原理,本文適用於對它們的用法不是很熟悉,或者想搞清楚它們原理的童鞋。
好,那我們開始!
在JavaScript中有三種方式來改變this的作用域call,apply和bind。我們先來看看它們是怎麼用的,只有知道怎麼用的,我們才能來模擬它。
Function.prototype.call()
首先是Function.prototype.call(),不熟的童鞋請猛戳MDN,它是這麼説的: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()的效果,當然前提是destination要有一個source屬性
好,現在大家應該明白call()的基本用法,我們再來看下面的例子:
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()也可以傳參,而且是以參數,參數,...的形式傳入。
上述我們知道call()的兩個作用:
1.改變this的指向2.支持對函數傳參
我們看到最後還還輸出一個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就返回什麼。
模擬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"));
打印效果如下:
參數出現了,現在就剩下返回值了,很簡單,我們再修改一下:
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;
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的作用,但是需要在調用bind()時生效2.bind中也可以添加參數
明白了區別,下面我們來模擬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]);
//因為ctx是外層的this指針,在外層我們使用一個變量that引用進來
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.bind1(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==>source.prototype
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()函數的模擬實現完畢!如有不對之處,歡迎拍磚!您的寶貴意見是我寫作的動力,謝謝大家。