關於javascript 中的高級定時器的若干問題

看到評論裏有仁兄建議我試試箭頭函數,真是受寵若驚,原本寫這篇文章也只是想記錄寫要點給本身往後看的。今天早上看到一篇總結javascriptthis的文章JavaScript 中的 this !,也一樣提到了箭頭函數中this的指向問題,因此,又對這篇文章進行了完善。javascript

1、問題的起源

論壇上看到這樣一道js編程題:要求用閉包實現每隔5s輸出0-9之間的十個數字。這裏先給出我寫的最終實現方案,以下圖:
圖片描述
毫無疑問,這裏必需要用到定時器setTimeout或者setInterval,可是考慮到setInterval存在的兩個問題:html

  • 某些間隔會被跳過前端

  • 多個定時器的代碼執行之間的間隔可能會比預期的小java

因此,用到setInterval的地方通常都是用遞歸調用setTimeout的方式來替代,可是關於這兩個定時函數中的this我以前的理解有些誤差,我知道這裏的this指的是全局對象window,由於setTimeoutsetInterval都是做爲全局函數,也就是window對象的方法存在的。可是這裏有兩個thises6

第一個this:setTimeout(this.func, times)web

第二個this: setTimeout(function(){ alert(this)},times);編程

那到底哪個'this'始終指向的是window呢?瀏覽器

2、執行環境、活動對象、變量對象、做用域鏈、this

首先澄清一下幾個概念。安全

執行環境

執行環境定義了變量和函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有一個與之關聯的變量對象,環境中定義的全部變量和函數都保存在這個變量對象中。閉包

全局執行環境是最外圍的一個執行環境。根據ECMAScript實現所在的宿主環境不一樣,表示全局執行環境的對象也不同。在web瀏覽器中,全局執行環境被認爲是window對象,由於全部的全局變量和函數都是做爲window對象的屬性和方法建立的。某個執行環境中的代碼執行完畢後,該環境就會被銷燬,保存在其中的全部變量和函數也隨之銷燬(全局執行環境直到應用程序退出時纔會銷燬)

每一個函數都有本身的執行環境。當執行流進入一個函數時,該函數的執行環境就會被推入一個環境棧中。而在函數執行後,棧將其環境彈出,把控制權返回給以前的執行環境。

做用域鏈

當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈
做用域鏈本質上是一個指向變量對象的指針列表,它只引用,但不實際包含變量對象
做用域鏈的做用,是保證對執行環境有權訪問的全部變量和函數的有序性。做用域鏈的最前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是函數,則將其活動對象做爲變量對象,活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。做用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自再下一個包含環境。這樣一直延續到全局執行環境;全局執行環境的變量對象始終是做用域鏈中的最後一個對象。
標識符解析就是沿着做用域鏈一級一級地搜索標識符的過程。

this

this是一個對象,this對象是在運行時基於函數的執行環境綁定的。
在全局函數中,this等於window;而當函數做爲某個對象的方法調用時,this等於那個對象。
匿名函數的執行環境具備全局性,其this一般指向window。這是由於,每一個函數再被調用時都會自動取得兩個特殊變量:thisarguments內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能訪問到外部函數中的這兩個變量。

閉包

閉包是指有權訪問另外一個函數做用域中的變量的函數
當某個函數被調用時,會建立一個執行環境及相應的做用域鏈。而後用arguments和其餘的命名參數的值來初始化函數的活動對象。
閉包的主要用途有:模仿塊級做用域和私用變量。

變量對象

變量對象中保存了當前執行環境中定義的全部變量和函數。
變量對象是和執行環境綁定的,而this是和函數運行時所在的執行環境綁定的。好比對於一個全局執行環境,其中的'this'指的是該函數運行時所在的全局執行環境,也就是window;而變量對象隸屬於這個函數建立的局部執行環境。

3、 setTimeoutsetInterval中的this

測試一

咱們先來作幾個測試

  • 測試1
    測試1

10行,setTimeout(this.method,500),此時調用的是構造函數內的method方法,也就是說這裏的第一個'this'指向的是構造函數生成的對象,便是根據setTimeout調用時所在的執行環境肯定的。

儘管調用的是對象的method方法,可是方法內的this(第二個this)等於window。爲何會是這樣呢?在看下面一個測試

圖片描述

其實,setTimeout 也只是一個函數而已,函數必然有可能須要參數,咱們把 this.a 看成一個參數傳給 setTimeout 這個函數,就像它須要一個 fun 參數,在傳入參數的時候,其實作了個這樣的操做 fun = this.a,看到沒有,這裏咱們直接把 fun 指向 this.a 的引用;執行的時候實際上是執行了 fun() 因此已經和 obj 無關了,它是被看成普通函數直接調用的,所以 this 指向全局對象。

  • 測試2
    圖片描述

10行,setTimeout(method,500),此時調用的是全局函數method。由於,雖然仍在構造函數的局部執行環境內,可是局部執行環境的變量對象中並無method方法,因此,在進行標識符解析時,沿着做用域鏈在全局執行環境中找到了method方法。

要注意經過第6this.method=...聲明的這個方法屬於構造函數生成的對象,而不屬於構造函數的變量對象,也就是說,並不存在於做用域鏈中。

第二個this仍然等於window

  • 測試3
    圖片描述

10行,setTimeout(method,500),此時調用的是構造函數method
第二個this仍然等於window

  • 測試4
    圖片描述

setTimeout第一個參數是javascript代碼字符串時,第二個this仍然等於window

  • 測試5
    圖片描述

setTimeout第一個參數是匿名函數時,第二個this仍然等於window

結論一

根據以上測試,能夠得出如下結論:

  • setTimeout 中的延遲執行函數中的this (也就是第二個this)始終指向window

  • setTimeout(this.method, minsec)這種形式的this(也就是第一個this),其指向是根據上下文的執行環境肯定的。

測試二

該測試的目的是肯定setTimeout 中的延遲執行函數中的變量是如何沿着做用域鏈搜索的。

  • 測試6
    圖片描述

  • 測試7
    圖片描述

  • 測試8
    圖片描述

測試6和測試7本質上是相同的,由於函數名只是一個指針,指向函數對象。
測試測試6和測試7中,console.log(value)中的value都是構造函數局部執行環境中的value值,而console.log(this.value)中的value都是全局執行環境中的value值。

測試8中的test指向的是全局執行環境中的test,相應的的value都是全局執行環境中的value值。

延遲函數中的變量也是根據其所在的執行環境上下文來肯定的,符合做用域鏈的標識符解析過程。

  • 測試9
    圖片描述

兩個value都指向的是全局執行環境中的value值,由於console.log(value)語句所在的局部執行環境上下文並無value值。

結論二

setTimeout 中的延遲執行函數中的變量也是根據其所在的執行環境上下文來肯定的,符合做用域鏈的標識符解析過程。

4、嚴格模式下的this

除了正常運行模式,ECMAscript 5添加了第二種運行模式:"嚴格模式"(trict mode)。顧名思義,這種模式使得Javascript在更嚴格的條件下運行。

關於嚴格模式的介紹,請移步這裏Javascript 嚴格模式詳解

嚴格模式所帶來的語法和行爲的改變大體有如下 條:

1.全局變量顯示聲明

2.靜態綁定

(1).禁止使用with語句

(2).創設eval做用域

3.加強的安全措施

(1).禁止this關鍵字指向全局對象

(2).禁止在函數內遍歷調用棧,主要是指callerarguments這兩個函數對象屬性。

4.禁止刪除變量,只有configurable(不懂這個的去看看《javascript高級教程》中關於數據屬性訪問器屬性的介紹)設置爲true的對象屬性,才能被刪除。

5.顯示報錯

6.重名錯誤

(1).對象不能有重名屬性

(2).函數不能有重名參數

7.禁止八進制表示法

8.對arguments對象的限制

(1).不容許對arguments賦值

(2).arguments再也不追蹤參數的變化

(3).禁止使用arguments.callee

9.只容許在全局做用域或函數做用域的頂層聲明函數

10.保留字

  • 在嚴格模式的狀況下執行純粹的函數調用,那麼這裏的的 this 並不會指向全局,而是undefined.請看以下測試:
    圖片描述

在這個測試例子中,匿名的自執行函數都返回1,目的是避免函數返回undefined形成誤解,要知道js的函數在沒有明確指定返回值的狀況下默認是返回undefined,用new調用的構造函數除外。

  • 在嚴格模式下,setTimeout 方法在調用傳入函數的時候,若是這個函數沒有指定了的 this,那麼它會作一個隱式的操做—-自動地注入全局上下文,等同於調用 foo.apply(window) 而非 foo();所以延遲執行函數中的this仍然指向window,而不是undefined.

  • 固然,若是咱們在傳入函數的時候已經指定this,那麼就不會被注入全局對象,好比: setTimeout(foo.bind(obj), 1);請看以下測試。
    圖片描述

5、箭頭函數中的this

在 ES6 的新規範中,加入了箭頭函數(想了解更多,請移步這裏ECMAScript 6 入門),它和普通函數最不同的一點就是 this 的指向.

  • 箭頭函數中的 this 只和定義它的時候所在的做用域的 this 有關,而與在哪裏以及如何調用它無關,同時它的 this 指向是不可改變的。請看以下測試。
    圖片描述

在執行 setTimeout 時候,咱們先是定義了一個匿名的箭頭函數,關鍵點就在這,箭頭函數內的 this 執行定義時所在的對象,就是指向定義這個箭頭函數時做用域內的 this,也就是obj.foo中的this(不要誤解爲是 setTimeout中的this啊,只不過是它的實參而已。),即 obj;因此在執行箭頭函數的時候,它的 this -> obj.foo 中的 this -> obj;

利用閉包這種固化this的特性,能夠完美的解決以前必須用閉包才能給延遲執行函數綁定this的問題。

  • 箭頭函數內的this指向不可改變。請看以下測試。
    圖片描述

6、參考

1.談談setTimeout的做用域以及this的指向問題
2.http://www.jb51.net/article/30858.htm
3.javascript高級教程
4.JavaScript 中的 this !

相關文章
相關標籤/搜索