2022-07-08 重新掌握JavaScript中的作用域和闭包

148 阅读6分钟

之前买的书籍《你不知道的JavaScript》系列又被我最近重新阅读起来,那会儿工作一年读起来没什么太大的感悟,现在工作三年了再重新翻读后,对JS这门语言的理解加深了,好记性不如烂笔头,于是乎写下文章记录下学习过程

作用域到底是什么

我们大部分人工作是为了赚钱,包括我自己在内,然而转的钱很多存在银行里了;

这里用来存储的钱的方式是银行,同样的道理,JS用来存储变量的地方是内存,但是在内存中怎么存储,以什么样的规则去存储查找变量,这么一套规则就是我们称呼的作用域;

在《你所不知道的JavaScript》一书中,这样介绍到作用域:一套设计良好的规则来存储变量,并且之后可以方面的找到这些变量,这套规则就是作用域。

理解作用域

一般编译语言都要经历词法分析->解析/语法分析->代码生成这三个阶段,而JavaScript则要复杂的的很多,它是一门动态语言,在执行前进行编译,JS引擎在这过程还要做很多优化工作。

  • 引擎

      从头到尾负责整个JavaScript程序的编译及执行过程
    
  • 编译器

      引擎的好朋友之一,负责语法分析及代码生成等脏活累活
    
  • 作用域

      引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,
      并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限
      
    

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后再运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

作用域嵌套

之前提到过,作用域时根据名称查找变量的一套规则。实际情况中,通常需要同时顾及几个作用域。

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域嵌套。因此,再当前作用域中无法找到某个变量时,引擎就会再外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

遍历嵌套作用域的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。

词法作用域

作用域主要有两种:第一种是最为普遍的,被大多数编程语言所采用的词法作用域,也叫静态作用域如JavaScript、C等;另外一种就是动态作用域,仍有一些编程语言在使用如Bash脚本等。本篇主要谈谈静态作用域即词法作用域。

词法作用域,简单来说就是定义再词法阶段的作用域,也就是声明位置的时候产生的作用域。换句话说,词法作用域是由你再写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时回保持作用域不变(大部分情况下是这样的)。

作用域查找会在找到第一个匹配的标识符时停止

在多层的嵌套作用域中可以定义同名的标识符,这叫做遮蔽效应。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。

欺骗词法

词法作用域也不一定完全由写代码期间函数所声明的位置来定义,也可以通过以下两个机制来实现作用域的改变的。

  • eval

      eval函数可以接受一个字符串为参数,并将其中的内容视为好像再书写时就存在程序中这个位置的代码。
      换句话说,把字符串参数转为表达式语句,插入到当前位置上
    
  • with

      with通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身
    

函数作用域和块作用域

函数作用域的含义是指,属于这个函数的全部变量都可以再整个函数的范围内使用及复用(事实上再嵌套的作用域中也可以使用)。

块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息,以下几种方式可以形成块作用域。

  • with
  • try/catch中的catch分句、
  • let
  • const

提升-先有蛋(声明)后有鸡(赋值)

直觉上JavaScript代码是由上到下一行一行执行的,但是并不是这样的。

  • 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地
  • 并且每个作用域都会进行提升操作
  • 函数声明和变量声明都会被提升,但是函数的优先级更高
  • 最新的let和const并不会使变量提升

作用域闭包

实质定义:当函数可以记住并访问所在的词法作用域时,即使函数在当前词法作用域之外执行,这时就产生了闭包,可以理解把函数当作值来进行传递。

下面这段代码准确来说不是产生了闭包:

non-closure.png

这里最准确的解释是bar()a的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。

这段代码则清晰的展示了闭包:

closure.png

bar()正常执行,而且在它自己定义的词法作用域之外的地方执行,bar()依然持有对该作用域的引用,而这个引用就叫做闭包。

闭包的使用场景

  • 定时器setTimeout等
  • for循环使用IIFE立即执行函数表达式
  • 模块化机制
  • 函数curry化