JavaScript 作用域、闭包

163 阅读3分钟

一、JavaScript 预编译

先思考这段代码的打印结果

    <script>
        console.log(a); // function
        var a = 123;
        console.log(a); // 123
        function a() {
            console.log(a); 
        }
        a();
    </script>

javascript 是解释型语言,逐行解析,逐行执行。

在JavaScript被真正解析之前,js解析引擎会首先把整个文件进行预处理,以消除一些歧义,这个预处理的过程就成为预编译

(一)全局对象GO (Global Object)

  • 在浏览器环境中,js引擎会整合 <script> 标签中的内容,产生 window 对象,这个 window 对象就是全局对象
  • 在node环境中,全局对象是global对象

1. 全局变量

<scrip> 标签中声明的变量为全局变量,全局变量会作为window对象的属性存在

2. 整合

所有<script>标签的内容会被整合到同一个全局window对象中

 <script>
  var a = 100;
  console.log(a);  // 100
  console.log(window.a) // 100
</script>

 <script>
 // 在这里能访问到a吗? 可以,因为js引擎会把所有<script>标签整合到一起,生成一个window对象
  console.log(a);  // 100
</script>

3. 全局函数

<script>标签中声明的函数,就是全局函数,也会被js引擎整合到window对象中.

  <script>
   function a(){
       console.log('111');
   }
   window.a();
   console.dir(window.a);//  使用dir方法打印出a函数的内部结构
 </script>

(二)活动对象 AO (Activation Object)

  • 也叫做激活对象
  • 函数调用时产生,用来保存当前函数内部的执行环境(Execution Context),也叫执行期上下文
  • 在函数调用结束时销毁

注意:除非函数被调用,否则函数声明的代码,是不会被执行的

1. 局部变量

在函数内部声明的变量叫局部变量,局部变量作为AO对象的属性存在。

注意,函数声明的代码是不会在预编译期被执行的,只有调用函数时,代码才会被执行

    function a(){
        var i = 0;
        console.log(i);
    }

    a();

如何理解局部变量

在函数a的外部,不能访问i变量,i变量只在函数a的范围内才能使用(这也是作用域的由来)

  • 如果不执行函数,不会产生AO对象,就不会存在i属性;
  • 如果执行函数,就会产生AO对象,并将变量i作为AO对象的属性;
  • 函数执行完后,AO对象被销毁,也就意味着AO对象的i属性也没有了。

2. 局部函数

在函数内部声明的函数,就叫局部函数,局部函数作为AO对象方法存在.

<script>
        function a() {
            function b() {
                console.log(222)
            }
            b()
        }
        a()
</script>

(三)全局预编译

流程:

  1. 查找变量声明,作为GO对象的属性名,值为undefined
  2. 查找函数声明,作为GO对象的属性名,值为function

(四)函数预编译

流程

  1. 在函数被调用时,为当前函数产生一个AO对象
  2. 查找形参和变量声明作为AO对象的属性名,值为undefined
  3. 使用实参的值改变形参的值
  4. 查找函数声明,作为AO对象的属性名,值为function

函数预编译顺序:局部变量声明、局部变量赋值、形参声明、实参赋值、局部函数声明、局部函数赋值

最终编译成果优先级:局部函数 > 实参 > 形参、局部变量

二、作用域与作用域链

(一)作用域

在JS中,作用域分为 全局作用域局部作用域

  • 全局作用域:由 <script>标签产生的区域,从计算机角度可以理解为GO对象(浏览器中就是window对象)
  • 局部作用域:由函数产生的区域,从计算机的角度可以理解为该函数的AO对象 (ES6中引入了块级作用域,也是局部作用域)

(二)作用域链

在JS中,函数存在一个隐式属性,[[scopes]],这个属性用来保存当前函数在执行时的环境(上下文),由于在数据结构上是链式的,也被成为作用域链,我们可以把它理解成一个数组。

(三)作用域链的作用

在访问变量或者函数时,会在作用域链上由内及外,由近及远依次查找。最直观的表现就是:

  • 内部函数可以使用外部函数声明的变量
    <script>
        function a() {
            var aa = 111;
            function b() {
                var aa = 222;
                console.log(aa);
            }
            b();
        }
        a();
        // 1.产生a函数的AO对象,aAO
        // 函数a的scopes:
            // 0: aAO={aa: undefined,b:funtion} -> aAO={aa: 111,b:funtion}
            // 1: GO
        // 2.产生b函数的AO对象,bAO
        // 函数b的scopes:
            // 0: bAO={aa: undefined} -> bAO={aa: 222}
            // 1: aAO={aa:111, b: function}  
            // 2: GO  
        
    </script>

结论:

  • 内部函数可以使用外部函数的变量
  • 外部函数不能使用内部函数的变量

三、闭包

(一)闭包的形成

  • 如果在内部函数使用了外部函数的变量,就会形成闭包,闭包保留了外部环境的引用
  • 如果内部函数被返回到了外部函数的外面,在外部函数执行完后,依然可以使用闭包里的值(闭包保持)

(二)闭包的保持

  • 如果希望在函数调用后,闭包依然保持,就需要将内部函数返回到外部函数的外面
    <script>
        function a() {
            var num = 0;
            function b() {
                console.log(num++);
            }
            return b;
        }
        var demo = a(); 
        demo();// 0
        demo();// 1
        
        // 1.全局预编译,产生GO
        // GO: { demo: undefined, a:function}
        // 2.全局预编译结束,依次执行代码,执行到 var demo = a() 这一行, 会调用a这个函数,对a函数进行预编译,产生aAO, 并返回b给demo
        // aAO:{ num: undefined, b: function} -> { num:0, b:function}
        // GO:{ demo: b, a:function}
        // 3.继续往下执行代码,执行到第一个demo()这一行,相当于执行b函数,对b函数进行预编译,产生bAO
        // bAO:{}
        // 此时b函数的作用域[[scopes]]为
            // 0:bAO {}
            // 1:aAO {num:0b:function} 此时打印num 为0,然后++,变为1
            // 2:GO {demo:b, a:function}
        // 4.继续往下执行代码,执行到第二个demo()这一行,相当于再次执行b函数,再次对b函数进行预编译,产生新的bAO
        // 此时b函数的作用域[[scopes]]为
            // 0:bAO {}
            // 1:aAO {num:1, b:function} 此时打印num为0,然后++,变为2
            // 2:GO {demo:b, a:function}
    </script>

(三)闭包的作用

  • 在函数外部直接访问函数的私有变量

1. 闭包的优点

  1. 隐藏变量,避免全局污染
  2. 可以读取函数内部的变量

2. 闭包的缺点

  1. 导致变量不会被垃圾回收机制主动回收,有内存泄露的可能