原生js:作用域和作用域链

96 阅读5分钟

一、作用域(Scope)

1、什么叫作用域

作用域就是一个变量一个函数在什么范围内可用

2、作用域的作用

隔离变量,不同作用域下的同名变量不会冲突

3、词法作用域

词法作用域也叫静态作用域,意思是变量或函数的作用域在变量声明时确定

示例:

      function test(fn) {
        const a = 1
        fn()
      }
      const a = 2
      function fn() {
        console.log(a)
      }
      test(fn) // 2

声明fn时,a变量为2,调用fn时,a变量为1,fn中a的值为2。因为js中采用的是词法作用域。

4、作用域的分类:

类型概念特点
全局作用域声明在最外层的变量或函数全局都可以使用
局部作用域也叫函数作用域,在函数内创建的变量只能在函数内使用,函数外是无法访问的(闭包会可能会导致AO被保持,这是一个特殊情况)
块级作用域ES6的let和const创建的变量具有块级作用域①只能在当前 {} 中访问 ②变量不会提示 ③不能反复声明
动态作用域call/apply/bind改变this指向动态地改变this的指向

注意:

暗示全局变量:未定义直接赋值的变量,实质上是为window添加了一个属性,它也是一种定义全局作用域的方式

      function fn() {
        a = 1
      }
      fn()
      console.log(a)

5、作用域与执行上下文(GO/AO)的区别

js属于解释型语言,js的执行分为:解释和执行两个阶段

  1. 解释阶段:词法分析、语法分析、作用域规则确定
  2. 执行阶段:创建执行上下文、执行函数代码、垃圾回收

执行阶段是在解释阶段没有问题的前提下开始,所以执行上下文是在作用域的范围内创建的,它们不是一回事,并且有着以下的区别:

区别作用域执行上下文
1. 确定时机解释阶段:在函数声明时作用域就确定了执行阶段:①GO在全局作用域确定后创建②AO在函数调用时,执行函数体代码前创建
2. 是否可变js采用词法作用域,也叫静态作用域,一旦确定,不会改变预编译期间,GO/AO随时可变(this的指向是调用时决定的)
3. 销毁时机不会销毁函数调用完释放AO,页面关闭时释放GO

6、自由变量

在函数中,需要跨越当前作用域才能访问到的变量,叫做自由变量

      var a = 10
      function fn() {
        console.log(a) // a就是自由变量
      }
      fn()

二、作用域链([[Scopes]])

1、什么叫作用域链

函数在声明时,系统生成的一个隐式属性([[Scopes]]),它是一个存储作用域链的容器,里面存放的是GO和AO对象,用户访问不到,由js引擎来访问。

查找变量时,从[[Scopes]]顶端开始向下查找,一直找到GO,还是没找到就报错(Uncaught ReferenceError: a is not defined),这种一层一层的关系,就是作用域链。

image.png

2、函数在声明到执行过程中,[[Scopes]]的变化

  1. 函数在声明时,已经生成了作用域和作用域链,GO被存储在第0位;函数在调用时(前一刻),生成自己的AO排在顶端,将GO挤到第1位
  2. 函数在声明时,它的作用域链就是上级函数调用时的作用域链;当函数调用时(前一刻),比上级多一个自己的AO,并且永远都是自己的AO排在第0位,旧的AO和GO依次向下排列,查找时从作用域链顶端向下查找
  3. 函数执行完,对应的AO就被销毁,所以多次调用函数会多次生成AO,AO和AO之间没有关系

示例:

      function a() {
        function b() {
          var b = 2
        }
        var a = 1
        b()
      }
      var c = 3
      a()
      console.dir(a)

①当函数a被声明时,系统生成[[Scopes]]属性,[[Scopes]]保存该函数的作用域链,该作用域链的第0位存储当前环境下的全局执行期上下文GO,GO里存储全局下的所有对象,其中包含函数a和全局变量c。每个函数的作用域链中至少有一个GO,所以在函数中可以访问全局变量

        GO: {
          c: undefined,
          a: f a(){}
        }

image.png

②当函数a执行时(前一刻),作用域链的顶端(第0位)存储a函数生成的函数执行期上下文AO,同时第1位存储GO。查找变量时,在函数a的作用域链([[Scopes]])中从顶端向下查找。通过打断点,当函数a执行完Local(函数a的AO)就被销毁了,下一次执行函数a时再创建它的AO

        AO: {
          a: undefined,
          b: f b(){}
        }

        GO: {
          c: 3,
          a: f a(){}
        }

动图.gif 为什么说第0位变成AO,而GO被挤到第1位了,可以看下调用栈

image.png ③当函数b被定义时,是在函数a的环境下,所以函数b的作用域链就是函数a在调用时(前一刻)的作用域链。

        AO: {
          a: undefined,
          b: f b(){}
        }

        GO: {
          c: 3,
          a: f a(){}
        }

④当函数b执行时(前一刻),在作用域链([[Scopes]])中第0位存储b的AO,a的AO和GO依次向下排列

        bAO: {
          b: 2
        }

        aAO: {
          a: 1,
          b: f b(){}
        }

        GO: {
          c: 3,
          a: f a(){}
        }

⑤函数b执行完,bAO释放;函数a执行完,aAO释放