作用域链!再不会就不礼貌了!

399 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

今天努力肝爆,明天躺平睡觉!------弗朗西西斯·航

前言

我们都知道在JavaScript里面,函数体可以访问全局变量而全局变量却不能访问函数体作用域,作为菜鸟小白我一直对此一直一知半解,经过今天的学习,终于搞懂了里面的原理,爆肝出这篇文章,希望能够帮助到一些正在学习js的同学。

那么、那么现在就开始了

作用域

1.什么是作用域(scope)

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。也就是说,作用域决定了代码区块中变量和其他资源的可见性。我们都知道函数是一个对象,那么对象就会出场自带一些属性,而函数的[[scope]]属性也是函数自带的一个属性,并且这个属性也被定义为不可访问的,我们也称这个属性为隐式属性。

在讨论为什么函数体可以访问全局变量而全局变量却不能访问函数体作用域这个问题前,为了让大家有一个更好的理解,我们先来了解一下预编译。

2.预编译

在JavaScript里面,函数在被执行前的很短一段时间内(通常是几微秒)才会被引擎预编译,此时函数将会产生一个AO(Activation Object)对象,等这个函数执行完成后,这个AO对象将会被回收。此时我们如果再次的调用这个函数,这个函数也会跟前面讲的一样:再次的在函数执行前的极短一段时间内被预编译,产生一个AO对象,等函数执行完后,第二次产生的AO对象将会被再次回收。

1.发生在函数作用域内的预编译

在这里我将使用一个例子来让大家更好的理解编译器是怎么在函数的作用域里面进行编译的。

  function fn(a){
  console.log(a);
  var a=111;
  console.log(a);
  function a(){};
  console.log(a);
  var b=function(){};
  console.log(b);
  function d(){};
  var d=a;
  console.log(a);
}
fn(1)

当引擎处理到fn(1)这个语句的时候,这个函数才开始进行预编译,那么在编译的过程中整个引擎是怎么处理这个函数的呢?其过程一共可以分为四步:

1. 首先创建一个AO对象
2. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined
3. 将实参和形参统一
4. 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予为函数体     

现在假装我们自己就是引擎,模拟一下引擎来对上面的函数进行预处理

首先创建一个AO对象:为了好理解我们以代码的形式呈现给大家

AO:{
    }

然后就是找到这个函数的形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined

AO:{
  a:undefined  //下面的var a 与形参a重名了,所以var a所产生的a:undefined与上面的重复,编译器只保留一个
  b:undefined
  d:undefined
  }

第三步:将实参和形参统一

//实参为1,所以a的值由undefined更改为1
AO:{
    a:1
    b:undefined
    d:undefined
    }

第四步:找到函数声明,赋函数体给值

AO:{
    //里面有个a函数与d函数的声明,此时a的值由1改为函数function(){}
    a:function(){}
    b:undefined
    //同理d也变为function(){}
    d:function(){}   
   }

那么到这里我们引擎就编译完成了。接下来就是对其赋值,按照从上到下的顺序依次进行赋值输出操作。

2.发生在全局作用域的预编译

我们可以将其分为三部步:
1. 创建GO(Global Object)对象
2. 找全局变量声明,将变量声明作为GO的属性名,值为undefined
3. 在全局找函数声明,将函数名作为GO对象的属性名,值赋予为函数值
<script>
var global=100
function fn(){
console.log(global);
}
fn()
</script>

同理:

首先创建一个GO对象
GO:{
   }
第二步:找出全局变量并且赋值为undefined
GO:{
    globalundefined
    }
第三步:在全局找函数声明,将函数名作为GO对象的属性名,值赋予为函数值
GO:{
    globalundefined
    fn:function(){}
    }

3.作用域链

现在我们已经对这两种预编译原理有了一些了解,现在我们引用实例:

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

对于上面的代码,引擎拿到了这段代码之后会先进行全局的预编译再全局执行,全局执行之后就调用了函数a(),在调用的前一刻开始对函数a()进行预编译。(下面就不再分析预编译是怎样进行的,不懂的同学可以返回上面看看)
现在对上面代码分析:首先定义了一个函数a(),那么此时就会给我们带来一个a的作用域a.[[scope]](这个是函数天生自带的属性),定义完成后引擎接着往下识别发现了一个全局变量var glob,那么此时会进行全局代码的执行,在执行的前一刻,我们引擎会生成一个GO{};

//识别到a() 定义一个 a.[[scope]] --->0:GO{}     //在作用域里面有一个GO{}对象,
这里的0与数组里下标为0的意思一样,指第一个元素

接着开始执行a(),在执行前又开始预编译,生成一个AO{}

//a()执行 a.[[scope]] --->0:AO{} 1:GO{}   // 在作用域里面后创建的元素会排在前面,此时AO在链顶端

看图!

QQ图片20220704232525.jpg a()的执行生成了一个AO对象,在这个对象里面有一个b函数,在b函数执行前也会进行预编译,此时会生成b函数自己的AO对象。那么此时b函数产生的AO对象又会在scope chain顶端,此时再看图!

// b()定义 b.[[scope]] --->0:bAO{} 1: aAO{} 2:GO{}
//此时b产生的AO会在链顶端

** QQ图片20220704233900.jpg 所以总作用域链如上;

那么问题来了,为什么函数体外的访问不到函数体内的?

如图,由上面的例子我们可以知道函数a执行的时候带来了函数b的创建(看图一),函数a的AO对象里有函数b的属性;函数a执行的时候会带来bAO的创建,如果函数a执行完毕,函数a的aAO会被回收掉,那么此时在它里面的函数b的bAO也会被一起回收掉;相反如果函数b调用执行完毕,那么函数b的bAO也会被回收掉,但是我们想清楚来,此时如果函数b结束了,并不代表函数a会被执行结束,所以此时函数a的aAO不会马上被回收。那么关键就来了当函数b执行完后,函数b的bAO被回收了,请记住此时函数a的aAO没有马上被回收,那么bAO已经没了(函数b的作用域没了),所以b函数外面的就不能访问函数b。如果函数b的外层函数a的作用域还能对函数b发生作用,那么对不存在的东西进行操作就不可能。

这是本人对作用域的一小点理解,如果存在不正确的地方,欢迎大家斧正。