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]]
属性中的变量 + 活动对象中的变量组成的。参考资料: