【JavaScript】this - 筆記

this

關於this,其實this是提供了一種更優雅的方式來隱式「傳遞」一個對象引用。靈活的應用this,可讓咱們在寫內容較多的代碼時,不會顯得愈來愈混亂。bash

關於this

指向自身

根據英語的語法角度來推斷的話,this能夠理解爲指向函數自身。可是在實際中並非如此。app

function foo(num) {
    console.log( "foo: " + num );
    // 記錄 foo 被調用的次數
    this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9


console.log( foo.count );// 0

console.log(count); //NaN
複製代碼

上述代碼中,咱們企圖用this.count指向foo函數自己,以此在其做用域建立一個count屬性。可是是代碼的實際結果告訴咱們,這樣寫並不能作到咱們所想要的結果。函數

這種寫法,最後會使代碼在無心中建立了一個全局變量count,其值爲NaNoop

若是要從函數對象內部引用其自身,只使用this是不夠的。通常而言,須要一個指向函數對象的詞法標識符來引用它。ui

對於上述狀況,其實有許多解決方式。可是既然咱們的主題是this,那就用this的方式來解決。this

function foo(num) {
    console.log( "foo: " + num );
    // 記錄 foo 被調用的次數
    // 注意,在當前的調用方式下(參見下方代碼),this 確實指向 foo
    this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
    if (i > 5) {
        // 使用 call(..) 能夠確保 this 指向函數對象 foo 自己
        foo.call( foo, i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

console.log( foo.count ); // 4
複製代碼

這個方式,是強制this指向foo函數對象。編碼

this的做用域

對於this,還有一種誤解。就是this指向函數的做用域。spa

可是實際上,this在任何狀況下都不指向函數的詞法做用域。prototype

舉個例子:code

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log( this.a );
}
foo(); // ReferenceError: a is not defined
複製代碼

上述方法,其實在有必定代碼基礎的人眼裏,會以爲很奇怪。由於咱們要調用bar()方法的話,這個this其實是多餘的。

而且,this這個操做並不能跨做用域引用變量。

切記,不要將this和詞法做用域混合使用。

What is 'this'

在標題上玩了一下英語的諧音梗,哈哈哈,但願不要介意。

上面講述了那些以後,咱們須要瞭解到的是,this究竟是一個什麼樣的機制。

其實,this的綁定和函數聲明的位置,沒有半毛錢關係,半分錢也沒有。它只取決於函數的調用方式。

當一個函數被調用時,會建立一個執行上下文。它會包含函數在哪裏被調用、調用方法和傳參等信息。this就是其中的一個屬性,在函數執行過程當中會用到。

this的全面解析

綁定規則

在瞭解this的綁定規則以前,咱們須要知道了解調用位置。

調用位置實際上就是指咱們調用對象的詞法標識符的位置。

this的綁定規則有四條。

默認綁定

最經常使用的函數調用類型:獨立函數調用。這種方法就可使用默認綁定。

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

var a = 2;
 foo(); //2
複製代碼

咱們能夠發現,咱們在調用foo()時,其內部的this指向了全局對象,這是由於應用了this的默認綁定。

代碼中,foo()是直接使用不帶任何修飾的函數引用進行調用,所以只能使用默認綁定,沒法應用其餘規則。

須要注意的是,上述狀況只有在非嚴格模式下 使用。

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

隱式綁定

隱式的使用,須要考慮調用位置是否有上下文對象。

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

var obj = {
    a:2,
    foo:foo
};
obj.foo();
複製代碼

咱們在使用了obj.foo()的方式來調用函數時,函數foo()其實不屬於obj對象。可是這種方式,讓函數的落腳點在obj對象內,this指向的是obj對象內,所以能夠調用到obj內部的變量。

隱式丟失 在使用this綁定的時候,有個常見的問題,就是被隱式綁定的函數會丟失綁定對象。

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,可是實際上,引用的仍是函數自己。因此,此時 bar()實際上是一個不帶任何修飾的函數調用 ,所以應用了默認綁定。

還有另外一種狀況:

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    // fn 其實引用的是 foo
    fn(); // <-- 調用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性
doFoo( obj.foo ); // "oops, global"
複製代碼

咱們在使用this的時候,應該嚴格注意避開相似狀況。由於咱們沒法控制回調函數的執行方式,從而致使沒法正確的使用咱們想要使用的綁定方式。

顯式綁定

JavaScript 中的「全部」函數都有一些有用的特性,能夠用來解決這個問題。具體點說,可使用函數的 call(..)apply(..) 方法。嚴格來講,JavaScript 的宿主環境有時會提供一些很是特殊的函數,它們並無這兩個方法。可是這樣的函數很是罕見,JavaScript 提供的絕大多數函數以及你本身建立的全部函數均可以使用 call(..)apply(..) 方法。

顯示綁定主要分爲兩種。

  1. 硬綁定 硬綁定其實就是使用 call(..)apply(..) 方法。

    call

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

    apply:

    function foo(something) {
        console.log( this.a, something );
        return this.a + something;
    }
    var obj = {
        a:2
    };
    var bar = function() {
        return foo.apply( obj, arguments );
    };
    var b = bar( 3 ); // 2 3
    console.log( b ); // 5
    複製代碼

因爲硬綁定是一種很是經常使用的模式,因此在 ES5 中提供了內置的方法Function.prototype. 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 ); // 2 3
console.log( b ); // 5
複製代碼

bind(..) 會返回一個硬編碼的新函數,它會把參數設置爲 this 的上下文並調用原始函數。

  1. API調用的上下文

第三方庫的許多函數,以及JavaScript語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,一般被稱爲「上下文」(context),其做用和 bind(..) 同樣,確保你的回調函數使用指定的 this

實際上該種方法也是經過call()或者apply()來實現的顯式綁定,只不過不須要咱們本身來寫。

new綁定

在傳統的面向類的語言中,「構造函數」是類中的一些特殊方法,使用 new 初始化類時會 調用類中的構造函數。一般的形式是這樣的:

something = new MyClass(..);
複製代碼

JavaScript 也有一個 new 操做符,使用方法看起來也和那些面向類的語言同樣,絕大多數開發者都認爲 JavaScriptnew 的機制也和那些語言同樣。然而,JavaScriptnew 的機制實際上和麪向類的語言徹底不一樣。

實際上, 在JavaScript中並不存在所謂的「構造函數」,只有對於函數的「構造調用」。

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

  1. 建立(或者說構造)一個全新的對象。
  2. 這個新對象會被執行 [[ 原型 ]] 鏈接。
  3. 這個新對象會綁定到函數調用的 this
  4. 若是函數沒有返回其餘對象,那麼 new 表達式中的函數調用會自動返回這個新對象。
function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
複製代碼

使用 new 來調用 foo(..) 時,咱們會構造一個新對象並把它綁定到 foo(..) 調用中的 this上。 new 是最後一種能夠影響函數調用時 this 綁定行爲的方法,咱們稱之爲 new綁定。

優先級

上述說了四種綁定方式,在實際開發中,確定會遇到多種綁定交叉的狀況。那咱們就須要知道,綁定的優先級如何,才能夠預知到代碼結果。

判斷this

如今咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。能夠按照下面的 順序來進行判斷:

  1. 函數是否在 new 中調用( new 綁定)?若是是的話 this 綁定的是新建立的對象。
var bar = new foo()
複製代碼
  1. 函數是否經過 callapply (顯式綁定)或者硬綁定調用?若是是的話, this 綁定的是指定的對象。
var bar = foo.call(obj2)
複製代碼
  1. 函數是否在某個上下文對象中調用(隱式綁定)?若是是的話, this 綁定的是那個上 下文對象。
var bar = obj1.foo()
複製代碼
  1. 若是都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到 undefined ,不然綁定到全局對象。
var bar = foo()
複製代碼

就是這樣。對於正常的函數調用來講,理解了這些知識你就能夠明白 this 的綁定原理了。

綁定例外

凡是規則,都會有出現意外的狀況。this綁定也不例外。

function foo() {
    console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
複製代碼

當咱們把null或者undefined傳入callapply或者bind時,這些值在調用時會被忽略,從而去應用默認綁定規則。

間接引用

當咱們建立一個函數的「間接引用」時,調用這個函數就會應用默認綁定規則。

簡介引用最容易在賦值時發生。

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

賦值表達式 p.foo = o.foo 的返回值是目標函數的引用,所以調用位置是 foo() 而不是 p.foo() 或者 o.foo() 。因此這裏會應用默認綁定。

對於默認綁定來講,決定 this 綁定對象的並非調用位置是否處於嚴格模式,而是 函數體是否處於嚴格模式。若是函數體處於嚴格模式, this 會被綁定到 undefined ,不然 this 會被綁定到全局對象。

軟綁定

前面有提到一種綁定方式,叫作硬綁定,因此通常相對的,其實也有一種叫作軟綁定的方式。

使用硬綁定會下降函數的靈活性,以後沒法再使用隱式綁定或者顯式綁定來修改this。可是軟綁定 ,能夠保留隱式綁定和顯示綁定修改this的能力。

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕獲全部 curried 參數
        var curried = [].slice.call( arguments, 1 );
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ?
                    obj : this
                curried.concat.apply( curried, arguments )
            );
        };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}
複製代碼

this詞法

ES6中,介紹了一種特殊的函數類型:箭頭函數。

箭頭函數並非使用 function關鍵字定義的,而是使用被稱爲「胖箭頭」的操做符 => 定 義的。箭頭函數不使用 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 綁定到 obj1bar (引用箭頭函數)的 this 也會綁定到 obj1 ,箭頭函數的綁定沒法被修改。

箭頭函數能夠像 bind(..) 同樣確保函數的 this 被綁定到指定對象,此外,其重要性還體 如今它用更常見的詞法做用域取代了傳統的 this 機制。

總結

要判斷this綁定,就須要先找到函數的直接調用位置。

以後根據規則來判斷this的綁定對象:

1. `new`調用
複製代碼

2.callapply或者bind 3.上下文對象調用 4.默認綁定

ES6中的箭頭函數並不會使用四條標準的綁定規則。而是根據當前的詞法做用域來決定this箭頭函數會繼承外層函數調用的 this 綁定

相關文章
相關標籤/搜索