JavaScript中的this是讓很多開發者頭疼的地方,而this關鍵字又是一個非常重要的語法點。毫不夸張地說,不理解它的含義,大部分開發任務都無法完成。
想要理解this,你可以先記住以下兩點:
1:this永遠指向一個對象;
2:this的指向完全取決于函數調用的位置;
針對以上的第一點特別好理解,不管在什么地方使用this,它必然會指向某個對象;確定了第一點后,也引出了一個問題,就是this使用的地方到底在哪里,而第二點就解釋了這個問題,但關鍵是在JavaScript語言之中,一切皆對象,運行環境也是對象,所以函數都是在某個對象下運行,而this就是函數運行時所在的對象(環境)。這本來并不會讓我們糊涂,但是JavaScript支持運行環境動態切換,也就是說,this的指向是動態的,很難事先確定到底指向哪個對象,這才是最讓我們感到困惑的地方。
先看原理
function fun(){
console.log(this.s);
}?
var obj={
s:'1',
f:fun
}
?var s='2';
?obj.f(); //1
fun(); //2
上述代碼中,fun函數被調用了兩次,顯而易見的是兩次的結果不一樣;
很多人都會這樣解釋,obj.f()的調用中,因為運行環境在obj對象內,因此函數中的this指向對象obj;
而在全局作用域下調用 fun() ,函數中的 this 就會指向全局作用域對象window;
但是大部分人不會告訴你,this的指向為什么會發生改變,this指向的改變到底是什么時候發生的;
而搞懂了這些,this的使用才不會出現意外;
首先我們應該知道,在JS中,數組、函數、對象都是引用類型,在參數傳遞時也就是引用傳遞;
上面的代碼中,obj 對象有兩個屬性,但是屬性的值類型是不同的,在內存中的表現形式也是不同的;
調用時就成了這個樣子:
因為函數在js中既可以當做值傳遞和返回,也可當做對象和構造函數,所有函數在運行時需要確定其當前的運行環境,this就出生了,所以,this會根據運行環境的改變而改變,同時,函數中的this也只能在運行時才能最終確定運行環境;
再來看下面的代碼,你可能會更加理解this對于運行環境的動態切換規則:
var A={
name: '張三',
f: function () {
console.log('姓名:' + this.name);
}
};?
var B={
name: '李四'
};?
B.f=A.f;
B.f() // 姓名:李四
A.f() // 姓名:張三
上面代碼中,A.f屬性被賦給B.f,也就是A對象將匿名函數的 地址 賦值給B對象;
那么在調用時,函數分別根據運行環境的不同,指向對象A和B
function foo() {
console.log(this.a);
}
var obj2={
a: 2,
fn: foo
};
var obj1={
a: 1,
o1: obj2
};
obj1.o1.fn(); // 2
obj1對象的o1屬性值是obj2對象的地址,而obj2對象的fn屬性的值是函數foo的地址;
函數foo的調用環境是在obj2中的,因此this指向對象obj2;
那么接下來,我們對this使用最頻繁的幾種情況做一個總結,最常見的基本就是以下5種:
對象中的方法,事件綁定 ,構造函數 ,定時器,函數對象的call()、apply() 方法;
上面在講解this原理是,我們使用對象的方法中的this來說明的,在此就不重復講解了,不懂的同學們,請返回去重新閱讀;
事件綁定中的this
事件綁定共有三種方式:行內綁定、動態綁定、事件監聽;
行內綁定的兩種情況:
?<input type="button" value="按鈕" onclick="clickFun()">
<script>
function clickFun(){
this // 此函數的運行環境在全局window對象下,因此this指向window;
}
</script>
?
<input type="button" value="按鈕" onclick="this">
<!-- 運行環境在節點對象中,因此this指向本節點對象 -->
行內綁定事件的語法是在html節點內,以節點屬性的方式綁定,屬性名是事件名稱前面加'on',屬性的值則是一段可執行的 JS 代碼段;而屬性值最常見的就是一個函數調用;
當事件觸發時,屬性值就會作為JS代碼被執行,當前運行環境下沒有clickFun函數,因此瀏覽器就需要跳出當前運行環境,在整個環境中尋找一個叫clickFun的函數并執行這個函數,所以函數內部的this就指向了全局對象window;如果不是一個函數調用,直接在當前節點對象環境下使用this,那么顯然this就會指向當前節點對象;
動態綁定與事件監聽:
<input type="button" value="按鈕" id="btn">
<script>
var btn=document.getElementById('btn');
btn.onclick=function(){
this ; // this指向本節點對象
}
</script>
因為動態綁定的事件本就是為節點對象的屬性(事件名稱前面加'on')重新賦值為一個匿名函數,因此函數在執行時就是在節點對象的環境下,this自然就指向了本節點對象;
事件監聽中this指向的原理與動態綁定基本一致,所以不再闡述;
構造函數中的this
function Pro(){
this.x='1';
this.y=function(){};
}
var p=new Pro();
?
對于接觸過 JS 面向對象編程的同學來說,上面的代碼和圖示基本都能看懂,new 一個構造函數并執行函數內部代碼的過程就是這個五個步驟,當 JS 引擎指向到第3步的時候,會強制的將this指向新創建出來的這個對象;基本不需要理解,因為這本就是 JS 中的語法規則,記住就可以了;
window定時器中的this
var obj={
fun:function(){
this ;
}
}
?
setInterval(obj.fun,1000); // this指向window對象
setInterval('obj.fun()',1000); // this指向obj對象
setInterval() 是window對象下內置的一個方法,接受兩個參數,第一個參數允許是一個函數或者是一段可執行的 JS 代碼,第二個參數則是執行前面函數或者代碼的時間間隔;
在上面的代碼中,setInterval(obj.fun,1000) 的第一個參數是obj對象的fun ,因為 JS 中函數可以被當做值來做引用傳遞,實際就是將這個函數的地址當做參數傳遞給了 setInterval 方法,換句話說就是 setInterval 的第一參數接受了一個函數,那么此時1000毫秒后,函數的運行就已經是在window對象下了,也就是函數的調用者已經變成了window對象,所以其中的this則指向的全局window對象;
而在 setInterval('obj.fun()',1000) 中的第一個參數,實際則是傳入的一段可執行的 JS 代碼;1000毫秒后當 JS 引擎來執行這段代碼時,則是通過 obj 對象來找到 fun 函數并調用執行,那么函數的運行環境依然在 對象 obj 內,所以函數內部的this也就指向了 obj 對象;
函數對象的call()、apply() 方法
函數作為對象提供了call(),apply() 方法,他們也可以用來調用函數,這兩個方法都接受一個對象作為參數,用來指定本次調用時函數中this的指向;
call()方法
call方法使用的語法規則
函數名稱.call(obj,arg1,arg2...argN);
參數說明:
obj:函數內this要指向的對象,
arg1,arg2...argN :參數列表,參數與參數之間使用一個逗號隔開
var lisi={names:'lisi'};
var zs={names:'zhangsan'};
function f(age){
console.log(this.names);
console.log(age);
}
f(23);//undefined
?
//將f函數中的this指向固定到對象zs上;
f.call(zs,32);//zhangsan
apply()方法
函數名稱.apply(obj,[arg1,arg2...,argN])
參數說明:
obj :this要指向的對象
[arg1,arg2...argN] : 參數列表,要求格式為數組
var lisi={name:'lisi'}; var zs={name:'zhangsan'}; function f(age,sex){ console.log(this.name+age+sex); }//將f函數中的this指向固定到對象zs上;f.apply(zs,[23,'nan']);
注意:call和apply的作用一致,區別僅僅在函數實參參數傳遞的方式上;這個兩個方法的最大作用基本就是用來強制指定函數調用時this的指向。
下來,筆者將按照以下目錄對this進行闡述:
this是JavaScript的一個關鍵字,但它時常蒙著面紗讓人無法捉摸,許多對this不明就里的同學,常常會有這樣的錯誤認知:
this的指向取決于他所處的環境. 大致上,可以分為下面的6種情況:
this在全局范圍內綁定什么呢?這個相信只要學過JS,應該都知道答案。如果不知道,同學真的應該反思自己的學習態度和方法是否存在問題了。話不多說,直接上代碼,一探究竟,揭開this在全局范圍下的真面目:
console.log(this); // Window
不出意外,this在全局范圍內指向window對象()。通常, 在全局環境中, 我們很少使用this關鍵字, 因此對它也沒那么在意. 讓我們繼續看下一個環境.
當我們使用new創建構造函數的實例時會發生什么呢?以這種方式調用構造函數會經歷以下四個步驟:
看完上面的內容,大家想必也知道this在對象的構造函數內的指向了吧!當你使用new關鍵字創建一個對象的新的實例時, this關鍵字指向這個實例 .
舉個栗子:
function Human (age) {
this.age=age;
}
let greg=new Human(22);
let thomas=new Human(24);
console.log(greg); // this.age=22
console.log(thomas); // this.age=24
// answer
Person { age:22}
Person { age:24}
方法是與對象關聯的函數的通俗叫法, 如下所示:
let o={
sayThis(){
console.log(this);
}
}
如上所示,在對象的任何方法內的this都是指向對象本身 .
好了,繼續下一個環境!
可能看到這里,許多同學心里會有疑問,什么是簡單函數?
其實簡單函數大家都很熟悉,就像下面一樣,以相同形式編寫的匿名函數也被認為是簡單函數(非箭頭函數)。
function hello(){
console.log("hello"+this);
}
這里需要注意,在瀏覽器中,不管函數聲明在哪里,匿名或者不匿名,只要不是直接作為對象的方法,this指向始終是window對象(除非使用call,apply,bind修改this指向)。
舉個栗子說明一下:
// 顯示函數,直接定義在sayThis方法內,this指向依舊不變
function simpleFunction() {
console.log(this);
}
var o={
sayThis() {
simpleFunction();
}
}
simpleFunction(); // Window
o.sayThis(); // Window
// 匿名函數
var o={
sayThis(){
(function(){consoloe.log(this);})();
}
}
o.sayThis();// Window
對于初學者來說,this在簡單函數內的表現時常讓他們懵逼不已,難道this不應該指向對象本身?這個問題曾經也出現在我的腦海里過,沒錯,在寫代碼時我也踩過這個坑。
通常的,當我們要在對象方法內調用函數,而這個函數需要用到this時,我們都會創建一個變量來保存對象中的this的引用. 通常, 這個變量名稱叫做self或者that。具體說下所示:
const o={
doSomethingLater() {
const self=this;
setTimeout(function() {
self.speakLeet();
}, 1000);
},
speakLeet() {
console.log(`1337 15 4W350M3`);
}
}
o.doSomethingLater(); // `1337 15 4W350M3`
心細的同學可能已經發現,這里的簡單函數沒有將箭頭函數包括在內,那么下一個環境是什么想必也能猜到啦,那么現在進入下一個環境,看看this指向什么。
和簡單函數表現不太一樣,this在箭頭函數中總是跟它在箭頭函數所在作用域的this一樣(在它直接作用域). 所以, 如果你在對象中使用箭頭函數, 箭頭函數中的this總是指向這個對象本身, 而不是指向Window.
下面我們使用箭頭函數,重寫一下上面的案例:
const o={
doSomethingLater() {
setTimeout(()=> this.speakLeet(), 1000);
},
speakLeet() {
console.log(`1337 15 4W350M3`);
}
}
o.doSomethingLater(); // `1337 15 4W350M3`
最后,讓我們來看看最后一種環境 - 事件偵聽器.
在事件偵聽器內, this被綁定的是觸發這個事件的元素:
let button=document.querySelector('button');
button.addEventListener('click', function() {
console.log(this); // button
});
事實上,只要記住上面this在不同環境的綁定值,足以應付大部分工作。然而,好學的同學總是會忍不住想說,為什么呢?對,為什么this在這些情況下綁定這些值呢?學習,我們不能只知其然,而不知所以然。所以,現在就讓我們來探尋,this值獲取的真相吧。
現在,讓我們回憶一下,在講什么是this的時候,我們說到“this的綁定取決于他所處的環境”。這句話其實不是十分準確,準確的說,this不是編寫時綁定,而是運行時綁定。它依賴于函數調用的上下文條件。this綁定和函數聲明的位置無關,反而和函數被調用的方式有關。
當一個函數被調用時,會建立一個活動記錄,也稱為執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是 如何被調用的,被傳遞了什么參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的this引用。this實際上是在函數被調用時建立的一個綁定,它指向什么是完全由函數被調用的調用點來決定的。
現在我們將注意力轉移到調用點 如何 決定在函數執行期間this指向哪里。
你必須考察call-site并判定4種規則中的哪一個適用。我們將首先獨立的解釋一下這4種規則中的每一種,之后我們來展示一下如果有多種規則可以適用調用點時,它們的優先級。
第一種規則來源于函數調用的最常見的情況:獨立函數調用。可以認為這種this規則是在沒有其他規則適用時的默認規則。我們給它一個稱呼“默認綁定”.
現在來看這段代碼:
function foo(){
console.log(this);
}
var a=2;
demo(); // 2
當foo()被調用時,this.a解析為我們的全局變量a。為什么?因為在這種情況下,對此方法調用的this實施了 默認綁定,所以使this指向了全局對象。
在我們的代碼段中,foo()是被一個直白的,毫無修飾的函數引用調用的。沒有其他的我們將要展示的規則適用于這里,所以 默認綁定 在這里適用。
如果strict mode在這里生效,那么對于 默認綁定 來說全局對象是不合法的,所以this將被設置為undefined。
'use strict'
function foo(){
console.log(this.a); // TypeError: Cannot read property 'a' of undefined
}
const a=1;
foo();
function foo(){
'use strict'
console.log(this.a); // TypeError: Cannot read property 'a' of undefined
}
const a=1;
foo();
微妙的是,即便所有的this綁定規則都是完全基于調用點,如果foo()的 內容 沒有在strint mode下執行,對于 默認綁定 來說全局對象是 唯一 合法的;foo()的call-site的strict mode狀態與此無關。
function foo(){
console.log(this.a);
}
var a=1;
(function(){
'use strict';
foo(); // 1
})();
注意: 在代碼中故意混用strict mode和非strict mode通常是讓人皺眉頭的。你的程序整體可能應當不是 Strict 就是非Strict。然而,有時你可能會引用與你的 Strict 模式不同的第三方包,所以對這些微妙的兼容性細節要多加小心。
另一種要考慮的規則是:調用點是否有一個環境對象(context object),也稱為擁有者(owning)或容器(containing)對象。
讓我們來看這段代碼:
function foo() {
console.log(this.a);
}
let o={
a: 2,
foo,
}
o.foo(); // 2
這里,我們注意到foo函數被聲明然后作為對象o的方法,無論foo()是否一開始就在obj上被聲明,還是后來作為引用添加(如上面代碼所示),都是這個 函數 被obj所“擁有”或“包含”。這里,調用點使用obj環境來引用函數,所以可以說 obj對象在函數被調用的時間點上“擁有”或“包含”這個 函數引用。
當一個方法引用存在一個環境對象時,隱式綁定 規則會說:是這個對象應當被用于這個函數調用的this綁定。
只有對象屬性引用鏈的最后一層是影響調用點的。比如:
function foo(){
console.log(this.a);
}
var obj1={
a:2,
obj2:obj2
};
var obj2={
a:42,
foo:foo
};
obj1.obj2.foo(); // 42
隱式綁定的隱患
當一個 隱含綁定丟失了它的綁定,這通常意味著它會退回到 默認綁定, 根據strict mode的狀態,結果不是全局對象就是undefined。
下面來看這段代碼:
function foo(){
console.log(this.a);
}
var obj={
a:2,
foo
};
var bar=obj.foo;
var a="Global variable";
bar(); // "Global variable"
盡管bar似乎是obj.foo的引用,但實際上它只是另一個foo自己的引用而已。另外,起作用的調用點是bar(),一個直白,毫無修飾的調用,因此 默認綁定 適用于這里。
這種情況發生的更加微妙,更常見,更意外的方式,是當我們考慮傳遞一個回調函數時:
function foo(){
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj={
a:2,
foo,
};
var a="Global variable";
dooFoo(obj.foo); // "Global variable"
參數傳遞僅僅是一種隱含的賦值,而且因為我們在傳遞一個函數,它是一個隱含的引用賦值,所以最終結果和我們前一個代碼段一樣。同樣的,語言內建,如setTimeout也一樣,如下所示
function foo(){
console.log(this.a);
}
var obj={
a:2,
foo,
};
var a="Global variable";
setTimeout(obj.foo, 100); // "Global variable"
把這個粗糙的setTimeout()假想實現當做JavaScript環境內建的實現的話:
function setTimeout(fn, delay){
// 等待delay毫秒
fn();
}
正如我們看到的, 隱含綁定丟失了它的綁定是十分常見的,不管哪一種意外改變this的方式,你都不能真正地控制你的回調函數引用將如何被執行,所以你(還)沒有辦法控制調用點給你一個故意的綁定。但是我們可以使用顯示綁定強行固定this。
我們看到隱含綁定,需要我們不得不改變目標對象使它自身包含一個對函數的引用,而后使用這個函數引用屬性來間接地(隱含地)將this綁定到這個對象上。
但是,如果你想強制一個函數調用使用某個特定對象作為this綁定,而不在這個對象上放置一個函數引用屬性呢?
js有提供call()、apply()方法,ES5中也提供了內置的方法 Function.prototype.bind,可以引用一個對象時進行強制綁定調用。
考慮這段代碼:
function foo(){
console.log(this.a);
}
var obj={
a:2,
};
foo.call(obj); // 2
通過foo.call(..)使用 明確綁定 來調用foo,允許我們強制函數的this指向obj。
如果你傳遞一個簡單原始類型值(string,boolean,或 number類型)作為this綁定,那么這個原始類型值會被包裝在它的對象類型中(分別是new String(..),new Boolean(..),或new Number(..))。這通常稱為“boxing(封箱)”。
注意: 就this綁定的角度講,call(..)和apply(..)是完全一樣的。它們確實在處理其他參數上的方式不同,但那不是我們當前關心的。
單獨依靠call和apply,仍然可能出現函數“丟失”自己原本的this綁定,或者被第三方覆蓋等問題。
但有一個技巧可以避免出現這些問題
考慮這段代碼:
function foo(){
console.log(this.a);
}
var obj={
a:2
};
var bar=function(){
foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
bar.call(window); // 2
我們創建了一個函數bar(),在它的內部手動調用foo.call(obj),由此強制this綁定到obj并調用foo。無論你過后怎樣調用函數bar,它總是手動使用obj調用foo。這種綁定即明確又堅定,該方法被開發者稱為 硬綁定(顯示綁定的變種)(hard binding)
用硬綁定將一個函數包裝起來的最典型的方法,是為所有傳入的參數和傳出的返回值創建一個通道:
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);
console.log(b); // 5
另一種表達這種模式的方法是創建一個可復用的幫助函數:
function foo(something){
console.log(this.a, something);
return this.a + something;
}
function bind(fn, obj){
return function(){
return fn.apply(obj, arguments);
};
}
var obj={ a:2};
var bar=bind(foo, obj);
var b=bar(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();
cobsole.log(b); // 5
bind(..)返回一個硬編碼的新函數,它使用你指定的this環境來調用原本的函數。
注意: 在ES6中,bind(..)生成的硬綁定函數有一個名為.name的屬性,它源自于原始的 目標函數(target function)。舉例來說:bar=foo.bind(..)應該會有一個bar.name屬性,它的值為"bound foo",這個值應當會顯示在調用棧軌跡的函數調用名稱中。
第四種也是最后一種this綁定規則
當在函數前面被加入new調用時,也就是構造器調用時,下面這些事情會自動完成:
考慮這段代碼:
function foo(a){
console.log(this.a);
}
var bar=new foo(2);
console.log(bar.a); // 2
通過在前面使用new來調用foo(..),我們構建了一個新的對象并這個新對象作為foo(..)調用的this。 new是函數調用可以綁定this的最后一種方式,我們稱之為 new綁定(new binding)。
箭頭函數并非使用function關鍵字進行定義,而是通過所謂的“大箭頭”操作符:=>,所以不會使用上面所講解的this四種標準規范,箭頭函數從封閉它的(function或global)作用域采用this綁定,即箭頭函數會繼承自外層函數調用的this綁定。
執行 fruit.call(apple)時,箭頭函數this已被綁定,無法再次被修改。
function fruit(){
return ()=> {
console.log(this.name);
}
}
var apple={
name: '蘋果'
}
var banana={
name: '香蕉'
}
var fruitCall=fruit.call(apple);
fruitCall.call(banana); // 蘋果
this是JavaScript的一個關鍵字,this不是編寫時綁定,而是運行時綁定。它依賴于函數調用的上下文條件。this綁定和函數聲明的位置無關,反而和函數被調用的方式有關。為執行中的函數判定this綁定需要找到這個函數的直接調用點。找到之后,4種規則將會以 這個 優先順序施用于調用點:
與這4種綁定規則不同,ES6的箭頭方法使用詞法作用域來決定this綁定,這意味著它們采用封閉他們的函數調用作為this綁定(無論它是什么)。它們實質上是ES6之前的self=this代碼的語法替代品。
在 Java 等面向對象的語言中,this 關鍵字的含義是明確且具體的,即指代當前對象。一般在編譯期確定下來,或稱為編譯期綁定。而在 JavaScript 中,this 是動態綁定,或稱為運行期綁定的,這就導致 JavaScript 中的 this 關鍵字有能力具備多重含義,帶來靈活性的同時,也為初學者帶來不少困惑。本文僅就這一問題展開討論,閱罷本文,讀者若能正確回答 JavaScript 中的 What 's this 問題,那就會覺得花費這么多功夫,撰寫這樣一篇文章是值得的。
在 Java 中定義類經常會使用 this 關鍵字,多數情況下是為了避免命名沖突,比如在下面例子的中,定義一個 Point 類,很自然的,大家會使用 x,y 為其屬性或成員變量命名,在構造函數中,使用 x,y 為參數命名,相比其他的名字,比如 a,b,也更有意義。這時候就需要使用 this 來避免命名上的沖突。另一種情況是為了方便的調用其他構造函數,比如定義在 x 軸上的點,其 x 值默認為 0,使用時只要提供 y 值就可以了,我們可以為此定義一個只需傳入一個參數的構造函數。無論哪種情況,this 的含義是一樣的,均指當前對象。
清單 1. Point.java
由于其運行期綁定的特性,JavaScript 中的 this 含義要豐富得多,它可以是全局對象、當前對象或者任意對象,這完全取決于函數的調用方式。JavaScript 中函數的調用有以下幾種方式:作為對象方法調用,作為函數調用,作為構造函數調用,和使用 apply 或 call 調用。下面我們將按照調用方式的不同,分別討論 this 的含義。
在 JavaScript 中,函數也是對象,因此函數可以作為一個對象的屬性,此時該函數被稱為該對象的方法,在使用這種調用方式時,this 被自然綁定到該對象。
清單 2. point.js
函數也可以直接被調用,此時 this 綁定到全局對象。在瀏覽器中,window 就是該全局對象。比如下面的例子:函數被調用時,this 被綁定到全局對象,接下來執行賦值語句,相當于隱式的聲明了一個全局變量,這顯然不是調用者希望的。
清單 3. nonsense.js
對于內部函數,即聲明在另外一個函數體內的函數,這種綁定到全局對象的方式會產生另外一個問題。我們仍然以前面提到的 point 對象為例,這次我們希望在 moveTo 方法內定義兩個函數,分別將 x,y 坐標進行平移。結果可能出乎大家意料,不僅 point 對象沒有移動,反而多出兩個全局變量 x,y。
清單 4. point.js
這屬于 JavaScript 的設計缺陷,正確的設計方式是內部函數的 this 應該綁定到其外層函數對應的對象上,為了規避這一設計缺陷,聰明的 JavaScript 程序員想出了變量替代的方法,約定俗成,該變量一般被命名為 that。
清單 5. point2.js
JavaScript 支持面向對象式編程,與主流的面向對象式編程語言不同,JavaScript 并沒有類(class)的概念,而是使用基于原型(prototype)的繼承方式。相應的,JavaScript 中的構造函數也很特殊,如果不使用 new 調用,則和普通函數一樣。作為又一項約定俗成的準則,構造函數以大寫字母開頭,提醒調用者使用正確的方式調用。如果調用正確,this 綁定到新創建的對象上。
清單 6. Point.js
讓我們再一次重申,在 JavaScript 中函數也是對象,對象則有方法,apply 和 call 就是函數對象的方法。這兩個方法異常強大,他們允許切換函數執行的上下文環境(context),即 this 綁定的對象。很多 JavaScript 中的技巧以及類庫都用到了該方法。讓我們看一個具體的例子:
清單 7. Point2.js
在上面的例子中,我們使用構造函數生成了一個對象 p1,該對象同時具有 moveTo 方法;使用對象字面量創建了另一個對象 p2,我們看到使用 apply 可以將 p1 的方法應用到 p2 上,這時候 this 也被綁定到對象 p2 上。另一個方法 call 也具備同樣功能,不同的是最后的參數不是作為一個數組統一傳入,而是分開傳入的。
如果像作者一樣,大家也覺得上述四種方式不方便記憶,過一段時間后,又搞不明白 this 究竟指什么。那么我向大家推薦 Yehuda Katz 的這篇文章:( http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/)。在這篇文章里,Yehuda Katz 將 apply 或 call 方式作為函數調用的基本方式,其他幾種方式都是在這一基礎上的演變,或稱之為語法糖。Yehuda Katz 強調了函數調用時 this 綁定的過程,不管函數以何種方式調用,均需完成這一綁定過程,不同的是,作為函數調用時,this 綁定到全局對象;作為方法調用時,this 綁定到該方法所屬的對象。
通過上面的描述,如果大家已經能明確區分各種情況下 this 的含義,這篇文章的目標就已經完成了。如果大家的好奇心再強一點,想知道為什么 this 在 JavaScript 中的含義如此豐富,那就得繼續閱讀下面的內容了。作者需要提前告知大家,下面的內容會比前面稍顯枯燥,如果只想明白 this 的含義,閱讀到此已經足夠了。如果大家不嫌枯燥,非要探尋其中究竟,那就一起邁入下一節吧。
JavaScript 中的函數既可以被當作普通函數執行,也可以作為對象的方法執行,這是導致 this 含義如此豐富的主要原因。一個函數被執行時,會創建一個執行環境(ExecutionContext),函數的所有的行為均發生在此執行環境中,構建該執行環境時,JavaScript 首先會創建 arguments變量,其中包含調用函數時傳入的參數。接下來創建作用域鏈。然后初始化變量,首先初始化函數的形參表,值為 arguments變量中對應的值,如果 arguments變量中沒有對應值,則該形參初始化為 undefined。如果該函數中含有內部函數,則初始化這些內部函數。如果沒有,繼續初始化該函數內定義的局部變量,需要注意的是此時這些變量初始化為 undefined,其賦值操作在執行環境(ExecutionContext)創建成功后,函數執行時才會執行,這點對于我們理解 JavaScript 中的變量作用域非常重要,鑒于篇幅,我們先不在這里討論這個話題。最后為 this變量賦值,如前所述,會根據函數調用方式的不同,賦給 this全局對象,當前對象等。至此函數的執行環境(ExecutionContext)創建成功,函數開始逐行執行,所需變量均從之前構建好的執行環境(ExecutionContext)中讀取。
有了前面對于函數執行環境的描述,我們來看看 this 在 JavaScript 中經常被誤用的一種情況:回調函數。JavaScript 支持函數式編程,函數屬于一級對象,可以作為參數被傳遞。請看下面的例子 myObject.handler 作為回調函數,會在 onclick 事件被觸發時調用,但此時,該函數已經在另外一個執行環境(ExecutionContext)中執行了,this 自然也不會綁定到 myObject 對象上。
清單 8. callback.js
button.onclick=myObject.handler;
這是 JavaScript 新手們經常犯的一個錯誤,為了避免這種錯誤,許多 JavaScript 框架都提供了手動綁定 this 的方法。比如 Dojo 就提供了 lang.hitch,該方法接受一個對象和函數作為參數,返回一個新函數,執行時 this 綁定到傳入的對象上。使用 Dojo,可以將上面的例子改為:
清單 9. Callback2.js
button.onclick=lang.hitch(myObject, myObject.handler);
在新版的 JavaScript 中,已經提供了內置的 bind 方法供大家使用。
JavaScript 中的 eval 方法可以將字符串轉換為 JavaScript 代碼,使用 eval 方法時,this 指向哪里呢?答案很簡單,看誰在調用 eval 方法,調用者的執行環境(ExecutionContext)中的 this 就被 eval 方法繼承下來了。簡單看個eval示例:
本文介紹了 JavaScript 中的 this 關鍵字在各種情況下的含義,雖然這只是 JavaScript 中一個很小的概念,但借此我們可以深入了解 JavaScript 中函數的執行環境,而這是理解閉包等其他概念的基礎。掌握了這些概念,才能充分發揮 JavaScript 的特點,才會發現 JavaScript 語言特性的強大。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。