JS基礎篇之做用域、執行上下文、this、閉包

這是一篇很短的文章,介紹了js幾個比較重要的概念,適合通勤路上快速閱讀加深理解和記憶。javascript

做用域和執行上下文

做用域:

js中的做用域是詞法做用域,即由 函數聲明時 所在的位置決定的。詞法做用域是指在編譯階段就產生的,一整套函數標識符的訪問規則。(區別於詞法做用域,動態做用域是在函數執行的時候確認的,js的沒有動態做用域,但js的this很像動態做用域,後面會提到。詞法做用域的概念十分重要,請多加記憶並理解。) 說到底js的做用域只是一個「空地盤」,其中並無真實的變量,可是卻定義了變量如何訪問的規則。前端

做用域鏈本質上是一個指向變量對象的指針列表,它只引用不包含實際變量對象,是做用域概念的延申。做用域鏈定義了變量在當前上下文訪問不到的時候如何沿做用域鏈繼續查詢的一套規則。java

執行上下文:

執行上下文是指 函數調用時 產生的變量對象,這個變量對象咱們沒法直接訪問,可是能夠訪問其中的變量、this對象等。例如:es6

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,圖中藍色的塊),區別與底下被掛起的上下文(變量對象)。

總結:做用域是在函數聲明的時候就肯定的一套變量訪問規則,而執行上下文是函數執行時才產生的一系列變量的環境。也就是說做用域定義了執行上下文中的變量的訪問規則,執行上下文在這個做用域規則的前提下進行變量查找,函數引用等具體操做。bash

理解函數執行過程

函數的執行過程分紅兩部分,一部分用來生成執行上下文環境,肯定this的指向、聲明變量以及生成做用域鏈;另外一部分則是按順序逐行執行代碼。閉包

  • 創建執行上下文階段(發生在:函數被調用時 && 函數體內的代碼執行前 )
  1. 生成變量對象,順序:建立 arguments 對象 --> 建立function函數聲明 --> 建立var變量聲明
  2. 生成做用域鏈
  3. 肯定this的指向
  • 函數執行階段
  1. 逐行執行代碼,這個階段會完成變量賦值,函數引用,以及執行其餘代碼。

this指向

關於js的this關鍵字,我記得第一次接觸仍是在作前端半年或一年的時候(哈哈我就是這麼水)。那時候徐哥(java大佬)教我在綁定click事件的時候把this傳給事件處理函數,相似<button onclick="handle(this)">確認</button>,我當時就懵了,this是什麼鬼?!今後正式開啓了我三年的js痛苦之旅:封裝啊、閉包啊、面向對象啊、繼承啊等等等等。this的指向說來講去其實只有四種:app

let fn = function(){
  alert(this.name)
}
let obj = {
  name: '',
  fn
}
fn() // 方法1
obj.fn() // 方法2
fn.call(obj) // 方法3
let instance = new fn() // 方法4
複製代碼
  1. 方法1中直接調用函數fn(),這種看着像光桿司令的調用方式,this指向window(嚴格模式下是undefined)。
  2. 方法2中是點調用obj.fn(),此時this指向obj對象。點調用中this指的是點前面的對象。
  3. 方法3中利用call函數把fn中的this指向了第一個參數,這裏是obj。即利用callapplybind函數能夠把函數的this變量指向第一個參數。
  4. 方法4中用new實例化了一個對象instance,這時fn中的this就指向了實例instance

若是同時發生了多個規則怎麼辦?其實上面四條規則的優先級是遞增的:函數

fn() < obj.fn() < fn.call(obj) < new fn()ui

首先,new調用的優先級最高,只要有new關鍵字,this就指向實例自己;接下來若是沒有new關鍵字,有call、apply、bind函數,那麼this就指向第一個參數;而後若是沒有new、call、apply、bind,只有obj.foo()這種點調用方式,this指向點前面的對象;最後是光桿司令foo() 這種調用方式,this指向window(嚴格模式下是undefined)。this

es6中新增了箭頭函數,而箭頭函數最大的特點就是沒有本身的this、arguments、super、new.target,而且箭頭函數沒有原型對象prototype不能用做構造函數(new一個箭頭函數會報錯)。由於沒有本身的this,因此箭頭函數中的this其實指的是包含函數中的this。不管是點調用,仍是call調用,都沒法改變箭頭函數中的this

閉包

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,對象中包含兩個函數plusminus,而這兩個函數都用到了所在詞法做用域中的變量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 。這是個利用閉包解決循環中變量發生異常的新方法。

最後

我真的學不動了。

相關文章
相關標籤/搜索