var a = 'heihei', b = 'xixi'
function foo () {
console.log(a)
}
function bar () {
var a = 'houhou'
foo()
console.log(b)
}
bar()
// heihei
// xixi
复制代码
若是您很快就能得出上述结果,那相信您的功底很是之扎实,若是没法肯定,那么这篇文章通读以后,相信能够帮您解疑。javascript
JS中可执行代码有三种:全局代码
,函数代码
,eval代码
。代码执行前须要准备的执行环境
也称为执行上下文
,因此也分为全局执行上下文
,函数执行上下文
,eval执行上下文
。html
全局执行上下文中的变量对象就是全局对象,预置了不少属性和函数。在浏览器中是window
,在NodeJS中是global
前端
[[scope]]
属性建立做用域链(下面会讲到)arguments
建立活动对象
变量对象
被激活为激活对象
,此时发生"hoist"
声明提高java
函数的全部形参git
arguments
,其值也是一个对象(类数组对象,有length
属性),按形参顺序赋值,值为实参undefined
函数声明github
Function
对象的引用。变量声明segmentfault
undefined
)组成一个变量对象的属性被建立;
注意
: 整个过程能够大概描述成:函数的形参=>函数声明=>变量声明
, 其中在建立函数声明时,若是名字存在,则会被重写,在建立变量时,若是变量名存在,则忽略不会进行任何操做。数组
根据代码修改激活对象对象中对应的值。若是当前执行上下文中的变量对象没有该属性,就去父级的执行上下文变量对象中寻找,直至到全局执行上下文。找不到就报错浏览器
eval执行上下文比较特殊,它取决于eval函数是直接调用仍是间接调用。 引用MDN上的说法:bash
若是间接的使用
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: <reference to function 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
]
复制代码