這是一篇很短的文章,介紹了js幾個比較重要的概念,適合通勤路上快速閱讀加深理解和記憶。
js中的做用域是詞法做用域,即由 函數聲明時 所在的位置決定的。詞法做用域是指在編譯階段就產生的,一整套函數標識符的訪問規則。(區別於詞法做用域,動態做用域是在函數執行的時候確認的,js的沒有動態做用域,但js的this
很像動態做用域,後面會提到。詞法做用域的概念十分重要,請多加記憶並理解。) 說到底js的做用域只是一個「空地盤」,其中並無真實的變量,可是卻定義了變量如何訪問的規則。javascript
做用域鏈本質上是一個指向變量對象的指針列表,它只引用不包含實際變量對象,是做用域概念的延申。做用域鏈定義了變量在當前上下文訪問不到的時候如何沿做用域鏈繼續查詢的一套規則。前端
執行上下文是指 函數調用時 產生的變量對象,這個變量對象咱們沒法直接訪問,可是能夠訪問其中的變量、this
對象等。例如:java
let fn, bar; // 一、進入全局上下文環境 bar = function(x) { let b = 5; fn(x + b); // 三、進入fn函數上下文環境 }; fn = function(y) { let c = 5; console.log(y + c); //四、fn出棧,bar出棧 }; bar(10); // 二、進入bar函數上下文環境
每次函數調用時,都會產生一個新的執行上下文環境,JavaScript引擎會以棧的方式來處理它們,這個棧,咱們稱其爲函數調用棧(call stack)。棧底永遠都是全局上下文,而棧頂就是當前處於活動狀態的正在執行的上下文,也稱爲活動對象(running execution context,圖中藍色的塊),區別與底下被掛起的上下文(變量對象)。es6
總結:做用域是在函數聲明的時候就肯定的一套變量訪問規則,而執行上下文是函數執行時才產生的一系列變量的環境。也就是說做用域定義了執行上下文中的變量的訪問規則,執行上下文在這個做用域規則的前提下進行變量查找,函數引用等具體操做。閉包
函數的執行過程分紅兩部分,一部分用來生成執行上下文環境,肯定this的指向、聲明變量以及生成做用域鏈;另外一部分則是按順序逐行執行代碼。app
關於js的this
關鍵字,我記得第一次接觸仍是在作前端半年或一年的時候(哈哈我就是這麼水)。那時候徐哥(java大佬)教我在綁定click
事件的時候把this
傳給事件處理函數,相似<button onclick="handle(this)">確認</button>
,我當時就懵了,this
是什麼鬼?!今後正式開啓了我三年的js痛苦之旅:封裝啊、閉包啊、面向對象啊、繼承啊等等等等。this
的指向說來講去其實只有四種:函數
let fn = function(){ alert(this.name) } let obj = { name: '', fn } fn() // 方法1 obj.fn() // 方法2 fn.call(obj) // 方法3 let instance = new fn() // 方法4
fn()
,這種看着像光桿司令的調用方式,this
指向window
(嚴格模式下是undefined
)。obj.fn()
,此時this
指向obj
對象。點調用中this
指的是點前面的對象。call
函數把fn
中的this
指向了第一個參數,這裏是obj
。即利用call
、apply
、bind
函數能夠把函數的this
變量指向第一個參數。new
實例化了一個對象instance
,這時fn
中的this
就指向了實例instance
。若是同時發生了多個規則怎麼辦?其實上面四條規則的優先級是遞增的:this
fn() < obj.fn() < fn.call(obj) < new fn()
首先,new
調用的優先級最高,只要有new
關鍵字,this
就指向實例自己;接下來若是沒有new
關鍵字,有call、apply、bind
函數,那麼this
就指向第一個參數;而後若是沒有new、call、apply、bind
,只有obj.foo()
這種點調用方式,this
指向點前面的對象;最後是光桿司令foo()
這種調用方式,this
指向window
(嚴格模式下是undefined
)。spa
es6中新增了箭頭函數,而箭頭函數最大的特點就是沒有本身的this、arguments、super、new.target
,而且箭頭函數沒有原型對象prototype
不能用做構造函數(new
一個箭頭函數會報錯)。由於沒有本身的this
,因此箭頭函數中的this
其實指的是包含函數中的this
。不管是點調用,仍是call
調用,都沒法改變箭頭函數中的this
。prototype
js的閉包是新手的噩夢,在學js的前三年,我查閱了無數的博文,苦苦搜索閉包的概念,然而最終一無所得。MDN上這樣定義閉包:閉包是函數和聲明該函數的詞法環境的組合。
what?能說人話嗎?
很長時間以來我對閉包都停留在「定義在一個函數內部的函數」這樣膚淺的理解上。事實上這只是閉包造成的必要條件之一。直到後來看了kyle大佬的《你不知道的JAVASCRIPT》上冊中關於閉包的定義,我才豁然開朗:
當函數可以記住並訪問所在的詞法做用域時,就產生了閉包。
let single = (function(){ let count = 0 return { plus(){ count++ return count }, minus(){ count-- return count } } })() single.plus() // 1 single.minus() // 0
這是個單例模式,這個模式返回了一個對象single
,對象中包含兩個函數plus
和minus
,而這兩個函數都用到了所在詞法做用域中的變量count
,因此在函數執行結束時count
所在的執行環境不會被銷燬,這就產生了閉包。每次調用single.plus()
或者single.minus()
,就會對閉包中的count
變量進行修改,這兩個函數保持住了對所在的詞法做用域的引用。
閉包實際上是一種特殊的函數,它能夠訪問函數內部的變量,還可讓這些變量的值始終保持在內存中,不會在函數調用後被垃圾回收機制清除。
看個經典安利:
// 方法1 for (var i = 1; i <= 5; i++) { setTimeout(function() { console.log(i) }, 1000) } // 方法2 for (let i = 1; i <= 5; i++) { setTimeout(function() { console.log(i) }, 1000) }
方法1中,循環設置了五個定時器,一秒後定時器中回調函數將執行,打印變量i
的值。毋庸置疑,一秒以後i
已經遞增到了5,因此定時器打印了五次5 。(定時器中並無找到當前做用域的變量i
,因此沿做用域鏈找到了全局做用域中的i
)
方法2中,因爲es6的let
會建立局部做用域,因此循環設置了五個做用域,而五個做用域中的變量i
分佈是1-5,每一個做用域中又設置了一個定時器,打印一秒後變量i
的值。一秒後,定時器從各自父做用域中分別找到的變量i
是1-5 。這是個利用閉包解決循環中變量發生異常的新方法。
我真的學不動了。