浅读js作用域链

323 阅读6分钟

对作用域链的理解

课本上的解释是:在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。

那作用域链到底具体是什么样的呢?让我们一起来看看吧

首先呢,我们要插入一个预编译的环节

2.png

预编译详解

(一) JavaScript编译过程的三个步骤: 1. 语法分析 2. 预编译 3. 解释执行

(二) 预编译概述

JavaScript预编译发生在代码片段执行前的很短时间内(时间可以忽略不计但又存在),预编译分为两种,一种是函数内部预编译,另一种是全局预编译,全局预编译发生在页面加载完成时执行,函数预编译发生在函数执行的前一刻。预编译会创建当前环境的执行上下文。(后文的作用域链会提及 执行上下文 注意注意哟!!)

(三) 函数的预编译执行四部曲

  1. 创建AO(Activation Object)对象;
  2. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为underfined;
  3. 将实参和形参值统一;
  4. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。

代码示例

//请问下面的console.log()输出什么?
function fn(a) { 
  console.log(a);   // 1
  var a = 123  //变量赋值 
  console.log(a);   // 123
  function a() { }  //函数声明
  console.log(a);   // 123
  var b = function () {   }//变量赋值(函数表达式)
  console.log(b);   //  function (){}
  function d() { }  //函数声明 
} 
fn(1)  //函数调用

接下来,就让我们根据 函数的预编译执行四部曲 通过AO的变化 一起来解答四个console.log()的输出值吧!

1.创建AO对象

AO{    
        //  空对象
}

2.找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined;

AO{
    a: undefined   //obj不能同时出现相同的key值 否则下面会覆盖上面的
    b: undefined
}

3.将实参和形参值统一;

AO{
    a: 1,
    b: undefined
}

4.在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。

AO{
    a: function(){}
    b: undefined
    d: function(){}
}

下面是完整的预编译过程

AO:{
a:undefined  1  function a(){}
b:undefined  function (){}
d:function d(){}
}

5.预编译结束,最后,代码开始执行,执行结果 在代码示例里哦

(四) 全局的预编译执行三部曲

  1. 创建 GO 对象
  2. 找全局变量声明,将变量声明作为AO的属性名,值为undefined
  3. 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体

代码示例

global = 100;
function fn() {
  console.log(global);  // 
  global = 200;
  console.log(global);   // 
  var global = 300;
}
fn();

根据全局预编译三部曲我们同样可以知道他的GO变化过程

1.创建GO对象

GO{
    // 空对象
}
  1. 找形参和变量声明,将变量声明和形参作为GO的属性名,值为underfined
GO: {
  global: undefined
}
  1. 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体
GO: {
  global: undefined
  fn: function() { }
}

只要存在函数声明就会带来函数自己的AO,预编译过程(多层嵌套情况同样适用)继续套用四部曲即可

作用域链

接下来便是主场show了,首先来了解几个概念名词吧,它们对作用域链的理解起重要作用哦

执行期上下文

当函数执行的时候,会创建一个称为执行期上下文的内部对象(AO对象)。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁

注意哦:函数在执行时会创建一个内部对象(AO对象),并且在执行完毕后会被销毁

查找变量

从作用域的顶端依次往下查找。当找到了第一个就会停止查找(遮蔽效应)

[[scope]]

函数的作用域,是不可访问的,其中存储了运行期上下文的集合

作用域链

[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链

当然,单纯看概念是很抽象的,那我们就来举举栗子吧

  //函数是对象,是对象就可以具备属性
  function test(){
  
  } 
  //test.name
  //test.prototype  //  原型(原型属于后面es6的内容,就是个概念,这里就不深究啦)
  //test.[[scope]]  //  作用域属性 隐式属性
  
  test() ---> AO:{}  // 函数执行之后,那么这个函数的AO对象就会被回收
  test() ---> AO:{}   // 再次创建,执行之后会被再次回收

接着再看看下面的代码和拆分详谈,思路会更加清晰

function a(){
    function b(){
        var b= 222
    }
    var a = 111
    b()
    console.log(a)
} 
var glob = 100
a()

function a(){}

a的定义给我们带来了a.[[scope]]

var global = 100

全局代码的执行会创建 GO{} 此时a.[[scope]] --->0:GO{}

此时a可以访问GO{}

a()

a的执行会创建了AO{} 此时a.[[scope]] --->0:AO{} 1:GO{}

注意:后创建的会放在前面,在作用域里面的排列方式,此时在a的作用域里既可以访问到AO还可以访问到GO

为了理解的更透彻,我们可以采用画图+文字的方法来描述

function a(){
    function b(){
        var b= 222
    }
    var a = 111
    b()
    console.log(a)
} 
var glob = 100
a()

000.jpg

a函数的定义,带来了a自己的作用域,在a函数作用域里,a函数的执行过程如上表

666.jpg

再次强调哦,谁后创建,谁就放在前面 a的执行带来了b的定义

// a定义 a.[[scope]] --->0:GO{} 
// a执行 a.[[scope]] --->0:AO{}  1:GO{}  // a的作用域链
// a的执行带来了b的定义
// b定义 b.[[scope]] --->0:bAO{} 1:aAO{} 2:GO{}   //  b的作用域链

为什么函数体外的作用域访问不到函数体内的作用域,而函数体内的作用域可以访问到函数体外的作用域的本质原因

答:因为函数的AO对象会在函数的执行完毕回收。函数b在调用结束之后,函数b的AO回收了,但是此时函数a未必执行完毕,如果函数a想要访问b的数据,极大可能会出现BUG,所以JavaScript这门语言一般不允许外层函数访问内层数据