原文:zhehuaxuan.github.io/2019/02/26/…
做者:zhehuaxuanjavascript
在JavaScript中有三種方式來改變this
的做用域call
,apply
和bind
。它們在前端開發中頗有用。好比:繼承,React的事件綁定等,本文先講用法,再講原理,最後本身模擬,旨在對這塊內容有系統性掌握。前端
在MDN中對call()
解釋以下:
call()
容許爲不一樣的對象分配和調用屬於一個對象的函數/方法。
也就是說:一個函數,只要調用call()
方法,就能夠把對象以參數傳遞給函數。java
若是仍是不明白,不急!咱們先來寫一個call()
函數最簡單的用法:git
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()的效果。github
好,明白基本用法,再來看下面的例子:數組
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)
沒有返回值。app
咱們如今給source
函數添加返回值:ide
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返回什麼。
根據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
的效果!
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()
函數的模擬完成。
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
函數。
和模擬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()
函數的模擬實現完畢!若有不對之處,歡迎拍磚!您的寶貴意見是我寫做的動力,謝謝你們。