
看到評論裡有小夥伴建議我試試箭頭函數,真是受寵若驚,本來寫文章也隻是想記錄,寫要點給自己日後看的。
之前看過一篇總結javascript中this的文章,也同樣提到了箭頭函數中this的指向問題,是以,我今天想對此做一個完善與總結。
一、問題的起源
論壇上看到這樣一道js程式設計題:要求用閉包實作每隔5s輸出0-9之間的十個數字。這裡先給出我寫的最終實作方案,如下圖:
毫無疑問,這裡必須要用到定時器setTimeout或者setInterval,但是考慮到setInterval存在的兩個問題:
- 某些間隔會被跳過
- 多個定時器的代碼執行之間的間隔可能會比預期的小
是以,用到setInterval的地方一般都是用遞歸調用setTimeout的方式來替代,但是關于這兩個定時函數中的this我之前的了解有些偏差,我知道這裡的this指的是全局對象window,因為setTimeout和setInterval都是作為全局函數,也就是window對象的方法存在的。但是這裡有兩個this:
第一個this:setTimeout(this.func, times)
第二個this: setTimeout(function(){ alert(this)},times);
那到底哪一個'this'始終指向的是window呢?
二、執行環境、活動對象、變量對象、作用域鍊、this
首先澄清一下幾個概念。
執行環境
執行環境定義了變量和函數有權通路的其他資料,決定了它們各自的行為。每個執行環境都有一個與之關聯的變量對象,環境中定義的所有變量和函數都儲存在這個變量對象中。
全局執行環境是最外圍的一個執行環境。根據ECMAScript實作所在的宿主環境不同,表示全局執行環境的對象也不一樣。在web浏覽器中,全局執行環境被認為是window對象,因為所有的全局變量和函數都是作為window對象的屬性和方法建立的。某個執行環境中的代碼執行完畢後,該環境就會被銷毀,儲存在其中的所有變量和函數也随之銷毀(全局執行環境直到應用程式退出時才會銷毀)
每個函數都有自己的執行環境。當執行流進入一個函數時,該函數的執行環境就會被推入一個環境棧中。而在函數執行後,棧将其環境彈出,把控制權傳回給之前的執行環境。
作用域鍊
當代碼在一個環境中執行時,會建立變量對象的一個作用域鍊。
作用域鍊本質上是一個指向變量對象的指針清單,它隻引用,但不實際包含變量對象。
作用域鍊的作用,是保證對執行環境有權通路的所有變量和函數的有序性。作用域鍊的最前端,始終都是目前執行的代碼所在環境的變量對象。如果這個環境是函數,則将其活動對象作為變量對象,活動對象在最開始時隻包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。作用域鍊中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自再下一個包含環境。這樣一直延續到全局執行環境;全局執行環境的變量對象始終是作用域鍊中的最後一個對象。
辨別符解析就是沿着作用域鍊一級一級地搜尋辨別符的過程。
this
this是一個對象,this對象是在運作時基于函數的執行環境綁定的。
在全局函數中,this等于window;而當函數作為某個對象的方法調用時,this等于那個對象。
匿名函數的執行環境具有全局性,其this通常指向window。這是因為,每個函數再被調用時都會自動取得兩個特殊變量:this和arguments内部函數在搜尋這兩個變量時,隻會搜尋到其活動對象為止,是以永遠不可能通路到外部函數中的這兩個變量。
閉包
閉包是指有權通路另一個函數作用域中的變量的函數
當某個函數被調用時,會建立一個執行環境及相應的作用域鍊。然後用arguments和其他的命名參數的值來初始化函數的活動對象。
閉包的主要用途有:模仿塊級作用域和私用變量。
變量對象
變量對象中儲存了目前執行環境中定義的所有變量和函數。
變量對象是和執行環境綁定的,而this是和函數運作時所在的執行環境綁定的。比如對于一個全局執行環境,其中的'this'指的是該函數運作時所在的全局執行環境,也就是window;而變量對象隸屬于這個函數建立的局部執行環境。
三、 setTimeout和setInterval中的this
測試一
我們先來做幾個測試
- 測試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
關于javascript 中的進階定時器的若幹問題
第10行,setTimeout(method,500),此時調用的是全局函數method。因為,雖然仍在構造函數的局部執行環境内,但是局部執行環境的變量對象中并沒有method方法,是以,在進行辨別符解析時,沿着作用域鍊在全局執行環境中找到了method方法。
要注意通過第6句this.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 中的延遲執行函數中的變量也是根據其所在的執行環境上下文來确定的,符合作用域鍊的辨別符解析過程。
四、嚴格模式下的this
除了正常運作模式,ECMAscript 5添加了第二種運作模式:"嚴格模式"(trict mode)。顧名思義,這種模式使得Javascript在更嚴格的條件下運作。
關于嚴格模式的介紹,請移步這裡Javascript 嚴格模式詳解
嚴格模式所帶來的文法和行為的改變大緻有以下 條:
1.全局變量顯示聲明
2.靜态綁定
- (1).禁止使用with語句
- (2).創設eval作用域
3.增強的安全措施
- (1).禁止this關鍵字指向全局對象
- (2).禁止在函數内周遊調用棧,主要是指caller和arguments這兩個函數對象屬性。
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);請看如下測試。
五、箭頭函數中的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指向不可改變。請看如下測試。
六、參考
1.談談setTimeout的作用域以及this的指向問題
2.http://www.jb51.net/article/30858.htm
3.javascript進階教程
4.JavaScript 中的 this !