JavaScript中知而不全的this

都說 JavaScript 是一種很靈活的語言,這其實也能夠說它是一個混亂的語言。它把 函數式編程 面向對象編程 糅合一塊兒,再加上 動態語言 特性,簡直強大無比(實際上是不能和C++比的,^_^ )。javascript

 

這裏的主題是 this ,不扯遠了。this 自己本來很簡單,老是指向類的當前實例,this 不能賦值。這前提是說 this 不能脫離 類/對象 來講,也就是說 this 是面嚮對象語言裏常見的一個關鍵字。說的極端點,若是你編寫的 JS 採用函數式寫法,而不是面向對象式,你全部的代碼裏 this 會少不少,甚至沒有。記住這一點,當你使用 this 時,你應該是在使用對象/類 方式開發,不然 this 只是函數調用時的反作用。html

 

JS 裏的 thisjava

  • 在 function 內部被建立
  • 指向調用時所在函數所綁定的對象(拗口)
  • this 不能被賦值,但能夠被 call/apply  改變

 

之前用 this 時常常擔憂,不踏實,你不知道它到底指向誰? 這裏把它全部用到的地方列出node

  1. this 和構造器
  2. this 和對象
  3. this 和函數
  4. 全局環境的 this
  5. this 和 DOM/事件
  6. this 能夠被 call/apply 改變
  7. me/self/that/_this 暫存 this
  8. ES5 中新增的 bind 和 this
  9. ES6 箭頭函數(arrow function) 和 this

 

1. this 和構造器

this 自己就是類定義時構造器裏須要用到的,和構造器在一塊兒再天然不過。jquery

/**
 * 頁籤
 * 
 * @class Tab
 * @param nav {string} 頁籤標題的class
 * @param content {string} 頁面內容的class
 *
 */
function Tab(nav, content) {
	this.nav = nav
	this.content = content
}

Tab.prototype.getNav = function() {
	return this.nav;
};
Tab.prototype.setNav = function(nav) {
	this.nav = nav;
};
Tab.prototype.add = function() {
};

按照 JavaScript 的習慣, this 應該掛屬性/字段,方法都應該放在原型上。git

 

2. this 和對象

JS 中的對象不用類也能夠建立,有人可能奇怪,類是對象的模板,對象都是從模板裏 copy 出來的,沒有類怎麼建立對象? JS 的確能夠,而且你徹底能夠寫上萬行功能代碼而不用寫一個類。話說 OOP 裏說的是面向對象編程,也沒說面向類編程,是吧 ^_^ 。程序員

var tab = {
	nav: '',
	content: '', 
	getNav: function() {
		return this.nav;
	},
	setNav: function(n) {
		this.nav = n;
	}
}

 

3. this 和函數

首先,this 和獨立的函數放在一塊兒是沒有意義的,前面也提到過 this 應該是和麪向對象相關的。純粹的函數只是一個低級別的抽象,封裝和複用。以下github

function showMsg() {
	alert(this.message)
}
showMsg() // undefined

定義 showMsg,而後以函數方式調用,this.message 是 undefined。所以堅定杜絕在 純函數內使用 this,但有時候會這麼寫,調用方式使用 call/apply編程

function showMsg() {
	alert(this.message)
}

var m1 = {
	message: '輸入的電話號碼不正確'
}
var m2 = {
	message: '輸入的身份證號不正確'
}

showMsg.call(m1) // '輸入的電話號碼不正確'
showMsg.call(m2) // '輸入的身份證號不正確'

用這種方式能夠節省一些代碼量,好比當兩個 類/對象 有一共類似的方法時,沒必要寫兩份,只要定義一個,而後將其綁定在各自的原型和對象上。這時候其實你仍是在使用對象或類(方式1/2),只是間接使用罷了。api

 

4. 全局環境的 this

前面提到 this 是 「指向調用時所在函數所綁定的對象」, 這句話拗口但絕對正確,沒有一個多餘的字。全局環境中有不一樣的宿主對象,瀏覽器環境中是 window, node 環境中是 global。這裏重點說下瀏覽器環境中的 this。

 

瀏覽器環境中非函數內 this 指向 window

alert(window=== this) // true

所以你會看很不少開源 JS lib 這麼寫

(function() {
    // ...
    
})(this);

或這樣寫

(function() {
    // ...
  
}).call(this);

好比 underscore 和 requirejs,大意是把全局變量 window 傳入匿名函數內緩存起來,避免直接訪問。至於爲啥要緩存,這跟 JS 做用域鏈有關係,讀取越外層的標識符性能會越差。請自行查閱相關知識,再說就扯遠了。

 

瀏覽器中比較坑人,非函數內直接使用 var 聲明的變量默認爲全局變量,且默認掛在 window 上做爲屬性。

var andy = '劉德華'
alert(andy === window.andy) // true
alert(andy === this.andy) // true
alert(window.andy === this.andy) // true

 

由於這個特性,有些筆試題如

var x = 10;
function func() {
    alert(this.x)
}
var obj = {
    x: 20,
    fn: function() {
        alert(this.x)
    }
}
var fn = obj.fn
func() // 10
fn() // 10

沒錯,最終輸出的都是全局的 10。永遠記住這一點:判斷 this 指向誰,看執行時而非定義時,只要函數(function)沒有綁定在對象上調用,它的 this 就是 window。

 

5. this 和 DOM/事件

W3C 把 DOM 實現成了各類節點,節點嵌套一塊兒造成 DOM tree。節點有不一樣類型,如文本節點,元素節點等10多種。元素節點又分紅了不少,對寫HTML的人來講即是很熟悉的標籤(Tag),如 divullabel 等。 看 W3C 的 API 文檔,會發現它徹底是按照面向對象方式實現的各類 API,有 interface,extends 等。如

看到了吧,這是用 Java 寫的,既然是用面向對象方式實現的API,必定有類/對象(廢話^_^),有 類/對象,則必定有 this (別忘了這篇文章的中心主題)。全部的 HTML tag 類命名如 HTMLXXXElement,如

前面說過 this 是指向當前類的實例對象,對於這些 tag 類來講,不看其源碼也知它們的不少方法內部用到的 this 是指向本身的。 有了這個結論,寫 HTML 和 JS 時, this 就清晰了不少。

 

示例A

<!-- this 指向 div -->
<div onclick="alert(this)"></div>

  

示例B

<div id="nav"></div>
<script>
    nav.onclick = function() {
        alert(this) // 指向div#nav
    }
</script>

 

示例C

$('#nav').on('click', function() {
    alert(this) // 指向 nav
})

 

以上三個示例能夠看到,在給元素節點添加事件的時候,其響應函數(handler)執行時的 this 都指向 Element 節點自身。jQuery 也保持了和標準一致,但卻讓人迷惑,按 「this 指向調用時所在函數所綁定的對象」 這個定義,jQuery 事件 handler 裏的 this,應該指向 jQuery 對象,而非 DOM 節點。所以你會發如今用 jQuery 時,常常須要把事件 handler 裏的 element 在用 $ 包裹下變成 jQuery 對象後再去操做。好比

$('#nav').on('click', function() {
    var $el = $(this) // 再次轉爲 jQuery 對象,若是 this 直接爲 jQuery 對象更好
    $el.attr('data-x', x)
    $el.attr('data-x', x)
})

 

有人可能有以下的疑問

<div id="nav" onclick="getId()">ddd</div>
<script>
    function getId() {
        alert(this.id)
    }
</script>

點擊 div 後,爲何 id 是 undefined,不說是指向的 當前元素 div 嗎? 若是記住了前面提到的一句話,就很清楚爲啥是 undefined,把這句話再貼出來。

判斷 this 指向誰,看執行時而非定義時,只要函數(function)沒有綁定在對象上調用,它的 this 就是 window

這裏函數 getId 調用時沒有綁定在任何對象上,能夠理解成這種結構

div.onclick = function() {
    getId()
}

getId 所處匿名函數裏的 this 是 div,但 getId 自身內的 this 則不是了。 固然 ES5 嚴格模式下仍是有個

 

6. this 能夠被 call/apply 改變

call/apply 是函數調用的另外兩種方式,二者的第一個參數均可以改變函數的上下文 this。call/apply 是 JS 裏動態語言特性的表徵。動態語言通俗的定義

程序在運行時能夠改變其結構,新的函數能夠被引進,已有的函數能夠被刪除,即程序在運行時能夠發生結構上的變化

 

一般有如下幾點特徵表示它爲動態語言

  1. 動態的數據類型
  2. 動態的函數執行
  3. 動態的方法重寫

動態語言多從世界第二門語言 LISP 發展而來,如死去的 SmallTalk/VB,目前還活着的 Perl/Python, 以及還流行的 Ruby/JavaScript。JS 裏動態數據類型的體現即是弱類型,執行的時候纔去分析標識符的類型。函數動態執行體現爲 eval,call/aply。動態方法重寫則體如今 JS 原型可重寫。不扯遠,這裏重點說下 call/apply 對 this 的影響。

var m1 = {
    message: 'This is A'
}  
var m2 = {
    message: 'This is B'
}   

function showMsg() {
    alert(this.message)
}

showMsg() // undefined
showMsg.call(m1) // 'This is A'
showMsg.call(m2) // 'This is B'

能夠看到單獨調用 showMsg 返回的是 undefined,只有將它綁定到具備 message 屬性的對象上執行時纔有意義。發揮想象力延伸下,若是把一些通用函數寫好,能夠任意綁定在多個類的原型上,這樣動態的給類添加了一些方法,還節省了代碼。這是一種強大的功能,也是動態語言的強表現力的體現。

常常會聽到轉向 Ruby 或 Python 的人提到「編程的樂趣」,這種樂趣是源自動態語言更接近人的思惟(而不是機器思惟),更符合業務流程而不是項目實現流程。一樣一個功能,動態語言能夠用更小的代碼量來實現。動態語言對程序員生產力的提升,是其大行其道的主要緣由。

性能方面,動態語言沒有太大的優點,但動態語言的理念是:優化人的時間而不是機器的時間。提升開發者的生產力,寧可犧牲部分的程序性能或者購買更高配置的硬件。隨着 IT 業的不斷髮展和摩爾定律的做用,硬件相對於人件一直在貶值,這個理念便有了合理的現實基礎。

 

JS 裏的 call/apply 在任何一個流行的 lib 裏都會用到,但幾乎就是兩個做用

  1. 配合寫類工具實現OOP,如 mootools, ClassJS, class.js,
  2. 修復DOM事件裏的 this,如 jQuery, events.js

 

關於 call 和 apply 複用:利用apply和arguments複用方法

關於 call 和 apply 的性能問題參考: 冗餘換性能-從Backbone的triggerEvents說開了去

 

7. me/self/that/_this 暫存 this

若是採用 OOP 方式寫 JS 代碼,無可避免的會用到 this,方法內會訪問類的內部屬性(字段),也可能會調用類的另外一個方法。當類的方法內又有一個 function 時,好比瀏覽器端開發常常碰見的給 DOM 元素添加事件,這時若是事件處理器(handler)中的想調用類的一個方法,此時 handler 內的 this 是 dom 元素而非類的當前對象。這個時候,須要把 this 暫存,開發者發揮着本身的聰明才智留下了幾種經典的命名 me, self, that, _this。如

 

又如這是我曾經採用 OOP 方式寫過的一段代碼

通常會在每一個方法的第一句就把 this 暫存下來。

 

 

8. ES5 中新增的 bind 和 this

上面 6 裏提到 call/apply 在 JS 裏體現動態語言特性及動態語言的流行緣由,其在 JS 用途如此普遍。ES5發佈時將其採納,提了一個更高級的方法 bind

var modal = {
    message: 'This is A'
}

function showMsg() {
    alert(this.message)
}

var otherShowMsg = showMsg.bind(modal)
otherShowMsg() // 'This is A'

由於是 ES5 才加的,低版本的IE不支持,能夠修復下Function.prototype。bind 只是 call/apply 的高級版,其它沒什麼特殊的。

 

9. ES6 箭頭函數(arrow function) 和 this

ES6 在今年的 6月18日 正式發佈(恰京東店慶日同一天,^_^),它帶來的另外一種類型的函數 - 箭頭函數。箭頭函數的一個重要特徵就是顛覆了上面的一句話,再貼一次

判斷 this 指向誰,看執行時而非定義時,只要函數(function)沒有綁定在對象上調用,它的 this 就是 window

 

是的,前面一直用這句話來判斷 this 的指向,在箭頭函數裏前面半句就失效了。箭頭函數的特徵就是,定義在哪,this 就指向那。即箭頭函數定義在一個對象裏,那箭頭函數裏的 this 就指向該對象。以下

var book = {
    author: 'John Resig',
    init:  function() {
        document.onclick = ev => {
            alert(this.author) ; // 這裏的 this 不是 document 了
        }
    }
};
book.init()

對象 book 裏有一個屬性 author, 有一個 init 方法, 給 document 添加了一個點擊事件,若是是傳統的函數,咱們知道 this 指向應該是 document,但箭頭函數會指向當前對象 book。

箭頭函數讓 JS 迴歸天然和簡單,函數定義在哪它 this 就指向哪,定義在對象裏它指向該對象,定義在類的原型上,就指向該類的實例,望文知意這樣更容易理解。

 

總結: 

函數的上下文 this 是 JS 裏不太好理解的,在於 JS 函數自身有多種用途。目的是實現各類語言範型(面向對象,函數式,動態)。this 本質是和麪向對象聯繫的,和寫類,對象關聯一塊兒的, 和「函數式」沒有關係的。若是你採用過程式函數式開發,徹底不會用到一個 this。 但在瀏覽器端開發時卻無可避免的會用到 this,這是由於瀏覽器對象模型(DOM)自己採用面向對象方式開發,Tag 實現爲一個個的類,類的方法天然會引用類的其它方法,引用方式必然是用 this。當你給DOM對象添加事件時,回調函數裏引用該對象就只能用 this 了。

相關文章
相關標籤/搜索