在上篇文章咱們明白了函數的this
是在調用時被綁定的,徹底取決於函數的調用位置。這一篇咱們來深刻學習this
。jquery
在理解this
的綁定過程以前,首先要理解調用位置:調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。app
function foo(){
// 當前調用棧是:foo
console.log( "foo" );
bar();// bar的調用位置
}
function bar(){
// 當前調用棧是 foo -> bar
console.log( "bar" );
baz();//baz的調用位置
}
function baz(){
// 當前調用棧是 foo -> bar -> baz
console.log( "baz" );
}
foo();// foo的調用位置
複製代碼
使用Chrome
控制檯,也能很方便的看出調用棧,如圖: 函數
接下來咱們來看看在函數的執行過程當中調用位置如何決定this
的綁定對象。學習
咱們最經常使用的函數調用,能夠把這條規則看做是沒法應用其餘規則時的默認規則。測試
思考下面一段代碼:ui
function foo(){
console.log(this.a);
}
var a = 1;
foo();
複製代碼
聲明在全局做用域中的變量a
就是全局對象的一個屬性。當調用foo()
函數時,this.a
被解析成了全局變量a
。函數調用時應用了this
的默認綁定,所以this
指向全局對象。this
那麼咱們怎麼知道這裏應用了默認綁定呢?能夠經過分析調用位置來看看foo()
是如何調用的。在代碼中,foo()
是直接使用不帶任何修飾的函數引用進行調用的,所以只能使用默認綁定,沒法應用其餘規則。spa
若是使用嚴格模式,那麼全局對象將沒法使用默認綁定,所以this
會綁定到undefined
:prototype
function foo() {
"use strict";
console.log( this.a );
}
var a = 1;
foo(); //Uncaught TypeError: Cannot read property 'a' of undefined
複製代碼
函數調用位置是否有上下文對象。3d
思考下面一段代碼:
function foo() {
console.log(this.name);
}
var obj = {
name: 'tom',
foo: foo
};
obj.foo(); // tom
複製代碼
這裏foo()
函數的調用是經過一個對象obj
的屬性來實現,調用位置會使用obj
上下文來引用函數,所以你能夠說函數調用時obj
對象「包含」它。
當foo()
被調用時,函數確實指向obj
對象。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this
綁定到這個上下文對象。由於調用foo()
時this
被綁定到obj
,所以this.name
和obj.name
是同樣的。
一個最多見的this
綁定問題就是被隱式綁定的函數會丟失綁定對象,也就是說它會應用默認綁定,從而把this
綁定到全局對象或者undefined
上,取決因而否是嚴格模式。
思考下面一段代碼:
function foo() {
console.log(this.name);
}
var obj = {
name: 'tom',
foo: foo
};
var bar = obj.foo;
var name = 'cat';
bar(); // cat
複製代碼
雖然bar
是obj.foo
的一個引用,可是實際上,它引用的是foo
函數自己,所以此時的bar()
實際上是一個不帶任何修飾的函數調用,所以應用了默認綁定。
在看另外一個微妙的例子:
function foo() {
console.log(this.name);
}
function handleFoo(fn){
fn();
}
var obj = {
name: 'tom',
foo: foo
};
var name = 'cat';
handleFoo(obj.foo); // cat
複製代碼
參數傳遞其實就是一種隱式賦值,所以咱們傳入函數時也會被隱式賦值,因此結果和上一個例子同樣。
還有值得注意的是,系統內置的函數也會丟失this
綁定,例如:
function foo() {
console.log(this.name);
}
var obj = {
name: 'tom',
foo: foo
};
var name = 'cat';
setTimeout(obj.foo, 100); //cat
複製代碼
回調函數丟失this
綁定是很是常見的。除了系統內置的函數外,jquery
中的回調就是其中之一,等等其餘第三方庫也都有此現象。
JavaScript
提供的絕大多數函數以及你本身建立的全部函數均可以使用call(..)
和apply(..)
方法。
思考下面一段代碼:
function foo(){
console.log(this.name);
};
var obj = {
name:'cat'
};
foo.call(obj); //cat
複製代碼
經過foo.call()
,咱們能夠在調用foo
函數時強制把它的this
綁定到obj
上。
思考下面一段代碼:
function foo(){
console.log(this.name);
}
var obj = {
name:'cat'
}
var bar = function(){
foo.call(obj);
}
bar(); //cat
setTimeout(bar,100); //cat
bar.call(window); //cat
複製代碼
咱們建立了一箇中間函數bar
,並在它的內部手動調用了foo.call(obj)
,所以強制把foo
的this
綁定到了obj
。不管以後如何調用函數bar
,它總會手動在obj
上調用foo
。這種綁定是一種顯式的強制綁定,所以咱們稱之爲硬綁定。
硬綁定的一個經典應用:
function foo(){
console.log(this.name);
}
var obj = {
name:'cat'
}
function bind(fn,obj){
return function (){
return fn.apply(obj,arguments);
}
}
var bar = bind(foo,obj);
bar(); //cat
複製代碼
因爲硬綁定是一種很是經常使用的模式,因此在ES5
中提供了內置的方法Function.prototype.bind
,它的用法以下:
function foo(){
console.log(this.name);
}
var obj = {
name:'cat'
}
var bar = foo.bind(obj);
bar(); //cat
複製代碼
第三方庫的許多函數,以及JavaScript
語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,一般被稱爲「上下文」,其做用和bind(..)
同樣,確保你的回調函數使用指定的this
。
例以下面代碼:
function foo(index){
console.log(index, this.name);
}
var obj = {
name:'cat'
}
var arr = [1, 2, 3];
arr.forEach(foo, obj);
複製代碼
這些函數實際上就是經過call(..)
或者apply(..)
實現了顯式綁定,這樣你能夠少些一些代碼。
JavaScript
也有一個new
操做符,使用方法看起來也和那些面向類的語言同樣,然而,JavaScript
中new
的機制實際上和麪向類的語言徹底不一樣。在JavaScript
中,構造函數只是一些使用new
操做符時被調用的函數。
使用new
來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。(這裏簡單說明一下,之後會有專門的章節介紹)
- 建立(或者說構造)一個全新的對象。
- 這個新對象會被執行[[原型]]鏈接。
- 這個新對象會綁定到函數調用的
this
。- 若是函數沒有返回其餘對象,那麼
new
表達式中的函數調用會自動返回這個新對象。
示例代碼:
function foo(name){
this.name = name;
}
var bar = new foo('cat');
console.log(bar.name); //cat
複製代碼
使用new
來調用foo(..)
時,咱們會構造一個新對象並把它綁定到foo(..)
調用中的this
上。new
是最後一種能夠影響函數調用時this
綁定行爲的方法,咱們稱之爲new
綁定。
如今咱們已經瞭解了函數調用中this
綁定的四條規則,咱們須要作的就是找到函數的調用位置並判斷應當應用哪條規則。
默認綁定的優先級是四條規則中最低的。
function foo(){
console.log(this.name);
};
var obj1 = {
a: 'tom',
foo: foo
};
var obj2 = {
a: 'cat',
foo: foo
};
obj1.foo(); // tom
obj2.foo(); // cat
obj1.foo().call(obj2); //cat
obj2.foo().call(obj1); //tom
複製代碼
能夠看到,顯式綁定優先級更高。
new
綁定哪一個優先級更高?咱們來測試一下:function foo(name){
this.name = name;
};
var obj = {
foo:foo
};
obj.foo('tom');
console.log(obj.name); //tom
var bar = new obj.foo('cat');
console.log(bar.name); //cat
複製代碼
能夠看到,new
綁定優先級高。
new
綁定和顯式綁定哪一個優先級更高?咱們來測試一下:
new
和call/apply
沒法一塊兒使用所以沒法經過new foo.call(obj1)
來直接進行測試,可是咱們能夠間接的測試。
function foo(name){
this.name = name;
};
var obj = {};
var bar = foo.bind(obj);
bar('tom');
console.log(obj.name); //tom
var baz = new bar('cat');
console.log(obj.name); //tom
console.log(baz.name); //cat
複製代碼
bar
函數被硬綁定到obj
對象上,但new bar('cat')
並無像咱們預計的那樣把obj.name
修改成cat
。相反new
修改了硬綁定bar
函數調用中的this
。new
綁定生成了一個新對象baz
,bar.name
的值爲cat
。
函數是否在new
中調用(new
綁定)?若是是的話this
綁定的是新建立的對象。
var bar = new foo()
函數是否經過call、apply
(顯式綁定)或者硬綁定調用?若是是的話,this
綁定的是指定的對象。
var bar = foo.call(obj)
函數是否在某個上下文對象中調用(隱式綁定)?若是是的話, this
綁定的是那個上下文對象。
var bar = obj.foo()
若是都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到undefined
,不然綁定到全局對象。
var bar = foo()
若是你把null
或者undefined
做爲this
的綁定對象傳入call、apply
或者bind
,這些值在調用時會被忽略,實際應用的是默認綁定規則:
function foo(){
console.log(this.name);
}
var name = 'tom';
foo.call(null); //tom
複製代碼
另外一個須要注意的是,有可能建立一個函數的「間接引用」,在這種狀況下,調用這個函數會應用默認綁定規則:
function foo(){
console.log(this.name);
}
var name = 'tom';
var obj = {
name:'cat',
foo:foo
}
obj.foo(); //cat
var baz = obj.foo;
baz(); //tom
複製代碼
若是要判斷一個運行中函數的this
綁定,就須要找到這個函數的調用位置。找到以後就能夠順序應用下面這四條規則來判斷this
的綁定對象。
new
調用?綁定到新建立的對象。call
、apply
(或者bind
)調用?綁定到指定的對象。undefined
,不然綁定到全局對象。