作用域
- 作用域是一个变量区域
- 作用域决定变量的访问权限,也规定了查找变量的方法
根据查找变量的方法,可以分为词法作用域(静态作用域)和动态作用域
js采用的是静态作用域
静态作用域(词法作用域)和动态作用域
- 静态作用域:函数的作用域在函数定义的时候就决定了
- 动态作用域:函数的作用于在函数调用的时候决定
var value = 1;
function foo(){
console.log(value);
}
function bar(){
var value = 2;
foo();
}
bar();
静态作用域:执行foo -> 从foo内部查找是否有value ->没有则根据函数定义的位置,查找上一层作用域->查找到value为1
动态作用域:执行foo -> 从foo内部查找是否有变量value -> 没有则根据当前调用foo的外层作用域,也就是bar -> 查找到bar的作用域value为2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
-------------------------------------------------
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
由于js是静态作用域,以上两段代码输出都是local scope(根据f函数定义的位置)
静态作用域和闭包
静态作用域,意味着函数对象的内部状态不仅包含函数逻辑的代码,还包含当前作用域链的引用。
严格意义上来说,所有JS函数都是闭包,他们都是对象,都包含关联到他们的作用于,形成一个所谓的闭包,这样外部函数就无法访问内部变量。
在JS中,我们说的闭包指的是让外部函数访问到其内部的变量,按照一般的做法,是使内部函数返回一个函数,然后操作其中的变量。这样做的话,一是可以读取到函数内部的变量,二是可以让这些变脸搞得值始终保存在内存中。
执行上下文
Js引擎线程创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
假设ECStack为执行上下文栈,JS开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文。
ECStack = [ globalContext ]
假设有如下代码:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕,就会将函数的执行上下文从栈中弹出:
ECStack.push(<fun1> functionContext)
ECStack.push(<fun2> functionContext)
ECStack.push(<fun3> functionContext)
ECStack.pop()
ECStack.pop()
ECStack.pop()
闭包中的执行上下文栈
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
-------------------------------------------------
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
上面两段代码的输出都是local scope,但他们的差别在于执行上下文栈的变化不一样:
//第一段代码
ECStack.push(<checkscope> functionContext)
ECStack.push(<f> functionContext)
ECStack.pop()
ECStack.pop()
//第二段代码
ECStack.push(<checkScope> functionContext)
//checkScope执行完毕,f未执行
ECStack.pop()
//f被执行
ECStack.push(<f> function Context)
ECStack.pop()
执行上下文的属性
当JS代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable Object, VO)
- 作用域链(Scope Chain)
- this
变量对象(变量存储在哪里)
变量对象与指向上下文相关的数据作用域,存储了在上下文中定义的 变量和函数声明
变量对象可以理解为一个对象,但内部存储的都是变量
分为全局上下文的变量对象和函数上下文的变量对象
全局上下文变量对象
- 全局上下文中的变量对象是全局对象
- 全局对象是预定义的对象,可以访问所有预定义的对象、函数和属性
- 在顶层js代码中,可以用关键字this引用全局对象。因为全局对象是作用域链的头。
可以通过this引用,在浏览器环境下,全局对象就是Window对象
console.log(this)
全局对象是Object的一个实例
this instanceof Object
预定义的对象、函数
Math.random()
this.Math.random()
全局变量的宿主
var a = 1
console.log(this.a)
console.log(window.a)
函数上下文变量对象
函数上下文,用活动对象activation object,AO来表示变量对象
为什么叫活动对象?
- 不可在JS环境中被访问,只有进入执行上下文中,这个变量对象才会被激活
- 只有被激活的变量对象,才能够被访问
- 活动对象是进入函数上下文时被创建的,通过函数的arguments属性初始化,arguments属性值是Arguments对象
执行上下文的代码会被分成两个阶段进行处理:分析和执行
- 进入执行上下文,变量对象会包括
- 代码执行
进入执行上下文,变量对象会包括:
- 函数的所有形参
- 由名称和对应值组成
- 如果没有值,属性值被设为undefined
- 函数声明
- 由名称和对应的函数对象组成
- 如果变量对象已经存在相同的名称属性,则覆盖
- 变量声明
- 由名称和对应值组成(未被执行所以是undefined)
- 如果变量名称跟已经声明的形式参数或者函数相同,这个变量生命不会干扰已经存在的属性。
即变量对象会通过Arguments构造函数进行初始化,对函数所有形参进行初始化,如果这些形参没有值,则设为Undefined,函数声明的优先级是最高的,会覆盖形参和变量声明,变量声明的优先级最低,不会覆盖函数生命和形参声明
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在进入执行上下文时,代码还没有被执行,此时的AO为:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
//函数变量提升
c: reference to function c(){},
//函数表达式,没有被提升
d: undefined
}
代码执行阶段,此时AO为:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
//函数表达式
d: reference to FunctionExpression "d"
}
- 全局上下文的变量对象初始化是全局对象
- 函数上下文的变量对象初始化只包括Arguments对象
- 进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
- 在代码执行阶段,会再次修改变量对象的属性值
变量对象思考题
function foo() {
console.log(a);
//赋予全局对象
a = 1;
}
//Uncaught ReferenceError: a is not defined 非严格模式下输出undefined
foo(); // ???
------------------------
function bar() {
a = 1;
console.log(a);
}
//输出 a = 1
bar(); // ???
第一段代码由于AO里面没有,从全局对象找也没有,所以报错
AO = {
arguments:{
length:0
}
}
第二段代码AO里面没有,但是全局对象找有,所以返回1
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
会打印函数,而不是undefined,因为执行console.log这行代码的时候AO:
AO = {
arguments:{
length:0
},
foo:reference to function foo()
}
在分析阶段,如果没有function foo()的声明,根据var foo =1 会将foo设置为undefined,但有foo的函数声明时,变量声明不能覆盖函数声明,所以执行到console.log()时,foo还未被替代。
console.log(foo);
var foo = 1;
上面的代码会输出undefined,而不是报错,这是由于在代码分析阶段,赋值了变量声明为undefined
console.log(foo);
上面的代码会输出“Uncaught ReferenceError: foo is not defined”,这是由于在代码分析阶段,foo没有变量声明。
作用域链
作用域链是由多个执行上下文的变量对象构成的链表。
当查找变量的时候,会先从当前上下文中的变量对象中查找,如果没有找到,就会从父级(静态作用域层面上的父级,在定义时就决定)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。
作用域链的创建和变化可以从两个时期来讲解:
- 函数的创建
- 函数的激活
函数创建
函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,可以理解[[scope]]就是所有父变量对象的层级链。
function foo(){
function bar(){
}
}
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
]
//bar被创建时 foo已经执行,所以有AO
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
]
函数激活
函数激活时,会将激活时的变量对象添加到作用域的前端。
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
- checkscope函数被创建,保存作用域到内部属性[[scope]]
checkscope.[[scope]] = [globalContext.VO]
- 执行checkscope函数,创建checkscope函数执行上下文,checkscope函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
]
- checkscope函数不立即执行,复制函数[[scope]]属性创建作用域
checkscopeContext = {
Scope:checkscope.[[scope]]
}
- 第二步,用Arguments初始化活动对象AO,加入形参声明、函数声明、变量声明
checkscopeContext = {
AO:{
arguments:{
length:0
},
scope2:undefined
}
}
- 第三步:将活动对象压入checkscope作用域链顶端
checkscopeContext = {
AO:{
arguments:{
length:0
},
scope2:undefined
},
Scope:[AO,[[Scope]]]
}
- 执行函数,随着函数执行,修改AO的属性值
checkScopeContext = {
AO:{
arguments:{
length:0
},
scope2:'local scope'
},
Scope:[AO,[[Scope]]]
}
- 函数执行完毕,函数上下文从上下文执行栈中弹出
ECStack = [
globalContext
]
- 函数创建时,其父级变量对象就被保存到属性[[Scope]]中,这一点是根据静态作用域决定的
- 执行上下文是在函数被执行的时候创建的,在创建执行上下文的同时,首先初始化作用域链为当前的[[Scope]]属性
- 在函数分析阶段,初始化函数的变量对象AO,然后将其推入到作用域链的顶部
- 在函数被执行的过程中,逐步完善AO
分析另外一段代码:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
- 执行全局代码,推入全局上下文到执行上下文栈
globalContext = {
VO:[global,scope,checkcope],
Scope:[globalContext.VO],
this:globalContext.VO
}
ECStack = [ globalContext ]
- checkscope被创建,赋值checkscope.[[scope]]
checkscope.[[scope]] = [...globalContext.Scope]
- checkscope被执行,推入checkscope上下文
ECStack.push(checkscopeContext)
- 将[[scope]]赋值给Scope作用域链
checkcopeContext = {
Scope:[...[[scope]]]
}
- checkscope函数分析阶段,用arguments构造函数初始化变量对象,并压入作用域链顶部
checkscopeContext = {
AO:{
arguments:{
length:0
},
scope:undefined,
f:reference to f(){}
},
Scope:[AO,...[[scope]]],
this:undefined
}
- f函数被创建,初始化f.[[scope]]为父级变量对象
f.[[scope]] = [...checkscopeContext.Scope]
- f函数被执行,推入f函数的执行上下文,并初始化f函数的执行上下文
fContext = {
AO:{
arguments:{
length:0
}
},
Scope:[AO,...[[scope]]]
}
ECStack.push(fContext)
- f函数执行,随着作用域链寻找scope变量,之后函数相继弹出执行上下文栈