學習this關鍵字(二)

在上篇文章咱們明白了函數的this是在調用時被綁定的,徹底取決於函數的調用位置。這一篇咱們來深刻學習thisjquery

函數的調用位置

在理解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的綁定規則

接下來咱們來看看在函數的執行過程當中調用位置如何決定this的綁定對象。學習

默認綁定

咱們最經常使用的函數調用,能夠把這條規則看做是沒法應用其餘規則時的默認規則。測試

思考下面一段代碼:ui

function foo(){
    console.log(this.a);
}
var a = 1;
foo();
複製代碼

聲明在全局做用域中的變量a就是全局對象的一個屬性。當調用foo()函數時,this.a被解析成了全局變量a。函數調用時應用了this的默認綁定,所以this指向全局對象。this

那麼咱們怎麼知道這裏應用了默認綁定呢?能夠經過分析調用位置來看看foo()是如何調用的。在代碼中,foo()是直接使用不帶任何修飾的函數引用進行調用的,所以只能使用默認綁定,沒法應用其餘規則。spa

若是使用嚴格模式,那麼全局對象將沒法使用默認綁定,所以this會綁定到undefinedprototype

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.nameobj.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
複製代碼

雖然barobj.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),所以強制把foothis 綁定到了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
複製代碼

API調用的「上下文」

第三方庫的許多函數,以及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(..)實現了顯式綁定,這樣你能夠少些一些代碼。

new綁定

JavaScript也有一個new操做符,使用方法看起來也和那些面向類的語言同樣,然而,JavaScriptnew的機制實際上和麪向類的語言徹底不一樣。在JavaScript中,構造函數只是一些使用new操做符時被調用的函數。

使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。(這裏簡單說明一下,之後會有專門的章節介紹)

  1. 建立(或者說構造)一個全新的對象。
  2. 這個新對象會被執行[[原型]]鏈接。
  3. 這個新對象會綁定到函數調用的this
  4. 若是函數沒有返回其餘對象,那麼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優先級

如今咱們已經瞭解了函數調用中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綁定和顯式綁定哪一個優先級更高?咱們來測試一下:

newcall/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函數調用中的thisnew綁定生成了一個新對象bazbar.name的值爲cat

判斷this

  1. 函數是否在new中調用(new綁定)?若是是的話this綁定的是新建立的對象。

    var bar = new foo()

  2. 函數是否經過call、apply(顯式綁定)或者硬綁定調用?若是是的話,this綁定的是指定的對象。

    var bar = foo.call(obj)

  3. 函數是否在某個上下文對象中調用(隱式綁定)?若是是的話, this綁定的是那個上下文對象。

    var bar = obj.foo()

  4. 若是都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到undefined,不然綁定到全局對象。

    var bar = foo()

綁定的例外

被忽略的this

若是你把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的綁定對象。

  1. new調用?綁定到新建立的對象。
  2. callapply(或者bind)調用?綁定到指定的對象。
  3. 由上下文對象調用?綁定到那個上下文對象。
  4. 默認:在嚴格模式下綁定到undefined,不然綁定到全局對象。

參考

相關文章
相關標籤/搜索