彻底理解javascript中的this指针
http://javascriptissexy.com/understand-javascripts-this-with-clarity-and-master-it/
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