深入理解JavaScript之this的四種繫結
之前對this的四種繫結不太理解,好在瀏覽了https://www.cnblogs.com/xiaohuochai/p/5735901.html這篇博文,才得以清晰思路,接下來我再次總結this的四種繫結機制。
1 this的四種繫結機制
在JavaScript中this共有四種繫結機制:分別是
- new繫結
- 顯式繫結(包括硬繫結)
- 隱式繫結
- 預設繫結
繫結優先順序從上到下
2 預設繫結
- 在非嚴格模式下,預設繫結指的是,函式獨立呼叫,不新增修飾的函式呼叫,它繫結的是全域性物件
- 嚴格模式下,this繫結"undefined"
接下來我們將預設繫結分為幾種情況,在這幾種情況下發生的一般都是預設繫結
2.1全域性作用域中使用this
我們不在任何函式中使用this,直接在全域性作用域中使用this,那麼這個this指向的是全域性物件
var a=2;
console.log(this.a); //繫結全域性物件屬性a=2
2.2 函式獨立呼叫時
我們在全域性作用域中單獨呼叫某一個函式,這個函式的this繫結的是全域性物件
function foo() { var a=3; console.log(this.a); } var a=2; foo(); //值為2,而不是3,代表繫結的是全域性物件
2.3 被巢狀的獨立函式呼叫時
被巢狀的函式獨立呼叫時,this預設繫結的是window物件
var a=2;
var obj={
a:3,
foo:function () {
function bar() {
console.log(this.a);
}
bar();
}
};
obj.foo(); //2
雖然bar(...)是巢狀在obj的foo(...)函式中,由於bar(...)是獨立呼叫,而不是方法呼叫,因此this被繫結到全域性物件window
2.4 立即執行函式表示式
在立即執行函式表示式中的this也會預設繫結全域性物件。
var a=2;
function foo() {
(function bar() {
console.log(this.a);
})();
}
var obj={
a:3,
foo:foo()
};
obj.foo; //2
foo(...)是obj的屬性函式,應該是屬於上下文呼叫,this的繫結是隱式繫結,但是在foo(...)函式中我們執行的程式是IIFE,那麼this就會預設繫結全域性物件。
2.5 閉包
我們先回憶一下閉包,閉包是外部函式內部定義了內部函式,在全域性作用域(或者是其他的作用域,只要不是外部函式本身的作用域)中呼叫內部函式,使得可以訪問外部函式作用域的功能。
var a=2;
function foo() { //外部函式
function bar() { //內部函式
console.log(this.a)
}
return bar(); //返回內部函式
}
var obj={
a:3,
foo:foo()
};
obj.foo; //2
那麼在閉包中,我們需要訪問到外部函式的作用域,這時我們該怎麼做呢?答案是使用 var self=this,在內部函式中使用self代替this。
var a=2;
function foo() { //外部函式
var self=this; //self代替this
function bar() { //內部函式
console.log(self.a) //self代替this
}
return bar; //返回內部函式
}
var obj={
a:3,
foo:foo
};
obj.foo()(); //3
2.6 隱式丟失
在this的隱式繫結過程中,由於失去了繫結物件,從而應用預設繫結規則。
2.6.1 函式別名的隱式丟失
在this的隱式繫結過程中,給帶有上下文的this函式值起一個別名(建立一個新的物件)
var a=2;
function foo() {
console.log(this.a);
}
var obj={
a:3,
foo:foo
};
obj.foo; //3
var bar=obj.foo; //函式別名
bar(); //2 隱式繫結的過程中發生了丟失
原本在obj中的foo屬性:foo(...)中的this繫結上下文obj物件,但是我們建立一個bar()作為obj.foo的引用,實際上bar()引用的是foo(...)函式的本身,bar()是一個不帶任何裝飾的函式呼叫,因此預設函式呼叫
2.6.2 傳入回撥函式時,引數的隱式賦值
在傳入回撥函式時,引數之間的隱式賦值也會導致this物件丟失從而應用預設繫結
var a=2;
function foo() {
console.log(this.a);
}
var obj={
a:3,
foo:foo
};
function doo(fn) { //傳入obj.foo,傳入的實際上只有foo(...),這算是一個獨立的函式呼叫
fn();
}
doo(obj.foo); //2 obj.foo傳值給foo,就跟之前的var fn=obj.foo;
2.6.3 內建函式
當我們使用Javascript時內建函式時,也會發生this物件的改變,從而應用預設繫結
var a=2;
function foo() {
console.log(this.a);
}
var obj={
a:3,
foo:foo
};
setTimeout(obj.foo,100);
我們應用了"setTimeout(...)"內建函式,原本foo(...)中的this應該繫結到obj上,但是我們引用了javascript內建函式,“obj.foo”作為引數,導致this應用了預設繫結。
3 隱式繫結
當呼叫有上下文物件的函式時,便是隱式繫結。通俗點來說,就是被直接物件所包含的函式呼叫時,也稱為該直接物件的方法呼叫。
function foo() {
console.log(this.a);
}
var obj={
a:2,
foo:foo //foo成為obj中的方法
}
obj.foo(); //obj物件的foo方法----方法呼叫,
函式foo(...)被儲存在obj物件中,成為obj物件的一個屬性,這時,我們稱這個函式(foo)為這個物件(obj)的方法,在最後一行,我們對這個函式進行呼叫(物件.屬性或者說是物件.方法),這就是方法的呼叫。而這個物件我們稱之為上下文物件。
3.1 多個巢狀的上下文物件呼叫
什麼是多個巢狀的上下文物件呼叫呢,就是在一個物件中引用另一個物件的方法,此時的this會繫結第一的物件還是最後的物件呢?
function foo() {
console.log(this.a);
}
var obj2={
a:2,
foo:foo
};
var obj1={
a:3,
obj2:obj2,
foo:foo
};
obj1.foo(); //3
obj2.foo(); //2
obj1.obj2.foo(); //2 巢狀的上下文呼叫
從結果可以看出,當發生巢狀的上下文呼叫時,首先this傳入的值是obj2的值,那麼就是說發生巢狀的上下文呼叫時,this的繫結物件是離this最近的那一個。
注意!!!在這裡,var obj2的定義一定要比 var obj1的定義位置靠前,因為之前講了函式的提升,變數(物件)的提升優先順序是一樣的,當先宣告obj1後宣告obj2時,會發生錯誤。
3.2 間接引用
在一般的程式設計中,函式的間接引用是最容易導致隱式丟失的
ar a=2;
function foo() {
console.log(this.a);
}
var obj1={
a:3,
foo:foo
};
var obj2={
a:4,
};
obj1.foo(); //3
obj2.foo=obj1.foo; //foo不存在obj2中,this繫結的物件應該是obj1
obj2.foo(); //4
var a=2;
function foo() {
console.log(this.a);
}
var obj1={
a:3,
foo:foo
};
var obj2={
a:4,
};
obj1.foo(); //3
(obj2.foo=obj1.foo)(); //2 IIFE中this預設繫結
3.3 其他情況
在JavaScript內部,obj物件單獨有一個記憶體地址M1,obj的屬性foo函式單獨有一個記憶體地址M2,當"obj.foo"時,引擎首先找到M1地址,再從M1地址處呼叫M2地址,這樣foo的this就會繫結到obj上。
如果單獨引用M2地址,就會發生隱式丟失
4 顯示繫結
顯示繫結是利用JavaScript中的方法".call(...)、.apply(...)"強制this繫結到指定物件上。對於被呼叫的函式來說這是間接引用
4.1 call()方法
call方法是JavaScript中,強制使物件繫結到指定物件上。
var a=3;
function foo(b) {
console.log(this.a);
}
var obj={
a:2
};
foo(1); //4
foo.call(obj,1); //3 強制foo繫結到obj上
4.2 apply()方法
apply()方法與call()方法作用相同,不同的是兩者除了第一個引數是物件其餘的引數不同。具體可看這一篇文章
https://www.cnblogs.com/lengyuehuahun/p/5643625.html
var a=3;
function foo(b,c) {
console.log(this.a+b+c);
}
var obj={
a:2
};
foo(1,2); //3
foo.apply(obj,[1,2]); //apply()傳入的引數必須要放到一個數組中
儘管顯式繫結很厲害,但是還是解決不了隱式丟失的問題。
4.3 硬繫結
儘管顯式繫結無法解決隱式丟失的問題,但是顯式繫結的變種-----硬繫結,可以。
硬繫結是,在一個函式中通過call(...)或者是apply(...)方法將帶有this的函式繫結到指定的物件中。
簡而言之就是,將call()或者是apply()方法強制使this指向一個物件這個操作封裝起來,呼叫這個封裝函式就行了。
var a=3;
function foo() {
console.log(this.a);
}
var obj={
a:2
};
function bind() { //在bind中this與obj物件繫結到了一起,所以只要呼叫bing函式,this肯定指向obj
foo.call(obj);
};
bind(); //2
setTimeout(bind,100); //2
bind.call(wind); //2
硬繫結是如何解決隱式丟失的呢?答案是通過封裝call()、apply()方法將this繫結到指定的物件上,等到呼叫時,用這個封裝函式就行了。如此一來,無論何時,在封裝函式中,this永遠指向物件。
在JavaScript中由於對硬繫結的需求是非常大,所以JavaScript中定義了一個內建函式"bind(...)"來代表硬繫結
function foo(something) {
console.log(this.a,something);
return this.a+something;
}
var obj={
a:2
};
var bar=foo.bind(obj);
var b=bar(3);
console.log(b); //5
4.4 API呼叫
JavaScript中不止有"bind(...)"這一個函式,還有許多內建函式具有顯式繫結功能,如陣列迭代的五個方法:map(...)、forEach(...)、filter(...)、some(...)、every(...)
var id='window';
function foo(el) {
console.log(el,this.id);
}
var obj={
id:'fn'
};
[1,2,3].forEach(foo); //1|2|3 window
[1,2,3].forEach(foo,obj); //1|2|3 fn
5 new繫結
new繫結有太多細節要講,具體的可以看我之前的文章,現在是簡單介紹下new繫結的概念。
new繫結:函式或者方法呼叫之前帶有關鍵字"new",它就是建構函式呼叫,這也就是this繫結
5.1 不帶return的new繫結
當不帶return的new建構函式呼叫時,它會初始化新物件,當建構函式的函式體執行完畢以後,它會顯式返回。在這種情況下 ,建構函式的計算值便是"new"建立的新物件的值
function foo() {
this.a=2;
};
var obj=new foo();
console.log(obj); //a:2
5.2 帶有return 但是卻不返回值的new繫結
當帶有return的函式,發生,建構函式呼叫(new繫結)時,它同上面型別的new繫結是一致的,初始化新物件,執行建構函式體,顯示返回,建構函式的計算值是"new"建立的新物件的值。
function foo() {
this.a=2;
return;
};
var obj=new foo();
console.log(obj); //a:2
5.3 帶有return返回一個物件
當帶有return的函式,發生,建構函式呼叫時,並且顯式返回一個物件時,新建立的物件的值等於這個物件。
var obj1={
a:3
}
function foo() {
this.a=2;
return obj1;
};
var obj=new foo();
console.log(obj); //a:3
5.5 new繫結發生的那些事
new繫結到底發生了什麼導致this物件的改變呢?
- 建立一個全新的物件
- 這個物件被執行原型連結
- 這個物件會被繫結到指定的this
- 如果函式中沒有返回物件,那麼new表示式中的函式會自動返回這個物件
5.6 new繫結的用法
使用new繫結非常之簡單,只需要建立指定的物件,new一個this函式。
var a=3;
function foo(a) {
this.a=a;
}
foo(a);
var bar=new foo(2); //建立的bar繫結到foo中的this上
console.log(bar.a); //2
注意!!!儘管建構函式看起來很像一個方法呼叫,它依然會使用這個新物件作為this。也就是說,在表示式new o.m() 中,this不是o而是m()
6 優先順序
在優先順序上:new>顯式繫結>隱式繫結>預設繫結
7 總結
new------建構函式呼叫
顯式繫結-----間接呼叫
隱式繫結-----方法呼叫
預設繫結-----獨立呼叫