前面兩篇文章介紹了JavaScript執行上下文中兩個重要屬性:VO/AO和scope chain。本文就來看看執行上下文中的this。
首先看看下面兩個對this的概括:
- this是執行上下文(Execution Context)的一個重要屬性,是一個與執行上下文相關的特殊對象。因此,它可以叫作上下文對象(也就是用來指明執行上下文是在哪個上下文中被觸發的對象)。
- this不是變量對象(Variable Object)的一個屬性,所以跟變量不同,this從不會參與到標識符解析過程。也就是說,在代碼中當訪問this的時候,它的值是直接從執行上下文中獲取的,並不需要任何作用域鏈查找。this的值只在進入上下文的時候進行一次確定。
關於this最困惑的應該是,同一個函數,當在不同的上下文進行調用的時候,this的值就可能會不同。也就是說,this的值是通過函數調用表達式(也就是函數被調用的方式)的caller所提供的。
下面就看看在不同場景中,this的值。
全局上下文
在全局上下文(Global Context)中,this總是global object,在瀏覽器中就是window對象。
console.log(this === window); this.name = "Will"; this.age = 28; this.getInfo = function(){ console.log(this.name + " is " + this.age + " years old"); }; window.getInfo(); // true // Will is 28 years old
函數上下文
在一個函數中,this的情況就比較多了,this的值直接受函數調用方式的影響。
Invoke function as Function
當通過正常的方式調用一個函數的時候,this的值就會被設置為global object(瀏覽器中的window對象)。
但是,當使用"strict mode"執行下面代碼的時候,this就會被設置為"undefined"。
function gFunc(){ return this; } console.log(gFunc()); console.log(this === window.gFunc()); // window // true
Invoke function as Method
當函數作為一個對象方法來執行的時候,this的值就是該方法所屬的對象。
在下面的例子中,創建了一個obj對象,並設置name屬性的值為"obj"。所以但調用該對象的func方法的時候,方法中的this就表示obj這個對象。
var obj = { name: "obj", func: function () { console.log(this + ":" + this.name); } }; obj.func(); // [object Object]:obj
為了驗證"方法中的this代表方法所屬的對象"這句話,再看下面一個例子。
在對象obj中,創建了一個內嵌對象nestedObj,當調用內嵌對象的方法的時候,方法中的this就代表nestedObj。
var obj = { name: "obj", nestedObj: { name:"nestedObj", func: function () { console.log(this + ":" + this.name); } } } obj.nestedObj.func(); // [object Object]:nestedObj
對於上面例子中的方法,通常稱為綁定方法,也就是說這些方法都是個特定的對象關聯的。
但是,當我們進行下面操作的時候,temp將是一個全局作用里面的函數,並沒有綁定到obj對象上。所以,temp中的this表示的是window對象。
var name = "Will"; var obj = { name: "obj", func: function () { console.log(this + ":" + this.name); } }; temp = obj.func; temp(); // [object Window]:Will
Invoke function as Constructor
在JavaScript中,函數可以作為構造器來直接創建對象,在這種情況下,this就代表了新建的對象。
function Staff(name, age){ this.name = name; this.age = age; this.getInfo = function(){ console.log(this.name + " is " + this.age + " years old"); }; } var will = new Staff("Will", 28); will.getInfo(); // Will is 28 years old
Invoke context-less function
對於有些沒有上下文的函數,也就是說這些函數沒有綁定到特定的對象上,那么這些上下文無關的函數將會被默認的綁定到global object上。
在這個例子中,函數f和匿名函數表達式在被調用的過程中並沒有被關聯到任何對象,所以他們的this都代表global object。
var context = "global"; var obj = { context: "object", method: function () { console.log(this + ":" +this.context); function f() { var context = "function"; console.log(this + ":" +this.context); }; f(); (function(){ var context = "function"; console.log(this + ":" +this.context); })(); } }; obj.method(); // [object Object]:object // [object Window]:global // [object Window]:global
call/apply/bind改變this
this本身是不可變的,但是 JavaScript中提供了call/apply/bind三個函數來在函數調用時設置this的值。
這三個函數的原型如下:
-
fun.apply(obj1 [, argsArray])
- Sets obj1 as the value of this inside fun() and calls fun() passing elements of argsArray as its arguments.
-
fun.call(obj1 [, arg1 [, arg2 [,arg3 [, ...]]]])
- Sets obj1 as the value of this inside fun() and calls fun() passing arg1, arg2, arg3, ... as its arguments.
-
fun.bind(obj1 [, arg1 [, arg2 [,arg3 [, ...]]]])
- Returns the reference to the function fun with this inside fun() bound to obj1 and parameters of fun bound to the parameters specified arg1, arg2, arg3, ....
下面看一個簡單的例子:
function add(numA, numB){ console.log( this.original + numA + numB); } add(1, 2); var obj = {original: 10}; add.apply(obj, [1, 2]); add.call(obj, 1, 2); var f1 = add.bind(obj); f1(2, 3); var f2 = add.bind(obj, 2); f2(3); // NaN // 13 // 13 // 15 // 15
當直接調用add函數的時候,this將代表window,當執行"this.original"的時候,由於window對象並沒有"original"屬性,所以會得到"undefined"。
通過call/apply/bind,達到的效果就是把add函數綁定到了obj對象上,當調用add的時候,this就代表了obj這個對象。
DOM event handler
當一個函數被當作event handler的時候,this會被設置為觸發事件的頁面元素(element)。
var body = document.getElementsByTagName("body")[0]; body.addEventListener("click", function(){ console.log(this); }); // <body>…</body>
In-line event handler
當代碼通過in-line handler執行的時候,this同樣指向擁有該handler的頁面元素。
看下面的代碼:
document.write('<button onclick="console.log(this)">Show this</button>'); // <button onclick="console.log(this)">Show this</button> document.write('<button onclick="(function(){console.log(this);})()">Show this</button>'); // window
在第一行代碼中,正如上面in-line handler所描述的,this將指向"button"這個element。但是,對於第二行代碼中的匿名函數,是一個上下文無關(context-less)的函數,所以this會被默認的設置為window。
前面我們已經介紹過了bind函數,所以,通過下面的修改就能改變上面例子中第二行代碼的行為:
document.write('<button onclick="((function(){console.log(this);}).bind(this))()">Show this</button>'); // <button onclick="((function(){console.log(this);}).bind(this))()">Show this</button>
保存this
在JavaScript代碼中,同一個上下文中可能會出現多個this,為了使用外層的this,就需要對this進行暫存了。
看下面的例子,根據前面的介紹,在body元素的click handler中,this肯定是指向body這個元素,所以為了使用"greeting"這個方法,就是要對指向bar對象的this進行暫存,這里用了一個self變量。
有了self,我們就可以在click handler中使用bar對象的"greeting"方法了。
當閱讀一些JavaScript庫代碼的時候,如果遇到類似self,me,that的時候,他們可能就是對this的暫存。
var bar = { name: "bar", body: document.getElementsByTagName("body")[0], greeting: function(){ console.log("Hi there, I'm " + this + ":" + this.name); }, anotherMethod: function () { var self = this; this.body.addEventListener("click", function(){ self.greeting(); }); } }; bar.anotherMethod(); // Hi there, I'm [object Object]:bar
同樣,對於上面的例子,也可以使用bind來設置this達到相同的效果。
var bar = { name: "bar", body: document.getElementsByTagName("body")[0], greeting: function(){ console.log("Hi there, I'm " + this + ":" + this.name); }, anotherMethod: function () { this.body.addEventListener("click", (function(){ this.greeting(); }).bind(this)); } }; bar.anotherMethod(); // Hi there, I'm [object Object]:bar
總結
本文介紹了執行上下文中的this屬性,this的值直接影響着代碼的運行結果。
在函數調用中,this是由激活上下文代碼的調用者(caller)來提供的,即調用函數的父上下文(parent context ),也就是說this取決於調用函數的方式,指向調用時所在函數所綁定的對象。
通過上面的介紹,相信對this有了一定的認識。