JavaScript中this詳解_javascript技巧

來源:互聯網
上載者:User

都說 JavaScript 是一種很靈活的語言,這其實也可以說它是一個混亂的語言。它把函數式編程和物件導向編程糅合一起,再加上動態語言特性,簡直強大無比(其實是不能和C++比的,^_^ )。

這裡的主題是 this ,不扯遠了。this 本身原本很簡單,總是指向類的當前執行個體,this 不能賦值。這前提是說 this 不能脫離 類/對象 來說,也就是說 this 是物件導向語言裡常見的一個關鍵字。說的極端點,如果你編寫的 JS 採用函數式寫法,而不是物件導向式,你所有的代碼裡 this 會少很多,甚至沒有。記住這一點,當你使用 this 時,你應該是在使用對象/類 方式開發,否則 this 只是函數調用時的副作用。

JS 裡的 this

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

以前用 this 時經常擔心,不踏實,你不知道它到底指向誰? 這裡把它所有用到的地方列出

this 和構造器
this 和對象
this 和函數
全域環境的 this
this 和 DOM/事件
this 可以被 call/apply 改變
ES5 中新增的 bind 和 this
ES6 箭頭函數(arrow function) 和 this

1. this 和構造器

this 本身就是類定義時構造器裡需要用到的,和構造器在一起再自然不過。

/** * 頁簽 * * @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 應該掛屬性/欄位,方法都應該放在原型上。

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 應該是和物件導向相關的。純粹的函數只是一個低層級的抽象,封裝和複用。如下

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),只是間接使用罷了。

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) // truealert(andy === this.andy) // truealert(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.fnfunc() // 10fn() // 10

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

5. this 和 DOM/事件

W3C 把 DOM 實現成了各種節點,節點嵌套一起形成 DOM tree。節點有不同類型,如文本節點,元素節點等10多種。元素節點又分成了很多,對寫HTML的人來說便是很熟悉的標籤(Tag),如 div,ul,label 等。 看 W3C 的 API 文檔,會發現它完全是按照物件導向方式實現的各種 API,有 interface,extends 等。如

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

HTMLDivElement
HTMLLabelElement
HTMLInputElement
...
前面說過 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 strict 模式下還是有個坑。

6. this 可以被 call/apply 改變

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

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

通常有以下幾點特徵表示它為動態語言

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

var m1 = {  message: 'This is A'} var m2 = {  message: 'This is B'}  function showMsg() {  alert(this.message)} showMsg() // undefinedshowMsg.call(m1) // 'This is A'showMsg.call(m2) // 'This is B'

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

經常會聽到轉向 Ruby 或 Python 的人提到“編程的樂趣”,這種樂趣是源自動態語言更接近人的思維(而不是機器思維),更符合商務程序而不是項目實現流程。同樣一個功能,動態語言可以用更小的代碼量來實現。動態語言對程式員生產力的提高,是其大行其道的主要原因。

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

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

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

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

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

7. 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 的進階版,其它沒什麼特殊的。

8. 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 了。

明白了嗎?

相信看完全文以後,this不再是坑~

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.