title: JavaScript中的做用域、執行上下文與閉包 date: 2019-03-18 08:34:44 tags:javascript
JavaScript 代碼的整個執行過程,分爲兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段做用域規則會肯定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段建立。前端
做用域是定義變量的區域。java
它規定了執行代碼時查找變量的範圍,也就是變量的做用範圍。git
JavaScript 採用詞法做用域(lexical scoping),也就是靜態做用域。github
由於 JavaScript 採用的是詞法做用域,函數的做用域在函數定義的時候就決定了。面試
var a = 1
function foo1() {
console.log(a)
}
function foo2() {
var a = 2
foo1()
}
foo2() // 1
複製代碼
在 JavaScript 中無塊級做用域(一對兒 { } 包含的區域 ),只有全局做用域和函數做用域。segmentfault
if (true) {
var name = 'abc'
}
console.log(name) // 'abc'
複製代碼
同一個頁面中全部的 <script>
標籤中的沒在函數體內的變量,都在同一個全局做用域中。瀏覽器
執行上下文能夠抽象成一個對象,當一個函數執行時就會建立一個執行上下文。bash
每一個執行上下文,都有三個屬性:閉包
既然每執行一個函數就會建立一個執行上下文,咱們寫的函數可不止一個,那麼如何管理衆多的上下文呢?
JavaScript 引擎建立了執行上下文棧(Execution context stack, ECS) 來管理執行上下文。
JavaScript 引擎在執行代碼時,最早遇到的是全局代碼,會建立一個全局上下文(globalContext)並將之壓入執行上下文棧(ECS)中,當執行一個函數時,建立一個函數上下文並壓入 ECS 中。函數執行完畢時,這個函數上下文會出棧並被清理。當整個程序執行完畢時,globalContext 也會出棧並被清理。
看一個例子:
function fun1() {
fun2()
}
function fun2() {
console.log('fun2')
}
fun1()
複製代碼
當 JavaScript 引擎執行代碼時,先建立一個全局上下文(globalContext) 壓入執行上下文棧(ECS)中。
當執行到 fun1()
時,建立一個函數上下文(EC_fun1)並壓入棧中。
而後執行 fun1 函數體裏的 fun2()
,建立 EC_fun2並壓入棧中。
執行 fun2 函數裏的 console.log()
函數,建立 EC_consolelog
壓入棧中。
而後 console.log
執行完畢,EC_consolelog
出棧並銷燬。
而後 fun2
執行完畢,EC_fun2
出棧並銷燬。
fun1
執行完畢,EC_fun1
出棧並銷燬。
變量對象是與執行上下文相關的數據做用域,它存儲了在該上下文定義的變量和函數聲明。變量對象是在建立函數上下文時建立的,它經過函數的
arguments
屬性初識化。
變量對象包括:
undefined
組成一個 VO 的屬性舉個例子:
function foo(a) {
var b = 2
var a = 10
function c() {}
var d = function() {}
}
foo(11)
複製代碼
在執行 foo(11)
時,建立一個執行上下文,此時執行上下文的變量對象時:
VO = {
arguments: {
0: 11,
length: 1
},
a: 11,
b: undefined,
c: reference to function c() {},
d: undefined
}
複製代碼
this
指向的是全局對象。window
,Node.js 環境下全局對象是 global
。活動對象與變量對象實際上是同一個東西,只有當進入一個執行上下文中(這個執行上下文處在執行上下文棧的棧頂),這個執行上下文的變量對象(VO)纔會被激活,此時這個變量對象叫作活動對象(AO)。只有活動對象上的各類屬性才能被訪問。
當查找變量時,會在當前執行上下文的變量對象(也是活動對象)中查找,若是沒找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象(也是全局對象)。這樣由多個執行上下文的變量對象構成的鏈表就叫作做用域鏈。
函數定義時,有一個內部屬性 [[scope]]
保存了全部父變量對象。當函數執行時會建立一個做用域鏈,這個做用域鏈包含了函數的 [[scope]]
屬性和執行上下文的活動對象(AO)。
var a = 1
var b = 1
function foo() {
var b = 2
return a + b
}
foo() // 3
複製代碼
執行過程以下:
[[scope]]
屬性中。foo.[[scope]] = [globalContext.VO]
複製代碼
ECS = [ fooContext,
globalContext]
複製代碼
fooContext = {
Scope: foo.[[scope]]
}
複製代碼
arguments
屬性初始化fooContext = {
AO: {
arguments: {
length: 0
},
b: undefined
},
Scope: [
foo.[[scope]]
]
}
複製代碼
fooContext = {
AO: {
arguments: {
length: 0
},
b: undefined
},
Scope: [
AO,
foo.[[scope]]
]
}
複製代碼
AO:{
arguments: {
length: 0
},
b: 2
}
複製代碼
ECS = [
globalContext
]
複製代碼
MDN 對閉包的定義爲:
閉包是指那些可以訪問自由變量的函數。
那什麼是自由變量呢?
自由變量是指在函數中使用的變量,但既不是函數參數也不是函數局部變量。
自由變量實際上是指函數執行上下文的做用域鏈中非活動對象的那部分屬性(也就是外層做用域的變量 函數.[[scope]]
)
因此在《JavaScript高級程序設計中》是這樣描述的:
閉包是指有權訪問另外一個函數做用域中的變量的函數。
var a = 1
function foo() {
console.log(a)
}
foo()
複製代碼
foo 函數能夠訪問變量a
,但 a
既不是函數參數也不是函數的局部變量,a
就是自由變量。
那麼,foo 函數就是一個閉包。
到這裏,你或許會有疑問
這怎麼和咱們平時知道的閉包不是同一個,不是什麼函數中嵌套一個函數,裏面的函數纔是一個閉包,這裏怎麼沒有嵌套函數了?
實際上是站的角度不一樣:
[[scope]]
屬性)。因此在函數中能夠做用域鏈訪問到外層做用域的變量,也就是能夠訪問自由變量,它就是一個閉包。舉個例子:
function foo() {
var a = 0
return function add() {
console.log(a++)
}
}
var add1 = foo()
var add2 = foo()
add1() // 0
add1() // 1
add1() // 2
add2() // 0
add2() // 1
複製代碼
foo() 函數執行完畢,foo函數執行上下文已經被銷燬,那麼上下文的變量對象中的 a
變量應該也被銷燬了啊,問啥還能訪問到?
由於 add 函數在定義時就存在一個 [[scope]]
屬性,它保存了 foo 函數執行上下文的變量對象,在執行 add 函數時,會建立一個執行上下文鏈並將 add.[[scope]]
複製到該執行上下文的做用域鏈中,因此在 add 函數中能夠經過做用域鏈訪問到 a
屬性。
你可能還有一個問題,爲何 add1()
和 add2()
訪問的不是同一個 a
?
由於每執行一次函數就會建立一個函數執行上下文,因此執行 add1 = foo()
和 add2 = foo()
產生的是不一樣的執行上下文(對象),他們的 a
屬性固然不一樣了。
經典面試題,使用閉包解決 for 循環中
var
異步打印 i 值的問題
for (var i = 0; i < 5 ; i++) {
setTimeout(function foo() {
console.log(i)
},1000 * i)
}
複製代碼
以上代碼執行結果是:5 5 5 5 5
。
這裏面涉及到事件循環機制,這裏就很少贅述,簡單的說就是等 for 循環結束後,纔開始依次執行這幾個 setTimeout()
裏面的 foo 函數。
JS 沒有塊級做用域,此時的 i
值爲 5 ,console.log(i)
訪問的就是全局變量 i
,因此打印 5。
咱們要作就是使用閉包的特性,讓 console.log(i)
訪問的不是全局變量 i
for (var i = 0; i < 5 ; i++) {
;(function(i) {
setTimeout(function foo() {
console.log(i)
},1000 * i)
})(i)
}
複製代碼
或者這樣:
for (var i = 0; i < 5 ; i++) {
setTimeout((function foo(i) {
return function() {
console.log(i)
}
})(i),1000 * i)
}
複製代碼
也可使用 ES6 中的 let
換掉 var
,使得 for 循環中 i
成爲一個塊級做用域的本地變量。
for (let i = 0; i < 5 ; i++) {
setTimeout(function foo() {
console.log(i)
},1000 * i)
}
複製代碼
閉包能夠建立私有屬性和方法。
var singel = (function () {
// 私有屬性,外部訪問不到
var age = 20
function foo() {
console.log('foo')
}
return {
// 公有屬性
name: 'Tom',
getAge: function() {
return age
},
setAge: function(n) {
age = n
}
}
})()
console.log(singel.age) // undefined
singel.foo() // Uncaught TypeError: singel.foo is not a function
console.log(singel.getAge()) // 20
singel.setAge(10)
console.log(singel.getAge()) // 10
複製代碼
單例:指的是隻有一個實例的對象。JavaScript 通常以字面量的方式來建立單例。
匿名函數最大的用途就是建立閉包。而且還能夠構建命名空間,減小全局變量的污染。
經過匿名函數實現一個閉包計數器:
var numberCounter = (function() {
var num = 0
return function() {
return ++num
}
})()
複製代碼
閉包的缺陷:
arguments
屬性初始化,它包含函數的參數、函數體裏聲明的變量。[[scope]]
屬性中的變量 + 活動對象中的變量組成的。參考資料: