1. 程式人生 > >JavaScript中的this—你不知道的JavaScript上卷讀書筆記(三)

JavaScript中的this—你不知道的JavaScript上卷讀書筆記(三)

情況下 修改 位置 必須 細節 fin 有關 zed 重要

this是什麽?

this 是在運行時進行綁定的,並不是在編寫時綁定,它的上下文取決於函數調用時的各種條件。this 的綁定和函數聲明的位置沒有任何關系,只取決於函數的調用方式。當一個函數被調用時,會創建一個活動記錄(有時候也稱為執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this 就是記錄的其中一個屬性,會在函數執行的過程中用到。

調用位置與調用棧:

調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。

下面我們來看看到底什麽是調用棧和調用位置:

function baz() {
    // 當前調用棧是:baz
    // 因此,當前調用位置是全局作用域
    console.log( "baz" );
    bar(); // <-- bar 的調用位置
}

function bar() {
    // 當前調用棧是baz -> bar
    // 因此,當前調用位置在baz 中
    console.log( "bar" );
    foo(); // <-- foo 的調用位置
}

function foo() {
    // 當前調用棧是baz -> bar -> foo
    // 因此,當前調用位置在bar 中
    console.log( "foo" );
}

baz(); // <-- baz 的調用位置

this綁定規則

1. 默認綁定:
function foo() {
    console.log( this.a );
}
var a = 2;
foo(); // 2

上述代碼中,函數調用時應用了this 的默認綁定,因此this 指向全局對象(非嚴格模式下)。

那麽我們怎麽知道這裏應用了默認綁定呢?可以通過分析調用位置來看看foo() 是如何調用的。在代碼中,foo() 是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則。

如果使用嚴格模式(strict mode),那麽全局對象將無法使用默認綁定,因此this 會綁定到undefined:

function foo() {
    "use strict";
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

這裏有一個微妙但是非常重要的細節,雖然this 的綁定規則完全取決於調用位置,但是只有foo() 運行在非strict mode 下時,默認綁定才能綁定到全局對象;嚴格模式下與foo()的調用位置無關:

function foo() {
    console.log( this.a );
}
var a = 2;

(function(){
    "use strict";
    foo(); // 2
})();
2. 隱式綁定:
function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

當foo() 被調用時,它的落腳點指向obj 對象。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this 綁定到這個上下文對象。因為調用foo() 時this 被綁定到obj,因此this.a 和obj.a 是一樣的。

對象屬性引用鏈中只有最頂層或者說最後一層會影響調用位置

隱式丟失

一個最常見的this 綁定問題就是被隱式綁定的函數會丟失綁定對象,也就是說它會應用默認綁定,從而把this 綁定到全局對象或者undefined 上,取決於是否是嚴格模式。

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函數別名!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"

雖然bar 是obj.foo 的一個引用,但是實際上,它引用的是foo 函數本身,因此此時的bar() 其實是一個不帶任何修飾的函數調用,因此應用了默認綁定。

3. 顯式綁定:

就像我們剛才看到的那樣,在分析隱式綁定時,我們必須在一個對象內部包含一個指向函數的屬性,並通過這個屬性間接引用函數,從而把this 間接(隱式)綁定到這個對象上。那麽如果我們不想在對象內部包含函數引用,而想在某個對象上強制調用函數,該怎麽做呢?

JavaScript 中的“所有”函數都有一些有用的特性(這和它們的[[ 原型]] 有關——之後我們會詳細介紹原型),可以用來解決這個問題。具體點說,可以使用函數的call(..) 和apply(..) 方法。嚴格來說,JavaScript 的宿主環境有時會提供一些非常特殊的函數,它們並沒有這兩個方法。但是這樣的函數非常罕見,JavaScript 提供的絕大多數函數以及你自己創建的所有函數都可以使用call(..) 和apply(..) 方法。

function foo() {
    console.log( this.a );
}
var obj = {
    a:2
};
foo.call( obj ); // 2

通過foo.call(..),我們可以在調用foo 時強制把它的this 綁定到obj 上。

顯式綁定仍然無法解決我們之前提出的丟失綁定問題,通過以下方法可以解決:

硬綁定

    function foo() {
        console.log( this.a );
    }
    var obj = {
        a:2
    };
    var bar = function() {
        foo.call( obj );
    };
    bar(); // 2
    setTimeout( bar, 100 ); // 2
    // 硬綁定的bar 不可能再修改它的this
    bar.call( window ); // 2
4. new綁定:

使用new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。

  1. 創建(或者說構造)一個全新的對象。

  2. 這個新對象會被執行[[ 原型]] 連接。
  3. 這個新對象會綁定到函數調用的this。

  4. 如果函數沒有返回其他對象,那麽new 表達式中的函數調用會自動返回這個新對象。

    function foo(a) {
        this.a = a;
    }
    var bar = new foo(2);
    console.log( bar.a ); // 2

優先級: new綁定>顯式綁定>隱式綁定

判斷this:

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

    var bar = new foo()

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

    var bar = foo.call(obj2)

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

    var bar = obj1.foo()

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

    var bar = foo()

綁定例外

1. 被忽略的this

把null 或者undefined 作為this 的綁定對象傳入call、apply 或者bind,這些值在調用時會被忽略,實際應用的是默認綁定規則。

function foo() {
    console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
更安全的ths

一種“更安全”的做法是傳入一個特殊的對象,把this 綁定到這個對象不會對你的程序產生任何副作用。。就像網絡(以及軍隊)一樣,我們可以創建一個“DMZ”(demilitarized zone,非軍事區)對象——它就是一個空的非委托的對象。

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 我們的DMZ 空對象
var ? = Object.create( null ); //此處的?不會創建Object.prototype 這個委托,所以它比{}“更空”
// 把數組展開成參數
foo.apply( ?, [2, 3] ); // a:2, b:3
// 使用bind(..) 進行柯裏化
var bar = foo.bind( ?, 2 );
bar( 3 ); // a:2, b:3

2. 間接引用

另一個需要註意的是,你有可能(有意或者無意地)創建一個函數的“間接引用”,在這種情況下,調用這個函數會應用默認綁定規則。

function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

3. 軟綁定(此處不討論)

this詞法(箭頭函數的this)

function foo() {
    // 返回一個箭頭函數
    return (a) => {
        //this 繼承自foo()
        console.log( this.a );
    };
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3 !

foo() 內部創建的箭頭函數會捕獲調用時foo() 的this。由於foo() 的this 綁定到obj1,bar(引用箭頭函數)的this 也會綁定到obj1,箭頭函數的綁定無法被修改。(new 也不行!)

JavaScript中的this—你不知道的JavaScript上卷讀書筆記(三)