var a = 'heihei', b = 'xixi'
function foo () {
console.log(a)
}
function bar () {
var a = 'houhou'
foo()
console.log(b)
}
bar()
// heihei
// xixi
如果您很快就能得出上述结果,那相信您的功底非常之扎实,如果无法确定,那么这篇文章通读之后,相信可以帮您解疑。
JS中可执行代码有三种:全局代码
,函数代码
,eval代码
。代码执行前需要准备的执行环境
也称为执行上下文
,所以也分为全局执行上下文
,函数执行上下文
,eval执行上下文
。
全局执行上下文中的变量对象就是全局对象,预置了很多属性和函数。在浏览器中是window
,在NodeJS中是global
[[scope]]
属性创建作用域链(下面会讲到)arguments
创建活动对象
变量对象
被激活为激活对象
,此时发生"hoist"
声明提升
函数的所有形参
arguments
,其值也是一个对象(类数组对象,有length
属性),按形参顺序赋值,值为实参undefined
函数声明
Function
对象的引用。变量声明
undefined
)组成一个变量对象的属性被创建;
注意
: 整个过程可以大概描述成:函数的形参=>函数声明=>变量声明
, 其中在创建函数声明时,如果名字存在,则会被重写,在创建变量时,如果变量名存在,则忽略不会进行任何操作。当函数内同时存在相同形参、函数和变量名称时,实际上这个名称指向的是函数。
根据代码修改激活对象对象中对应的值。如果当前执行上下文中的变量对象没有该属性,就去父级的执行上下文变量对象中寻找,直至到全局执行上下文。找不到就报错
eval执行上下文比较特殊,它取决于eval函数是直接调用还是间接调用。
引用MDN上的说法:
如果间接的使用
eval()
,比如通过一个引用来调用它,而不是直接的调用eval
。 从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。
eval
执行上下文为执行时所处的执行上下文,具有和这个执行上文相同的作用域eval
执行上下文为全局执行上下文function foo () {
var x = 2, y = 4
console.log(eval('x + y')) // 直接调用,执行上下文为当前函数执行上下文,结果是 6
var geval = eval // 等价于在全局作用域调用
console.log(geval('x + y')) // 间接调用,执行上下文为全局执行上下文,x is not defined,实际上y也是not defined
console.log(window.eval('x + y')) // 这也是间接调用
}
foo()
JS通过执行上下文栈
来管理上述这些执行上下文
。
globalContext
表示它,并且只有当整个应用程序结束的时候,ECStack
才会被清空,所以程序结束之前,ECStack
最底部永远有个globalContext
globalContext
function foo (a) {
console.log(a)
}
function bar (b) {
foo(b)
}
bar('hehe')
变量对象
(variable object, VO):每个执行上下文都一个与之对应的变量对象,它是与执行上下文相关的数据作用域,存储了在上下文中的函数标识符、形参、变量声明等。但在规范上或者引擎实现上,这个对象是不能在JS环境中访问的激活对象
(activation object, AO):当进入某个函数执行上下文中
时,其对应变量对象会被激活,变量对象上的属性才能被访问,所以称之为激活对象。
激活对象
就是在函数执行上下文中被激活成可访问的变量对象
。
根据词法环境规范定义:
- A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
- 词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由
环境记录
(environment record)和可能为空引用(null)的外部词法环境
组成。
环境记录
:主要是声明性环境记录
(declarative Environment Records)和对象环境记录
(object Environment Records),其次还有全局环境记录
(global Environment Records)和函数环境记录
(function Environment Records)。
声明性环境记录
(declarative Environment Records):存储变量、函数和参数, 用于函数声明、变量声明和catch语句。
对象环境记录
(object Environment Records):用于像with这样绑定对象标识符(作用域)的语句。
全局环境记录
和函数环境记录
:是特殊的声明性环境记录,形式上可理解为对应的变量对象。
外部词法环境
:构成作用域链的关键
- 词法作用域(静态作用域):函数的作用域在函数定义的时候就决定了。JS使用的是词法作用域。
- 动态作用域:函数的作用域是在函数调用的时候才决定的。
作用域(Scope)用于规定如何查找变量,也就是确定当前执行上下文中对变量的访问权限。
在函数中有一个内部属性,当函数创建的时候,就会保存所有父变量对象到其中,在查找变量值的时候,会先从[[scope]]
顶部即当前上下文的变量对象(作用域)中查找,如果没有找到,就会根据当前执行上下文中的[[scope]]
对外部执行环境的引用顺序,从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
。
注意
:
- 当进入函数执行上下文(函数激活)时,会将该函数的变量对象推入到作用域链前端。
- 正式由于作用域与作用域链的这种关系,在当前函数执行上下文的活动对象中必定存在this和arguments,所以this和arguments的搜索在当前执行执行上下文就停止了。
回过头来看前言中的问题,按照下面的流程进行(只列出关键部分):
ECStack = [
globalContext
]
globalContext = {
VO: [global], // 指向全局对象
Scope: [globalContext.VO], // 可访问权限
this: globalContext.VO
}
foo
函数和bar
函数被创建,生成内部作用域链。foo.[[scope]] = [
globalContext.VO
]
bar.[[scope]] = [
globalContext.VO
]
2.2节
中所述,执行bar
函数前,创建bar
函数执行上下文,并推入执行上下文栈中。ECStack = [
barContext,
globalContext
]
bar
函数执行上下文barContext = {
AO: {
arguments: {
length: 0
},
a: undefined,
foo:
},
Scope: [barContext.AO, globalContext.VO],
this: undefined
}
bar
函数执行,开始执行foo
函数,同理,创建foo
函数执行上下文,并推入执行上下文栈中ECStack = [
fooContext,
barContext,
globalContext
]
foo
函数执行上下文fooContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [fooContext.AO, globalContext.VO],
this: undefined
}
foo
函数执行,foo
函数执行上下文中的激活对象没有属性a
,所以沿着作用域链[[scope]]
找到全局执行上下文中的变量对象,其指向全局对象,故输出'heihei'
。执行完毕弹出foo
函数执行上下文并销毁。ECStack = [
barContext,
globalContext
]
bar
函数执行,bar
函数执行上下文中的激活对象没有属性b
,所以沿着作用域链[[scope]]
找到全局执行上下文中的变量对象,其指向全局对象,故输出'xixi'
。执行完毕弹出bar
函数执行上下文并销毁。ECStack = [
globalContext
]