作用域和作用域链查找机制|内附思维导图

2,528 阅读7分钟

我们今天先从概念入手,然后在通过一道例题,完完整整的了解代码执行的过程,在这个过程中会用到作用域和作用域链查找机制;我们在根据概念的带入详细梳理;

思维导图

一、作用域的概念

在某一个上下文中创建函数,除了开辟内存和赋值之外,还会给当前函数设置一个作用域属性[[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 ;
      • 首先创建值12;
      • (此时var a 操作已经在变量提升阶段完成,所以直接跳过);
      • 把变量a12关联;
    • 3.var b = 12 ; => 同上面var a = 12一样;
    • 4.function fn(){...}; => (变量提升阶段已经完成,直接跳过);
    • 5.fn() ; => 让fn函数执行;
      • 形成一个新的私有的执行上下文(我们把它命名为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 ; => 创建值13var a跳过,a13关联;
        • b = 13 ; => 通过作用域链找到全局的b;给b重新赋值13;
        • console.log(a, b); => 13 / 13;
        • 此时函数体中代码以全部执行完。
      • 第四步:出栈销毁,同时全局上下文恢复到原来位置,继续执行。
    • 6.console.log(a, b); => 此时打印全局下的a,b;
      • 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的情况
  • 无形参的情况