执行上下文与闭包
说明
词法环境、执行上下文、作用域、闭包笔记总结
# 执行栈
执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
# 执行上下文
执行上下文(Execution context stack),一个解析和执行代码的环境;即代码都在执行上下文中运行。是一个抽象的概念。
(1)全局执行上下文
任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
(2)函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个
JavaScript引擎使用执行上下文栈来管理执行上下文
当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
# 创建执行上下文
创建执行上下文有两个阶段:创建阶段 和 执行阶段。
在创建阶段会发生三件事:
this 值的决定,即我们所熟知的 This 绑定。
在全局执行上下文中,
this
的值指向全局对象。(在浏览器中,this
引用 Window 对象)。在函数执行上下文中,
this
的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么this
会被设置成那个对象,否则this
的值被设置为全局对象或者undefined
(在严格模式下创建词法环境组件。
创建变量环境组件。
# 作用域
作用域就是一个独立的区域,它可以让变量不会向外暴露出去。作用域最大的用处就是隔离变量。内层作用域可以访问外层作用域。一个作用域下可能包含若干个执行上下文。
(1)全局作用域
直接写在script标签的JS代码,都在全局作用域。在全局作用域下声明的变量叫做全局变量(在块级外部定义的变量)。
全局变量在全局的任何位置下都可以使用;全局作用域中无法访问到局部作用域的中的变量。
全局作用域在页面打开的时候创建,在页面关闭时销毁。
所有 window 对象的属性拥有全局作用域
var和function命令声明的全局变量和函数是window对象的属性和方法
let命令、const命令、class命令声明的全局变量,不属于window对象的属性
(2)函数作用域(局部作用域)
- 调用函数时会创建函数作用域,函数执行完毕以后,作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的。
- 在函数作用域中可以访问全局变量,在函数的外面无法访问函数内的变量。
- 当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有就向上一作用域中寻找,直到找到全局作用域,如果全局作用域中仍然没有找到,则会报错。
(3)块级作用域
- ES6之前JavaScript采用的是函数作用域+词法作用域,ES6引入了块级作用域。
- 任何一对花括号{}中的语句集都属于一个块,在块中使用let和const声明的变量,外部是访问不到的,这种作用域的规则就叫块级作用域。
- 通过var声明的变量或者非严格模式下创建的函数声明没有块级作用域。
# 闭包
闭包就是指有权访问另一个函数作用域中的变量的函数。
浏览器在加载页面会把代码放在栈内存( ECStack )中执行,函数进栈执行会产生一个私有上下文( EC ),此上下文能保护里面的使用变量( AO )不受外界干扰,并且如果当前执行上下文中的某些内容,被上下文以外的内容占用,当前上下文不会出栈释放,这样可以保存里面的变量和变量值,所以闭包是一种保存和保护内部私有变量的机制。
# 词法环境
在 JavaScript 中,词法环境(Lexical Environment)是表示变量和函数标识符(Identifier)绑定的集合,使得 JavaScript 引擎可以通过标识符找到对应的变量值和函数。词法环境也是变量和函数的作用域(Scope)。
简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
一个词法环境可以有一个或多个父级词法环境,形成一个词法环境链(Lexical Environment Chain)。这个链条在执行 JavaScript 代码时进行查找,直到找到目标标识符绑定的值或到达全局环境为止。全局环境的词法环境链上不存在任何父级词法环境。
现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用。
- 环境记录器是存储变量和函数声明的实际位置。
- 外部环境的引用意味着它可以访问其父级词法环境(作用域)。
# 词法环境内部组成
在 JavaScript 中,每个运行的函数,代码块 {...}
以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
词法环境对象由两部分组成:
**环境记录(Environment Record)**一个存储变量和函数标识符以及它们的绑定关系的对象
1.声明式环境记录(Declarative Environment Record)
用来存储变量、函数声明和块级作用域声明等声明的绑定关系,例如 let、const、class、function 等声明。
2.对象式环境记录(Object Environment Record)
用来存储 this 和 with 语句等的绑定关系。对象式环境记录可以看作是一个指向外部对象的引用。
对外部词法环境的引用(Outer Lexical Environment Reference),与外部代码相关联。
外部词法环境引用是指指向父级词法环境的引用,形成了一个词法环境链,用于支持词法作用域。全局环境的外部词法环境引用为 null。
一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。
矩形表示环境记录(变量存储),箭头表示外部引用。全局词法环境没有外部引用,所以箭头指向了 null
。
- 当脚本开始运行,词法环境预先填充了所有声明的变量。
- 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用
let
声明前,不能引用它。几乎就像变量不存在一样。
- 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用
- 然后
let phrase
定义出现了。它尚未被赋值,因此它的值为undefined
。从这一刻起,我们就可以使用变量了。 phrase
被赋予了一个值。phrase
的值被修改。
词法环境是一个规范对象
“词法环境”是一个规范对象(specification object):它只存在于 语言规范 (opens new window) 的“理论”层面,用于描述事物是如何工作的。我们无法在代码中获取该对象并直接对其进行操作。
但 JavaScript 引擎同样可以优化它,比如清除未被使用的变量以节省内存和执行其他内部技巧等,但显性行为应该是和上述的无差。
# 内部和外部的词法环境
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。
在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。
在这个函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局):
- 内部词法环境与
say
的当前执行相对应。它具有一个单独的属性:name
,函数的参数。我们调用的是say("John")
,所以name
的值为"John"
。 - 外部词法环境是全局词法环境。它具有
phrase
变量和函数本身。
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
let counter2 = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter2() ); // ?
alert( counter2() ); // ?
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
答案是 0和1
函数 counter
和 counter2
是通过 makeCounter
的不同调用创建的。
因此,它们具有独立的外部词法环境,每一个都有自己的 count
。
# 作用域链
js 变量查找的方式是通过作用域链来查找,而不是通过执行上下文从上到下查找。每个执行上下文中会定义下一个查找变量的执行上下文的位置,形成了作用域链,每个节点的指向取决于函数的实际位置。这种以实际位置形成作用域的方式,就叫它词法作用域
# 暂时性死区
从程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的 let
语句。换句话说,一个变量从技术的角度来讲是存在的,但是在 let
之前还不能使用
let x = 1;
function func() {
console.log(x); // error
let x = 2;
}
func();
2
3
4
5
6
7
8
9
# 函数提升
函数提升只针对具名函数,而对于赋值的匿名函数,并不会存在函数提升。
console.log(a); // f a()
console.log(b); //undefined
function a(){
console.log('hello')
}
var b=function(){
console.log('world')
}
2
3
4
5
6
7
8
9
10
# 变量提升
函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数。
console.log(a); //f a()
console.log(a()); //1
var a=1;
function a(){
console.log(1);
}
console.log(a); //1
a=3
console.log(a()) //a not a function
2
3
4
5
6
7
8
9
10
# 参考
JavaScript高级程序设计(第4版)
现代 JavaScript 教程 (opens new window)
深入理解JavaScript——词法环境 - 个人文章 - SegmentFault 思否 (opens new window)
[译] 理解 JavaScript 中的执行上下文和执行栈 - 掘金 (juejin.cn) (opens new window)