「你必须知道的JavaScript」作用域与闭包

334 阅读5分钟

基础知识

引擎和它的好朋友

  • 引擎:从头到尾负责整个JavaScript程序的编译和执行过程。
  • 编译器:负责语法分析及代码生成的内容。
  • 作用域:负责收集和维护所有声明的标识符(变量)组成的一系列 查询 ,并实施一套严格的规则,控制当前执行代码对标识符(变量)的访问权限。

编译器——编译原理

一般分为三个步骤:

  • 词法分析:将字符组成的字符串分解为代码块(词组单元)
  • 语法分析:将词组单元流转化为AST树(抽象语法🌲)
  • 代码生成:将AST树转化为机器可执行的代码。

举第一个🌰:var a = 1 ;

变量的赋值操作,实际上分为两步:

  1. 编译器会在当前的作用域中声明一个变量(如果没声明过),也就是 var a ;
  2. 在运行时,引擎会在作用域中查找该变量,如果能找到就进行赋值,也就是 a = 2,找不到就会报错。

引擎——查询

分为LHS查询和RHS查询:

  • LHS:找到某个变量的容器本身,并对它赋值

    • 例如在第一个🌰中,a = 2 就是一个LHS查询
  • RHS:查找某个变量,获取它的值

    • 例如 console.log(a) 就是一个RSH查询

作用域嵌套

实际情况中,通常需要同时顾及几个作用域。在当前作用域找不到时,引擎就会在外层嵌套的作用域中继续查找,直到在全局作用域也找不到时,查找停止。

可能发生的两种异常类型:

  • ReferenceError:作用域判别失败相关
  • TypeError:作用域判别成功,但对结果的操作是不合法的。

词法作用域

  • 词法作用域Lexical Scopes)是 javascript 中使用的作用域类型,词法作用域 也可以被叫做 静态作用域,与之相对的还有 动态作用域
  • 无论函数在哪里,在何时被调用,它的词法作用域都只在函数被声明时所处的位置决定。
  • 词法作用域查找只会查找一级标识符。

欺骗词法(不推荐使用)

  • eval(..)函数

    • 通常被用来执行动态创建的代码
    • 接受一个字符串,插入的字符串会被当做原本就在那里一样来处理。
    • 可能对原本的词法作用域进行了修改。
  • with 关键字

    • 通常被当做重复引用同一个对象的多个属性快捷方式,可以不用重复引用对象本身
     var obj = {
       a:1,
       b:2,
       c:3
     }
     with(obj){
       a = 3;
       b = 4;
       c = 5;
     }
    
    • with声明实际上是根据传递的对象,凭空创建了一个全新的词法作用域,当对象中的标识符在全局作用域也找不到时,会自动创建一个全局变量。

函数作用域

属于这个函数的全部变量都可以在整个函数的范围内使用及复用。

  • 在外部使用一个函数包裹,可以隐藏内部实现。

  • 规避冲突

    • 使用全局命名空间
    • 使用模块管理
  • (function fn(){...})作为函数表达式,说明foo只能在...所代表的位置中被访问,外部作用域则不行,不会污染外部作用域。

  • 匿名函数表达式和具名函数表达式

    • 匿名的缺点

      • 在栈追踪中不会显示有意义的函数名,调试困难
      • 不方便自身引用
      • 代码可读性差
  • 立即执行函数表达式(IIFE)

    • (function(){})()

    • 用法

      • 通过函数作用域解决命名冲突
      • 把他们当做函数调用并传递参数进去
      • 倒置代码的运行数据

块作用域

简单来说,花括号内 {...} 的区域就是块级作用域区域。

ES6 标准提出了使用 letconst 代替 var 关键字,来“创建块级作用域”。

变量提升

  • 只有声明本身会提升,而赋值或者其他运行逻辑会留在原地。
  • 每个作用域都会进行提升操作。
  • 函数声明首先被提升,然后才是变量。
  • 后面出现的函数声明,可以覆盖前面的。

闭包

什么是闭包

指有权访问另一个函数作用域中变量的函数

形成闭包的原因

内部的函数存在外部作用域的引用就会导致闭包

闭包的作用

  • 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
  • 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化。

闭包的场景

1、return一个函数

 var n = 10
 function fn(){
     var n =20
     function f() {
        n++;
        console.log(n)
      }
     return f
 }
 ​
 var x = fn()
 x() // 21

fn()就是一个闭包,存在上级作用域的引用,即 n = 20,输出结果为21

2、函数作为参数

 var a = 'merlin'
 function foo(){
     var a = 'foo'
     function fo(){
         console.log(a)
     }
     return fo
 }
 ​
 function f(p){
     var a = 'f'
     p()
 }
 f(foo())
 /* 输出
 *   foo
 / 

fo()就是闭包,存在上级作用域的引用,即a = 'foo',console.log 结果为foo

3、自执行函数

 var n = 'merlin';
 (function p(){
     console.log(n)
 })()
 /* 输出
 *   merlin
 / 

同样存在上级作用域的引用,即n = 'merlin',console.log 结果为merlin

4、循环赋值

 for(var i = 0; i<10; i++){
   (function(j){
      setTimeout(function(){
       console.log(j)
       }, 1000) 
   })(i)
 }

因为存在闭包,所以能依次输出0-9,闭包形成了10个互不干扰的私有作用域。每个作用域里的i都不同

如果去掉自执行函数,就不存在外部作用域的引用,那么每一个setTimeout调用的都是全局的i。

由于js是单线程的,遇到异步代码会先入栈,不先执行,等到同步代码执行完后才执行,此时的全局i为10,所以输入的为10个10

5、回调函数

 window.name = 'merlin'
 setTimeout(function timeHandler(){
   console.log(window.name);
 }, 100)

闭包需要注意什么

容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。

过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。