淺析JavaScript中的 this 指向
前言
this指向是學習JavaScript必須理解的知識點,在ES6沒有出現時,this是動態繫結的,或稱為執行期繫結的,這就導致this關鍵字有能力具備多重含義,帶來靈活性的同時,也為初學者帶來不少困惑。
JavaScript中的this
JavaScript中的this含義很豐富,他可以是全域性物件、當前物件或者任意物件,這完全取決於函式的呼叫方式。
JavaScript中函式的呼叫有以下幾種方式:
作為物件方法呼叫
作為函式呼叫
作為建構函式呼叫
使用apply或call呼叫、bind呼叫
下面分別來看一下上面說的幾種呼叫方式
作為物件方法呼叫
var point = { x:0, y:0, moveTo: function(x,y){ this.x = this.x + x; this.y = this.y + y; } }; point.moveTo(1,1)// this 繫結到當前物件,即point物件
上面這段程式碼我們定義了一個point物件,其有一個moveTo方法,當該方法作為物件方法被呼叫時,此時this繫結到當前物件,及point物件。
作為函式呼叫
普通函式內部的this分兩種情況, 嚴格模式和非嚴格模式
。
非嚴格模式下,this 預設指向全域性物件window
function f1(){ return this; } f1() === window; // true
而嚴格模式下, this為undefined
function f2(){ "use strict"; // 這裡是嚴格模式 return this; } f2() === undefined; // true
我們常見的是非嚴格模式,函式可以直接被呼叫,此時this繫結到全域性物件。在瀏覽器中,全域性物件就是window物件,在下面的例子中,函式被呼叫時,this被繫結到全域性物件,接下來執行賦值語句,相當於聲明瞭一個全域性變數,這顯然是不應該的。
function setX(x) { this.x = x; } setX(6); x; //x 已經是一個全域性變數
對於內部函式(即宣告在一個函式體內的函式),這種繫結到全域性物件的方式會產生另一個問題,我們仍然以前面提到的point為例,這次我們希望在 moveTo 方法內定義兩個函式,分別將 x,y 座標進行平移。結果可能出乎大家意料,不僅 point 物件沒有移動,反而多出兩個全域性變數 x,y。
var point = { x:0, y:0, moveTo: function(x,y){ var moveX = function(x){ this.x = x;//this 繫結在哪裡 } var moveY = function(y){ this.y = y; } moveX(x); moveY(y); } point.moveTo(5,5); console.log(point.x);//0 console.log(point.y);//0 console.log(x);//5 console.log(y);//5 }
這屬於JavaScript的設計缺陷,正確的設計方式是內部函式的this應該繫結到其外層函式對應的物件上,為例避免這一設計缺陷,聰明的JavaScript程式員想出了變數替代的方法,約定俗稱,該變數一般被命名為that。上面的程式碼更改如下:
var point = { x : 0, y : 0, moveTo : function(x, y) { //繫結內部函式的this為外層函式對應的物件 var that = this; // 內部函式 var moveX = function(x) { that.x = x; }; // 內部函式 var moveY = function(y) { that.y = y; } moveX(x); moveY(y); } }; point.moveTo(5, 5); console.log(point.x); //==>5 console.log(point.y); //==>5 //Uncaught ReferenceError: x is not defined console.log(x);
作為建構函式呼叫
JavaScript 支援面向物件式程式設計,與主流的面向物件式程式語言不同,JavaScript 並沒有類(class)的概念,而是使用 基於原型(prototype)的繼承方式
。相應的,JavaScript 中的建構函式也很特殊,如果不使用 new
呼叫,則和普通函式一樣。作為又一項約定俗成的準則, 建構函式以大寫字母開頭,提醒呼叫者使用正確的方式呼叫 。如果呼叫正確,this 繫結到新建立的物件上。
function Point(x, y){ this.x = x; this.y = y; } var point1 = new Point(1,1);//this 指向point1
使用 apply 或 call 呼叫
讓我們再一次重申,在 JavaScript 中函式也是物件,物件則有方法, apply 和 call 就是函式物件的方法 。這兩個方法異常強大,他們允許切換函式執行的上下文環境(context),即 this 繫結的物件。很多 JavaScript 中的技巧以及類庫都用到了該方法。讓我們看一個具體的例子:
function Point(x, y){ this.x = x; this.y = y; this.moveTo = function(x, y){ this.x = x; this.y = y; } } var p1 = new Point(1,1); var p2 = {x:2, y:2}; p1.moveTo(3,3); p1.moveTo.apply(p2, [10, 10]); // call使用 // p1.moveTo.call(p2, 10,10);
在上面的程式碼中,我們使用了建構函式生成了物件p1,該物件同時具有moveTo方法。然後我們使用字面量建立了物件p2,我們看到使用 apply 可以將 p1 的方法應用到 p2 上,這時候 this 也被繫結到物件 p2 上。另一個方法 call 也具備同樣功能,不同的是最後的引數不是作為一個數組統一傳入,而是分開傳入的。
ofollow,noindex">apply和call使用bind
ind是在EcmaScript5中擴充套件的方法(IE6,7,8不支援)
bind() 方法與 apply 和 call 很相似,也是可以改變函式體內 this 的指向。
MDN的解釋是:bind()方法會建立一個新函式,稱為繫結函式,當呼叫這個繫結函式時,繫結函式會以建立它時傳入 bind()方法的第一個引數作為 this,傳入 bind() 方法的第二個以及以後的引數加上繫結函式執行時本身的引數按照順序作為原函式的引數來呼叫原函式。
[注意:bind方法的返回值是函式]
var bar=function(){ console.log(this.x); } var foo={ x:3 } bar();//此時this指向全域性變數,x不存在 bar.bind(foo)(); /*或*/ var func=bar.bind(foo);//此時this指向foo物件,執行函式後列印結果為3 func(); 輸出: undefined 3
我們再來看一個多引數的例子:
傳入多個引數時,this指向第一個引數({x:1}這個物件),第二個引數2,按照順序作為原函式的引數傳入,故y=2,bind方法產生的新的函式m中傳入的引數3傳給原函式的z,故最終答案為6.
function f(y, z){ return this.x + y + z; } var m = f.bind({x : 1}, 2); console.log(m(3)); //6
另外有個有趣的問題,如果連續 bind() 兩次,亦或者是連續 bind() 三次那麼輸出的值是什麼呢?像這樣:
var bar = function(){ console.log(this.x); } var foo = {x:3} var sed = {x:4} var func = bar.bind(foo).bind(sed); func(); //? var fiv = {x:5} var func = bar.bind(foo).bind(sed).bind(fiv); func(); //?
答案是,兩次都仍將輸出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是無效的。更深層次的原因, bind() 的實現,相當於使用函式在內部包了一個 call / apply ,第二次 bind() 相當於再包住第一次 bind() ,故第二次以後的 bind 是無法生效的。
apply、call、bind比較
1、apply 、 call 、bind 三者都是用來改變函式的this物件的指向的;
2、apply 、 call 、bind 三者第一個引數都是this要指向的物件,也就是想指定的上下文;
3、apply 、 call 、bind 三者都可以利用後續引數傳參;
4、bind 是返回對應函式,便於稍後呼叫;apply 、call 則是立即呼叫 。
DOM 事件處理函式中的 this & 內聯事件中的 this
1.當函式被當做監聽事件處理函式時, 其 this 指向觸發該事件的元素 (針對於addEventListener事件)
// 被呼叫時,將關聯的元素變成藍色 function bluify(e){ //在控制檯打印出所點選元素 console.log(this); //阻止時間冒泡 e.stopPropagation(); //阻止元素的預設事件 e.preventDefault(); this.style.backgroundColor = '#A5D9F3'; } // 獲取文件中的所有元素的列表 var elements = document.getElementsByTagName('*'); // 將bluify作為元素的點選監聽函式,當元素被點選時,就會變成藍色 for(var i=0 ; i<elements.length ; i++){ elements[i].addEventListener('click', bluify, false); }
2.內聯事件
內聯事件中的this指向分兩種情況:
(1)當代碼被內聯處理函式呼叫時,它的this指向監聽器所在的DOM元素
(2)當代碼被包括在函式內部執行時,其this指向等同於 ****函式直接呼叫****的情況,即在非嚴格模式指向全域性物件window, 在嚴格模式指向undefined。

內聯事件中的this
ES6中 this指向
由於箭頭函式不繫結this, 它會捕獲其所在(即定義的位置)上下文的this值, 作為自己的this值。
1.所以 call() / apply() / bind() 方法對於箭頭函式來說只是傳入引數,對它的 this 毫無影響。
2.考慮到 this 是詞法層面上的,嚴格模式中與 this 相關的規則都將被忽略。(可以忽略是否在嚴格模式下的影響)
我們來看一個例子:
//定義一個物件 var obj = { x:100, //屬性x show(){ //延遲500毫秒,輸出x的值 setTimeout( //匿名函式 function(){console.log(this.x);}, 500); } }; obj.show();//列印結果:undefined
為什麼結果列印是undefined呢,問題出在了this上,當代碼執行到了setTimeout( )的時候,此時的this已經變成了window物件(setTimeout( )是window物件的方法),已經不再是obj物件了,所以我們用this.x獲取的時候,獲取的不是obj.x的值,而是window.x的值,再加上window上沒有定義屬性x,所以得到的結果就是:undefined。
如果使用箭頭函式來編寫同樣的一段程式碼,得到的this.x又是另一番景象。
//定義一個物件 var obj = { x:100,//屬性x show(){ //延遲500毫秒,輸出x的值 setTimeout( //不同處:箭頭函式 () => { console.log(this.x)}, 500 ); } }; obj.show();//列印結果:100
為什麼會是這樣呢?
箭頭函式的this繫結看的是this所在的函式定義在哪個物件下,繫結到哪個物件則this就指向哪個物件 。
持續更新...