[TOC]
# 作用域、作用域鏈、閉包
## 作用域和作用域鏈
作用域和作用域鏈,簡單來說只要記住以下幾個點:
- 作用域最大的用處是隔離變量,不同作用域下同名變量不會沖突。
- 作用域是分層的,內層作用域可以訪問外層作用域的變量,反之則不行。
- 作用域鏈用于標識符解析。標識符解析是沿著作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,然后逐級地向后回溯,直至找到標識符為止(如果找不到標識符,通常會導致錯誤發生)。
從編譯原理的角度來講,JavaScript 引擎會在代碼執行前對其進行編譯,在這個過程中,像 var a = 2 這樣的聲明會被分解為兩個獨立的步驟:
1. 首先,var a 在其作用域中聲明新變量。這會在最開始的階段,也就是代碼執行前進行。
2. 接下來,a = 2 會查詢(LHS 查詢)變量 a 并對其進行賦值,查詢是沿著作用域鏈進行的。
## 詞法作用域、函數作用域、塊作用域
上面大致介紹了作用域和作用域鏈的用途,下面對其進行更深入的分析。
<span style="font-size: 20px;">詞法作用域</span>
簡單來說,詞法作用域就是定義在詞法階段的作用域(大部分編譯器都有詞法分析這一階段),即詞法作用域是由你在寫代碼時將變量和塊作用域寫在哪里來決定的。
在《你不知道的 JavaScript》中舉了這么一個例子:
```js
function foo (a) {
var b = a * 2
function bar (c) {
console.log(a, b, c)
}
bar(b * 3)
}
foo(2) // 2, 4, 12
```
這個例子中有三個逐級嵌套的作用域,可以將它們想象成幾個逐級包含的氣泡。

? 包含著整個全局作用域,其中只有一個標識符:foo
? 包含著 foo 所創建的作用域,其中有三個標識符:a、bar 和 b
? 包含著 bar 所創建的作用域,其中只有一個標識符:c
顯然,這些氣泡的排列順序是在書寫代碼時決定的,那么什么會產生一個新的氣泡?只有函數會生成新的氣泡嗎?
對于 JavaScript 而言,每聲明一個函數確實會為其自身創建一個氣泡,但是需要注意的是除了函數之外其他結構也能創建作用域氣泡,比如 with、try / catch、以及 let 聲明對應的塊作用域。
<span style="font-size: 20px;">函數作用域</span>
函數作用域的含義是:屬于這個函數的全部變量都可以在整個函數的范圍內使用及復用。
函數聲明與函數表達式:函數聲明是不可以省略函數名的,而函數表達式可以
```js
function foo (a) {
console.log('function')
}
setTimeout(function () {
console.log('I waited 1 second')
}, 1000)
```
另外,需要注意的是立即執行函數表達式(IIFE - Immediately Invoked Function Expression)的作用域問題:
```js
var a = 2; // 加分號...否則編譯報錯
(function foo () {
var a = 3
console.log(a) // 3
console.log(foo) // [Function: foo]
})()
console.log(a) // 2
console.log(foo) // ReferenceError: foo is not defined
```
由于函數被包含在一對()括號內部,因此成為了一個函數表達式,通過在末尾加上另外一個()可以立即執行這個函數。這種形式讓 foo 變量名隱藏在自身中而不會非必要地污染外部作用域。
<span style="font-size: 20px;">塊作用域</span>
除 JavaScript 外的很多編程語言都支持塊作用域,塊作用域的用處就是讓變量的聲明距離其使用的地方盡可能的近,通常是將變量綁定到所在的 {...} 中。
```js
var foo = true
if (foo) {
var bar = foo * 2
}
console.log(bar) // 2
```
bar 變量僅在 if 聲明的上下文中使用,因此如果能將它聲明在 if 塊內部將會是一個非常有意義的事情,ES6 的 let 關鍵字就很好地解決了這個問題。
```js
var foo = true
if (foo) {
let bar = foo * 2
}
console.log(bar) // ReferenceError: bar is not defined
```
另一個常見的問題就是 for 循環:
```js
for (var i = 0; i < 10; i++) {
}
console.log(i) // 10
for (let j = 0; j < 10; j++) {
}
console.log(j) // ReferenceError: j is not defined
```
## 閉包
函數 A 返回了一個函數 B,并且函數 B 中使用了函數 A 的變量,就形成了閉包。
理解閉包的關鍵在于:**內部函數的作用域鏈中包含外部函數的作用域(本身作用域的上一級)**。
《JavaScript高級程序設計》的這個例子十分清晰地解釋了閉包
``` js
function createComparisonFunction(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName]
var value2 = object2[propertyName]
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
}
// 創建函數
var compareNames = createComparisonFunction('name')
// 調用函數
var result = compareNames({name: 'Nicholas'}, {name: 'Greg'})
// 解除對匿名函數的引用(以便釋放內存)
compareNames = null
```

這個圖涉及到了 **執行環境**、**變量對象**、**活動對象**、**作用域鏈** 等概念,其解釋如下:
**執行環境(execution context)**:執行環境定義了變量或函數有權訪問的其他數據
- 全局執行環境是最外圍的一個執行環境,在 Web 瀏覽器中被認為是 window 對象,因此所有全局變量和函數都是作為 window 對象的屬性和方法創建的
- 每個函數都有自己的執行環境
- 當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。 而在函數執行之后,棧將其環境彈出,把控制權返回給之前的執行環境。ECMAScript 程序中的執行流正是由這個方便的機制控制著。
**變量對象(variable object)**:每個執行環境都有一個與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在后臺使用它。
**活動對象(activation object)**:執行環境為函數,則其變量對象就稱為活動對象
**作用域鏈(scope chain)**:當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。
*****
> 冴羽的博客對這幾個名詞進行了深入的探討:[http://www.hmoore.net/chenmk/web-knowledges/1154482](http://www.hmoore.net/chenmk/web-knowledges/1154482)
理解閉包的關鍵在于:**內部函數的作用域鏈中包含(本身作用域的上一級)外部函數的作用域**,再觀察下上面的圖,就是說內部函數對外部函數的活動對象有引用,所以即使外部函數出棧了,其活動對象仍然駐留在內存中。
因此,閉包不能濫用,其會導致內存泄漏,影響網頁的性能。閉包使用完之后,要立即釋放資源,將引用變量指向 null,這等同于通知垃圾回收例程將其清除。
### 閉包中的 this
在閉包中使用 this 對象也可能會導致一些問題。我們知道,this 對象是在運行時基于函數的執行環境綁定的:**在全局函數中,this 等于 window,而當函數被作為某個對象的方法調用時,this 等于那個對象**。this 的綁定規則見下一部分,下面來看一個例子
``` js
var name = 'The Window'
var object = {
name: 'My Object',
getNameFunc: function () {
return function () {
return this.name
}
}
}
console.log(object.getNameFunc()()) // "The Window"(在非嚴格模式下)
// node.js 環境下是 undefined
```
按照之前的對閉包的解釋會有如下一個問題,為什么匿名函數沒有取得其包含作用域(或外部作用域)的 this 對象呢?
每個函數在被調用時都會自動取得兩個特殊變量:this 和 arguments。內部函數在搜索這兩個變量時,**只會搜索到其活動對象為止,因此永遠不可能直接訪問外部函數中的這兩個變量**。不過,把外部作用域中的 this 對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了,如下所示。
``` js
var name = 'The Window'
var object = {
name: 'My Object',
getNameFunc: function () {
var self = this
return function () {
return self.name
}
}
}
console.log(object.getNameFunc()()) // My Object
```
### 閉包的用途
一句話回顧下閉包的定義:函數 A 返回了一個函數 B,并且函數 B 中使用了函數 A 的變量,就形成了閉包。其大致有以下用途:
1. 可以讀取函數內部的變量。
2. 可以使變量的值長期保存在內存中
3. 可以用來實現 JS 模塊。
4. 模仿塊級作用域
5. 實現私有變量
``` js
// IIFE + 閉包實現 JS 模塊
var Module = (function () {
var _private = 'safe now'
var foo = function () {
console.log(_private)
}
return {
foo: foo
}
})()
Module.foo() // safe now
console.log(Module._private) // undefined
```
[IIFE](https://developer.mozilla.org/zh-CN/docs/Glossary/%E7%AB%8B%E5%8D%B3%E6%89%A7%E8%A1%8C%E5%87%BD%E6%95%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F)
什么樣的代碼才能稱為 JS 模塊呢?
具有特定功能的 JS 文件,將所有的數據和功能都封裝在一個函數內部(私有的),只向外暴露一個包含 n 個方法的對象或函數,模塊的使用者,只需要通過模塊暴露的對象調用方法來實現對應的功能。
另外函數節流與防抖也用到了閉包,有時候需要緩存一些變量也會用到閉包,可以參看 JS-Coding 和設計模式章節。
### 例題
``` js
function outer() {
var num=0 // 內部變量
return function add() { // 通過 return 返回 add 函數,就可以在 outer 函數外訪問了
num++ // 內部函數有引用,作為 add 函數的一部分了
console.log(num)
}
}
var func1 = outer()
func1() // 實際上是調用 add 函數, 輸出1
func1() // 輸出2 因為 outer 函數內部的私有作用域會一直被占用
var func2 = outer()
func2() // 輸出1 每次重新引用函數的時候,閉包是全新的。
func2() // 輸出2
```
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs