我们今天先从概念入手,然后在通过一道例题,完完整整的了解代码执行的过程,在这个过程中会用到作用域和作用域链查找机制;我们在根据概念的带入详细梳理;
思维导图
一、作用域的概念
在某一个上下文中创建函数,除了开辟内存和赋值之外,还会给当前函数设置一个作用域属性
[[scope]]
:
- 当前函数
[[scope]]
= 当前函数创建时候所在的上下文 ;
简单来说:当前函数的作用域取决于当前函数创建时候的上下文,它在哪个上下文创建的,那它的作用域就是谁
二、全局变量 VS 私有变量
- 全局变量:在全局上下文
EC(G)
中的全局变量对象VO(G)
中,存储的变量 - 私有变量:在函数执行形成的私有上下文
EC(XXX)
中的变量对象AO(XXX)
中,存储的变量;
都有哪些是私有变量呢:
- 当前函数执行形成的上下文中:声明过的变量或者函数,都会存储到
AO(XXX)
中,- 函数定义的形参变量,也会存储到当前上下文的
AO(XXX)
中;
三、作用域链查找机制
1、作用域链scopeChain
的概念
作用域链scopeChain
里面包含了:
- 当前形成的上下文,
- 以及当前函数所对应的
scope
作用域;
scopeChain
:<当前EC,函数[[scope]]>
;
以此来建立链式关系,之后我们在查找变量的时候,就按照这个链式关系找(先找自己上下文的,自己没有,按照作用域链向上级作用域找)
2、作用域链的形成
作用域链是在函数执行的时候形成的;执行函数的具体步骤为:
- 创建私有上下文EC(有存放私有变量的变量对象AO)
- 进栈执行(时会把全局上下文压缩到底部)
- 初始化作用域链
scopeChain
:<当前EC,函数[[scope]]>- 初始化
THIS
指向- 形参赋值(包含初始化
ARGUMENTS
)- 变量提升
- ……这里我们省略了一些
- 代码执行
- 执行完可能会出栈(也可能不出栈)
当函数执行的时候除了按照我们上面的,还有很多很多操作;
3、作用域链的查找机制
- 在当前上下文中,代码执行的过程中遇到一个变量时:
首先看它是否是私有的
1.如果是私有的,接下来的所有操作,都是操作自己的,和别人没有关系;
2.如果不是私有的,则按照
scopeChain
作用域链进行查找,在哪个上下文中找到,当前变量就是谁的……一直找到全局上下文为止
如果找到
EC(G)
都找不到:
- 是获取变量值就会报错,
- 是设置值,相当于给
GO
加属性
四、以题为例:详细解析
1、一个小知识点:var a = b = 12 ;
和 var a = 12, b = 12 ;
的区别
在此之前我们先了解一个零散的小知识点:
var a = b = 12 ;
和var a = 12, b = 12 ;
的区别
var a = b = 12;
//=> 相当于:var a = 12; b = 12; 只有第一个带VAR,第二个不带VAR
var a=12, b=12;
//=> 相当于var a = 12; var b = 12; 连续创建多个变量,可以使用逗号分隔;
复制代码
2、例题按步骤详解
好了,接下来开始我们的正式内容;
console.log(a, b);
var a = 12,
b = 12;
function fn() {
console.log(a, b);
var a = b = 13;
console.log(a, b);
}
fn();
console.log(a, b);
复制代码
此题的操作过程:
第一步:形成执行环境栈ECStack
第二步:形成一个全局上下文EC(G)
第三步:形成全局变量对象VO(G)
第四步:进栈执行
……:这里我们省略一些我们暂时用不到的内容
第五步:变量提升;
var a
;var b
;function fn(){...}
; (这里声明+定义fn )
- 开辟堆内存,生成一个空间地址(这里我们假设堆的地址为
AAAFFF000
),把函数当作字符串存进堆中; - 与此同时,在创建函数的时候还会给函数加一个
scope
作用域属性
- 开辟堆内存,生成一个空间地址(这里我们假设堆的地址为
- 作用域属性是指:当前函数是在哪个上下文中创建的,那它的作用域就是哪个上下文
- 所以本题的
fn[[scope]] = EC(G)
;
- 把堆的地址
AAAFFF000
,当作值与fn
变量关联在一起;
- 把堆的地址
第六步:代码执行;
- 1.
console.log(a, b);
=>undefined
/undefined
; - 2.
var a = 12 ;
- 1.
- 首先创建值
12
; - (此时
var a
操作已经在变量提升阶段完成,所以直接跳过); - 把变量
a
与12
关联;
- 首先创建值
- 3.
var b = 12 ;
=> 同上面var a = 12
一样; - 4.
function fn(){...};
=> (变量提升阶段已经完成,直接跳过); - 5.
fn() ;
=> 让fn
函数执行;
- 3.
- 形成一个新的私有的执行上下文(我们把它命名为
EC(fn1)
); - 形成一个私有变量对象
AO
(我们把它命名为AO(fn1)
); EC(G)
被压缩到ECStack
底部,同时EC(fn1)
进栈执行 ;- 此时是函数执行过程,也是我们真正要讲解内容的开始,我们按步骤命名 ;函数体中在代码执行之前,要做很多很多的事情,我们这里省略不需要的内容
- 形成一个新的私有的执行上下文(我们把它命名为
- 第一步:初始化作用域链:
scopeChain
:<当前EC,函数[[scope]]>
这里的fn[[scope]]
我们在函数创建时,已经知道是EC(G)
,所以最终的作用域链为=>scopeChain
:<EC(fn1),EC(G)>
;
- ……
- 第二步:变量提升;
var a
;
- 第三步:代码执行;
console.log(a, b);
=> 此时就按照作用域链查找,
- 此时
a
已经存在,只不过没有赋值,所以a
是自己的私有属性,直接操作自己的即可;=>undefined
b
不是自己的私有属性,通过作用域链查找,找到EC(G)
中的b
,此后操作的都是EC(G)
中的b
; =>12
- 此时
var a = 13 ;
=> 创建值13
,var a
跳过,a
与13
关联;b = 13 ;
=> 通过作用域链找到全局的b
;给b
重新赋值13
;console.log(a, b);
=>13
/13
;- 此时函数体中代码以全部执行完。
- 第四步:出栈销毁,同时全局上下文恢复到原来位置,继续执行。
- 6.
console.log(a, b);
=> 此时打印全局下的a
,b
;
- 6.
a
=> 12 ;b
=> 13 ;
- 执行完成
3、例题图解析
五、补充几道练习题
1.写出下面代码输出的结果(本题我们画图解析)
console.log(a, b, c);//=> undefined undefined undefined
var a = 12,
b = 13,
c = 14;
function fn(a) {
console.log(a, b, c);//=> 10 , 13 , 14
a = 100;
c = 200;
console.log(a, b, c);//=> 100 , 13 ,200
}
b = fn(10);
console.log(a, b, c);//=> 100 undefined 200
复制代码
2.写出下面代码输出的结果(本题我们画图解析)
var ary = [12, 23];
function fn(ary) {
console.log(ary);//=> [12,23]
ary[0] = 100;
ary = [100];
ary[0] = 0;
console.log(ary);//=> [0]
}
fn(ary);
console.log(ary);//=> [100,23]
复制代码
3.写出下面代码输出的结果(本题我们画图解析)
var a=1;
var obj ={
"name":"tom"
}
function fn(){
var a2 = a;
obj2 = obj;
a2 =a;
obj2.name =”jack”;
}
fn();
console.log(a);//=> 1
console.log(obj);//=> {"name":"jack"}
复制代码
4.写出下面代码输出的结果(本题我们画图解析)
var i = 20;
function fn() {
i -= 2;
var i = 10;
return function (n) {
console.log((++i) - n);
}
}
var f = fn();
f(1);//=>10
f(2);//=>10
fn()(3);//=>8
fn()(4);//=>7
f(5);//=>8
console.log(i);//=>20
复制代码
5.写出下面代码输出的结果(本题我们画图解析)
//========有形参=======
let x = 5;
function fn(x) {
return function(y) {
console.log(y + (++x));
}
}
let f = fn(6);
f(7);//=>14
fn(8)(9);//=>18
f(10);//=>18
console.log(x);
//========无形参==========
let x = 5;
function fn() {
return function(y) {
console.log(y + (++x));
}
}
let f = fn(6);
f(7);//=>13
fn(8)(9);//=>16
f(10);//=>18
console.log(x);
复制代码
- 有形参
x
的情况 - 无形参的情况