徹底理解javascript中的this指針


http://javascriptissexy.com/understand-javascripts-this-with-clarity-and-master-it/

https://www.bennadel.com/blog/2265-changing-the-execution-context-of-javascript-functions-using-call-and-apply.htm

this關鍵字對於javascript初學者,即便是老手可能都是一個比較容易搞暈的東西。本文試圖理順這個問題。

this和自然語言的類比

實際上js中的this和我們自然語言中的代詞有類似性。比如英語中我們寫"John is running fast because he is trying to catch the train"

注意上面的代詞"he",我們當然可以這樣寫:"John is running fast because John is trying to catch the train" ,這種情況下我們沒有使用this去重用代替John。

在js中this關鍵字就是一個引用的shortcut,他指代一個object,是執行的代碼body的context環境。看下面的代碼:

var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    fullName: function () {
        ​// Notice we use "this" just as we used "he" in the example sentence earlier?:
        console.log(this.firstName + " " + this.lastName);
    ​// We could have also written this:​
        console.log(person.firstName + " " + person.lastName);
    }
}

如果我們使用person.firstName,person.lastName的話,我們的代碼可能會產生歧義。比如,如果有一個全局變量,名字就是person,那么person.firstName可能試圖從全局的person變量來訪問其屬性,這可能導致錯誤,並且難以調試。因此,我們使用this關鍵字,不僅為了代碼更美(作為以個referent),而且為了更加精確而不會產生歧義。類似於剛剛舉例的自然語言中,因為代詞"he"使得句意更清晰,因為更加清晰地表明我們正在引用一個特定的John這個人。

正如代詞"he"用於引用存於context中無歧義的名詞一樣,this關鍵字用於引用一個對象,而這個對象就是function(在該函數中,使用了this指針)所綁定的對象.(the this keyword is similarly used to refer to an object that the function(where this is used) is bound to.) t

this基礎

什么是執行上下文(execution context)?

在js中,所有的函數體(function body)都能訪問this關鍵字. this keyword就是函數執行的context.默認情況下,this引用着調用該函數的那個對象(也就是thisObj.thisFunction中的thisObj)(this is a reference to the object on which a particular function is called),在js中,所有的函數都會被綁定到一個object.然而,我們可以使用call(), apply()來在運行時變更這種binding關系。

首先,我們需要澄清的是:js中所有的函數實際上都是"methods".也就是說所有的function都是某個對象的屬性。雖然我們可以定義看起來像是獨立自由的函數,但是實際上這時候,這些含糊被隱式地被創建為window object的屬性properties(在node環境中是其他的object,可能是process)。這也就意味着,即使我們創建一個自由獨立的function而沒有顯然指定其context,我們也可以以window屬性的方式來調用該函數。

// Define the free-floating function.
function someMethod(){ ... }

// Access it as a property of Window.
window.someMethod();

既然我們知道了js的所有函數度作為一個對象的屬性而存在,我們可以具體探討下執行上下文的默認綁定這個概念了。默認情況下,一個js函數在該函數所屬的對象的上下文中執行(js function is executed in the context of the object for which it is a property).也就是說,在函數body中,this關鍵詞就是對父親對象的引用,看看下面的代碼:

//  "this" keyword within the sayHello() method is a reference to the sarah object
sarah.sayHello();
// "this" keyword within the getScreenResolution() function is a reference to the window object (since unbound functions are implicitly bound to the global scope)
getScreenResolution();

以上是默認的情況,除此之外,js提供了一種變更任何一個method的執行上下文的機制: call()或者apply().這兩個函數都能用於綁定"this"關鍵字到一個明確的context(object)上去。

method.call( newThisContext, Param1, ..., Param N )
method.apply( newThisContext, [ Param1, ..., Param N ] );

再看一個復雜一點的代碼案例:

<!DOCTYPE html>
<html>
<head>
    <title>Changing Execution Context In JavaScript</title>

    <script type="text/javascript">
        // Create a global variable for context (this lives in the
        // global scope - window).
        var context = "Global (ie. window)";
        // Create an object.
        var objectA = {
            context: "Object A"
        };
        // Create another object.
        var objectB = {
            context: "Object B"
        };
        // -------------------------------------------------- //
        // -------------------------------------------------- //
        // Define a function that uses an argument AND a reference
        // to this THIS scope. We will be invoking this function
        // using a variety of approaches.
        function testContext( approach ){
            console.log( approach, "==> THIS ==>", this.context );
        }
        // -------------------------------------------------- //
        // -------------------------------------------------- //
        // Invoke the unbound method with standard invocation.
        testContext( "testContext()" );
        // Invoke it in the context of Object A using call().
        testContext.call(
            objectA,
            ".call( objectA )"
        );
        // Invoke it in the context of Object B using apply().
        testContext.apply(
            objectB,
            [ ".apply( objectB )" ]
        );
        // -------------------------------------------------- //
        // -------------------------------------------------- //
        // Now, let's set the test method as an actual property
        // of the object A.
        objectA.testContext = testContext;
        // -------------------------------------------------- //
        // -------------------------------------------------- //
        // Invoke it as a property of object A.
        objectA.testContext( "objectA.testContext()" );
        // Invoke it in the context of Object B using call.
        objectA.testContext.call(
            objectB,
            "objectA.testContext.call( objectB )"
        );
        // Invoke it in the context of Window using apply.
        objectA.testContext.apply(
            window,
            [ "objectA.testContext.apply( window )" ]
        );
    </script>
</head>
<body>
    <!-- Left intentionally blank. -->
</body>
</html>

 以上代碼的輸出如下:

testContext() ==> THIS ==> Global (ie. window)
.call( objectA ) ==> THIS ==> Object A
.apply( objectB ) ==> THIS ==> Object B
objectA.testContext() ==> THIS ==> Object A
objectA.testContext.call( objectB ) ==> THIS ==> Object B
objectA.testContext.apply( window ) ==> THIS ==> Global (ie. window)

 

為什么this是undefined?

(function () {
    "use strict";

    this.foo = "bar"; // *this* is undefined, why?
}());
function myConstructor() {
    this.a = 'foo';
    this.b = 'bar';
}

myInstance     = new myConstructor(); // all cool, all fine. a and b were created in a new local object
// 如果是strict mode, 則顯示 "TypeError: this is undefined"
myBadInstance  = myConstructor(); // oh my gosh, we just created a, and b on the window object

在js中有一種所謂沙盒模型"boxing" mechanism. 這個盒子在進入被調用函數執行上下文之前將包裹或者變更this object.在匿名函數中,由於在strict mode下並未以obj.method方式來調用匿名函數,因此this就為undefined(原因是匿名函數就是一個閉包,其作用就是隔離了global scope,因此不會默認到window.method上去).而在非strict mode下則this指向window.

function Request(destination, stay_open) {
    this.state = "ready";
    this.xhr = null;
    this.destination = destination;
    this.stay_open = stay_open;

    this.open = function(data) {
        this.xhr = $.ajax({
            url: destination,
            success: this.handle_response,
            error: this.handle_failure,
            timeout: 100000000,
            data: data,
            dataType: 'json',
        });
    };

    /* snip... */

}

Request.prototype.start = function() {
    if( this.stay_open == true ) {
        this.open({msg: 'listen'});
    } else {

    }
};
var o = new Request(destination, stay_open);
o.start()

this object is not set based on declaration, but by invocation. What it means is that if you assign the function property to a variable like x = o.start and call x()this inside start no longer refers to o

var o = new Request(...);
 setTimeout(function() { o.start(); }, 1000);

 

另一個角度來看this-functionObj.this

首先,需要知道的是js中的所有函數都有peroperties,就像objects都有properties一樣,因為function本身也是一個object對象。this可以看作是this-functionObj的屬性,並且只有當該函數執行時,該函數對象的this屬性將會被賦值,"it gets the this property ---- a variable with the value of the object that invokes the function where this is used"

this總是指向或者說引用(並包含了對應的value)一個對象,並且this往往在一個function或者說method中來使用。注意:雖然在global scope中我們可以不在function body中使用this,而是直接在global scope中使用this(實際上指向了window),但是如果我們在strict mode的話,在global function中,或者說沒有綁定任何object的匿名函數中,如果使用this, 那么這個this將是undefined值.

假設this在一個function A中被使用,那么this就將引用着調用 function A的那個對象。我們需要這個this來訪問調用function A對象的method和property.特別地,有些情況下我們不知道調用者對象的名稱,甚至有時候調用者對象根本沒有名字,這時就必須用this關鍵字了!

    var person = {
    firstName   :"Penelope",
    lastName    :"Barrymore",
    // Since the "this" keyword is used inside the showFullName method below, and the showFullName method is defined on the person object,
    // "this" will have the value of the person object because the person object will invoke showFullName ()
    showFullName:function () {
    console.log (this.firstName + " " + this.lastName);
    }

    }
    person.showFullName (); // Penelope Barrymore

再看一個jquery事件處理函數中使用this關鍵字的常見例子:

    // A very common piece of jQuery code

    $ ("button").click (function (event) {
    // $(this) will have the value of the button ($("button")) object
// because the button object invokes the click () method, this指向button
    console.log ($ (this).prop ("name"));
    });

 The use of $(this), which is jQuery’s syntax for the this keyword in JavaScript, is used inside an anonymous function, and the anonymous function is executed in the button’s click () method. The reason $(this) is bound to the button object is because the jQuery library binds$(this) to the object that invokes the click method. Therefore, $(this) will have the value of the jQuery button ($(“button”)) object, even though $(this) is defined inside an anonymous function that cannot itself access the “this” variable on the outer function.

深入一步理解this

我們先拋出一個心法: this不會有value,直到一個object invoke了這個函數(this在這個函數中使用).為了行文方便,我們將使用this關鍵字的函數稱為thisFunction.

雖然默認情況下,this都會引用定義了this(也就是有this引用)的對象,但是只到一個對象調用了thisFunction,這個this指針才會被賦值。而這個this value只決定於調用了thisFunction的對象。盡管默認情況下this的值就是invoking ojbect(xxObj.thisFunction),但是我們也可以通過xxObj.thisFunction.call(yyObj,parameters), apply()等方式來修改this的默認值!~

在global scope中使用this

在global scope中,當代碼在瀏覽器中執行時,所有的全局variable和function都被定義在window object上,因此,在一個全局函數中當使用this時,this是引用了全局的window對象的(注意必須是非stric mode哦),而window對象則是整個js應用或者說web page的容器

最讓人頭疼和誤解的this使用場景

有以下幾個場景,this會變得非常易於令人誤解:

1.當我們借用一個使用了this的方法method;

2.當我們將使用了this的method給到一個變量時;

3.當一個使用了this的函數被作為回調函數參數時;

4.當在一個閉包closure里面的函數中使用this時

下面我們將一一探討這些情況下this的正確取值是什么

繼續下文前,再聊一聊"context"

javascript中context的概念和自然語言中的主語有類似性。“John is the winner who returned the money”.本句的主語是John, 我們可以說本劇的context上下文就是John,因為本句此時的焦點就在他身上,甚至who這個代詞指代的也是前面的這個主語John.正如我們可以通過使用一個分號   "  ; " 來更改句子的主語一樣,我們也可以通過使用另外一個object來調用這個function,從而改變context.

var person = {
   firstName   :"Penelope",
   lastName    :"Barrymore",
   showFullName:function () {
// The "context"
console.log (this.firstName + " " + this.lastName);
 }
}

// The "context", when invoking showFullName, is the person object, when we invoke the showFullName () method on the person object.
// And the use of "this" inside the showFullName() method has the value of the person object,
person.showFullName (); // Penelope Barrymore

// If we invoke showFullName with a different object:
var anotherPerson = {
firstName   :"Rohit",
lastName    :"Khan"
};

// We can use the apply method to set the "this" value explicitly—more on the apply () method later.
// "this" gets the value of whichever object invokes the "this" Function, hence:
person.showFullName.apply (anotherPerson); // Rohit Khan

// So the context is now anotherPerson because anotherPerson invoked the person.showFullName ()  method by virtue of using the apply () method

1. 當方法作為callback方式的傳入時,如何fix住里面的this值

    // We have a simple object with a clickHandler method that we want to use when a button on the page is clicked
    var user = {
    data:[
    {name:"T. Woods", age:37},
    {name:"P. Mickelson", age:43}
    ],
    clickHandler:function (event) {
    var randomNum = ((Math.random () * 2 | 0) + 1) - 1; // random number between 0 and 1

    // This line is printing a random person's name and age from the data array
    console.log (this.data[randomNum].name + " " + this.data[randomNum].age);
    }
    }

    // The button is wrapped inside a jQuery $ wrapper, so it is now a jQuery object
    // And the output will be undefined because there is no data property on the button object
    $ ("button").click (user.clickHandler); // Cannot read property '0' of undefined

在上面的代碼中,($('button'))自己是一個對象,我們將user.clickHandler method方法作為callback函數參數傳入該jquery對象的click()方法中,我們知道user.clickHandler()中的this不再指向user對象了。this將指向user.clickMethod運行地所在的對象--因為this在user.clickHandler方法中定義。而invoking這個user.Handler方法的對象則是button object,---user.clickHandler將在button對象的click方法中被執行。

需要說明的是即使我們通過user.clickHandler()方式來調用(實際上我們也必須這么做,因為clickHandler本身就作為user的一個method來定義的,因此必須這么去調用), clickHandler()方法也將以button對象作為上下文去執行,也就是說this現在將指向這個button context對象($('button')).

到這里,我們可以下一個結論:

At this point, it should be apparent that when the context changes—when we execute a method on some other object than where the object was originally defined, the this keyword no longer refers to the original object where “this” was originally defined, but it now refers to the object that invokes the method where this was defined.

如何能解決這類問題,並且fix住this指向呢?

在上面的例子中,既然我們總是希望this.data就是指向到user object的data屬性,我們可以使用Bind(),Apply()或者Call()方法去特別設定this的value.

 

 $ ("button").click (user.clickHandler); // 這個clickHandler this指向button jquery對象
$("button").click (user.clickHandler.bind (user)); // P. Mickelson 43 通過bind指定這個clickHandler中的this就是指user

 

2. 如何在一個閉包的inner function中(或匿名函數)fix住this的值

正如上面提及,當我們使用一個inner method(a closure)時,this也是非常容易搞混淆的。非常重要一點是:closures閉包不能訪問外部函數(outer function)的this值,因為this變量只能由函數本身來訪問,而不是inner function(的this)

var user = {
    tournament:"The Masters",
    data      :[
    {name:"T. Woods", age:37},
    {name:"P. Mickelson", age:43}
    ],

    clickHandler:function () {
    // the use of this.data here is fine, because "this" refers to the user object, and data is a property on the user object.

    this.data.forEach (function (person) {
    // But here inside the anonymous function (that we pass to the forEach method), "this" no longer refers to the user object.
    // This inner function cannot access the outer function's "this"
   
    console.log ("What is This referring to? " + this); //[object Window]
 
    console.log (person.name + " is playing at " + this.tournament);
    // T. Woods is playing at undefined
    // P. Mickelson is playing at undefined
    })
    }

    }
    user.clickHandler(); // What is "this" referring to? [object Window]

上面代碼中,由於匿名函數中的this不能訪問外部函數的this,因此當在非strict mode時,this將綁定到global window對象。

同樣地,也可以對匿名函數調用bind來fix住this

var object = {

  property: function() {

    this.id = 'abc'; // 'this' binds to the object

    aFunctionWithCallback(this.id, function(data) {
      console.log(this); // null
    });
aFunctionWithCallbackOK(this.id, function(data) {
  console.log(this);
}.bind(this));
  }
};

 

維護在匿名函數中使用的this值方案:

var user = {
    tournament:"The Masters",
    data      :[
    {name:"T. Woods", age:37},
    {name:"P. Mickelson", age:43}
    ],
    clickHandler:function (event) {
    // To capture the value of "this" when it refers to the user object, we have to set it to another variable here:
    // We set the value of "this" to theUserObj variable, so we can use it later
    var that = theUserObj = this;
    this.data.forEach (function (person) {
    // Instead of using this.tournament, we now use theUserObj.tournament
    console.log (person.name + " is playing at " + theUserObj.tournament);
    })
    }
    }
    user.clickHandler();
    // T. Woods is playing at The Masters
    //  P. Mickelson is playing at The Masters

3. 當method被賦值給一個變量時,如何fix住this指向

// This data variable is a global variable
    var data = [
    {name:"Samantha", age:12},
    {name:"Alexis", age:14}
    ];

    var user = {
    // this data variable is a property on the user object
    data    :[
    {name:"T. Woods", age:37},
    {name:"P. Mickelson", age:43}
    ],
    showData:function (event) {
    var randomNum = ((Math.random () * 2 | 0) + 1) - 1; // random number between 0 and 1

    // This line is adding a random person from the data array to the text field
    console.log (this.data[randomNum].name + " " + this.data[randomNum].age);
    }

    }

    // Assign the user.showData to a variable
    var showUserData = user.showData;

    // When we execute the showUserData function, the values printed to the console are from the global data array, not from the data array in the user object
    //
    showUserData (); // Samantha 12 (from the global data array)

解決方案是特別地通過使用bind方法來指定this值

 // Bind the showData method to the user object
    var showUserData = user.showData.bind (user);

    // Now we get the value from the user object, because the this keyword is bound to the user object
    showUserData (); // P. Mickelson 43

4. 當借用一個定義了this的method方法時

在js開發中,借用方法是一個非常常見的實例,作為js開發者,我們一定會經常遇到。

// We have two objects. One of them has a method called avg () that the other doesn't have
    // So we will borrow the (avg()) method
    var gameController = {
    scores  :[20, 34, 55, 46, 77],
    avgScore:null,
    players :[
    {name:"Tommy", playerID:987, age:23},
    {name:"Pau", playerID:87, age:33}
    ]
    }

    var appController = {
    scores  :[900, 845, 809, 950],
    avgScore:null,
    avg     :function () {

    var sumOfScores = this.scores.reduce (function (prev, cur, index, array) {
    return prev + cur;
    });

    this.avgScore = sumOfScores / this.scores.length;
    }
    }
    //If we run the code below,
    // the gameController.avgScore property will be set to the average score from the appController object "scores" array
    // Don't run this code, for it is just for illustration; we want the appController.avgScore to remain null
    gameController.avgScore = appController.avg();

avg方法的"this"不再指向gameController object,它將指向到appController對象,因為該avg()方法是在appController對象上執行的。

解決方法:

// Note that we are using the apply () method, so the 2nd argument has to be an array—the arguments to pass to the appController.avg () method.
    appController.avg.apply (gameController, gameController.scores);

    // The avgScore property was successfully set on the gameController object, even though we borrowed the avg () method from the appController object
    console.log (gameController.avgScore); // 46.4

    // appController.avgScore is still null; it was not updated, only gameController.avgScore was updated
    console.log (appController.avgScore); // null

gameController對象借用了appController's avg()方法,在appController.avg()中的this value會被設定為gameContrller對象,因為我們使用了apply()方法。

最后,需要牢記:

Always remember that this is assigned the value of the object that invoked the this Function

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM