深入理解JavaScript的作用域

178 阅读8分钟

深入理解JavaScript的作用域

作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

JavaScript的作用域是什么?

作用域用于确定何处以及如何查找变量(标识符)的一套规则。那这套规则是什么呢?作用域如何规定在何处查找变量?作用域如何规定查找变量(标识符)?为了解决这些疑惑,我们先来理解一下作用域。

理解作用域

学习作用域的过程模拟成几个人物之间的对话。那么,由谁进行这场对话呢?

演员表

引擎: 从头到尾负责整个JavaScript程序的编译及执行过程。

编译器: 负责语法分析及代码生成等脏活累活。

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

对话

当你看到var a = 2;这段程序的时候,很可能认为这是一句声明。但引擎却不这么看。引擎认为这里有两个完全不同的声明,一个var a由编译器在编译时处理,另一个a = 2则由引擎在运行时处理的。

编译器

遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译,否则它会要求作用域在当前的作用域的集合中声明一个新的变量,并命名为a。

引擎

引擎运行时处理a = 2赋值操作。首先询问作用域,在当前的作用域集合中是否存在一个叫做a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。

查找变量(标识符)

LHS:赋值操作的目标是谁,RHS:谁是赋值操作的源头。

在何处查找变量

作用域查找变量按照遍历嵌套作用域链的规则进行查找,引擎从当前的执行作用域开始查找变量,如果在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量。或抵达最外层的作用域(也就是全局作用域)为止。

function foo(a){

   console.log(a+b);

}

var b = 2;

foo(2);//4

对b进行的RHS引用无法在函数foo内部完成,但可以在上一级作用域中完成。

不支持在 Docs 外粘贴 block

LHS和RHS引用都会在当前楼层进行查找,如果没有找到,往上一层楼进行查找,如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域),无论是否找到整个查找过程都将停止。

“遮蔽效应”

在多层的嵌套作用域中可以定义同名的标识符,作用域查找会在找到第一个匹配的标识符时停止,这叫做“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向上进行查找,直到遇到匹配的第一个标识符。由于全局变量会自动成为全局对象的属性,因此可以通过全局属性进行访问,避开“遮蔽效应”。

“隐藏内部实现”

每一个作用域都会将所写的代码片段进行包装,将这些代码“隐藏”起来了。因此变量查找不能向下查找。这样做的目的是为了满足最小授权最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模版或对象的API设计。延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作用域中,当然可以在所有的内部嵌套作用域中访问到它们。但这些变量或函数本应该是私有的,正确的代码应该是可以阻止对这些变量或函数进行访问。

最小授权原则

最小特权原则要求每个用户和程序在操作时应当使用尽可能少的特权,而角色允许主体以参与某特定工作所需要的最小特权去签入(Sign)系统。被授权拥有强力角色(Powerful Roles)的主体,不需要动辄运用到其所有的特权,只有在那些特权有实际需求时,主体才去运用它们。

JavaScript的作用域有哪些?

每一个作用域比喻成一个“气泡”,作用域包含了一系列的“气泡”,每一个都可以作为容器,其中包含了标识符(变量,函数)的定义。这些气泡互相嵌套并且整齐地排列成蜂窝型,排列的结构是在写代码时定义的。究竟是什么生成了一个新的气泡?最常见的答案是函数作用域,意味着每建一个函数都会为自身创建一个新的气泡?难道其他结构不会创建气泡吗?答案是否定的。

作用域分为两类词法作用域和动态作用域。绝大多数语言使用的词法作用域,JavaScript使用的是词法作用域,因此不介绍动态作用域。

词法作用域

词法作用域定义在词法阶段的作用域。词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。

全局作用域

贯穿整个 javascript 文档,在所有函数声明或者大括号之外定义的变量,都在全局作用域里。 一旦你声明了一个全局变量,那么你在任何地方都可以使用它,包括函数内部。 事实上, JavaScript 默认拥有一个全局对象 window ,声明一个全局变量,就是为 window 对象的同名属性赋值。

函数作用域

函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用和复用。当每对一个函数进行声明都将创建一个函数作用域,然而函数表达式并不会创建函数作用。区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

块作用域

块级作用域指使用一对大括号包裹的一段代码,比如判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

垃圾收集:...........。

在作用域内的变量声明有哪些?

任何声明在某个作用域内的变量,都将附属于这个作用域。在作用域内部的变量声明有:函数声明、var、let、const。

首先了解一下什么是提示?提升是指声明会被视为存在作用域的整个范围内。函数声明和var声明都会被提升,但函数会首先被提升,然后才是var。

函数声明: 通过关键字function进行声明,为该函数创建一个函数作用域。声明的函数在声明所处的整个作用域都可以访问到。

var 声明一个变量,并可选地将其初始化为一个值。var声明变量只存在当前作用域,但当var声明的作用域是属于块作用域时,变量属于外部作用域。

let、 const: let、const关键字可以将变量绑定到所在的任意作用域中(通常是{......}内部)。不存在变量提升。

作用域的闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的。

闭包核心作用

  • 函数在当前词法作用域之外执行,可以访问函数所在的词法作用域。
  • 让变量始终保存在内存当中

什么是垃圾回收机制

垃圾回收又称为 GC(Garbage Collecation)。垃圾收集器会按照固定的时间间隔(或预定的收集时间)周期性地执行找出那些不再使用的变量,然后释放其占用的内存的操作。

垃圾回收机制的实现

通过使用引用计数法判断代码中的变量是否被释放,即引擎中有一张“引用表”,保存了内存里面所有资源(通过是变量)的引用次数。当一个变量的引用次数为0,则表示该值不再用到了,因此资源也被内存释放了。

闭包让变量始终保存在内存当中

由于闭包的定义:函数在当前词法作用域之外执行,可以访问函数所在的词法作用域,使得函数所在的词法作用域的变量生存时间延长,造成常驻内存。