读《你不知道的JavaScript》理解作用域、变量提升

711 阅读4分钟

编译过程

在编译过程可以宏观的分为以下三个阶段

  1. 分词/词法分析。将代码分解成词法单元。
  2. 解析/语法分析。一步得到的词法单元组成抽象语法树(AST)。
  3. 代码生成:将抽象语法树转换为可执行代码

在编译这个过程中有三个角色会参与,他们各司其职保证程序能够正常运行。

  1. 引擎: 从头到尾负责js程序的编译与执行。
  2. 编译器: 负责词法分析与代码生成。
  3. 作用域:对标识符收集与查询。有一套严格的规则来保证当前代码的标识符权限。

通过分析 'var a = 2'这句简单的代码来分析这个过程。

  1. 遇到var a的时候编译器会询问作用域是否已经有一个该名称的变量,如果有则忽略继续编译。如果没有则在该作用域下新申明一个变量a
  2. 编译器为引擎生成所需代码,代码是用于处理a = 2 这个赋值操作。引擎会询问作用域是否有一个叫做a这个变量如果有执行赋值操作,没有则继续查找,如果找不到则抛出异常。
LHS && RHS

生成代码之后,引擎会去执行这段代码时候回去查找变量。查找变量有两种不同的方式。LHS与RHS。可以笼统的理解为赋值操作符(=)的左侧还是右侧。本职是:LHS关注赋值的容器(即:操作的目标),RHS关注操作的值(即:操作的源头)。

function foo() {
  b = 3
}

foo()

console.log(b)

对b进行LHS,不会报错,会在全局生成一个变量b


function foo(a) {
  a = b
}

foo(3)

对b进行RHS,抛出错误:ReferenceError。

词法作用域

定义在词法分析阶段的作用域。在编写代码的时候,函数的位置所决定。通常是不会改变。在运行阶段可以通过eval、with进行作用域改变。

eval
function foo(str, a){
  eval(str)
  console.log(a,b)
}

var b = 2

foo('var b = 3',1)

在词法分析阶段foo作用域中的标识符: str, a。但是在运行阶段foo作用域的标识符:b,a。出现遮蔽现象

with

with早期出现是为了方便给对象属性赋值

var o = {a: 1,b: 2, c: 3}
console.log(o)
with(o){
  a = 2
  b = 3
  c = 4
}
console.log(o)

可以看出用with包住的地方重新形成了一个作用域(o),其中with中的a,b,c都进行了左查找(LHS,可以理解为顺着作用域查找a,b,c这些标识符,如果在全局作用域中都找不到,就会在全局中创建标识符)

var obj = {}
with(obj) {
  name = 'jgmiu'
  age = 24
}
console.log(obj.name)
console.log(obj.age)
console.log(name,age)

以上代码,name与age会泄露到全局作用域上面去。

函数作用域&&块作用域

函数作用域的含义:属于这个函数的全部变量都可以在整个函数范围内使用及复用。而函数作用域的方式有以下几个用处:

  • 函数作用域的一个作用是用于隐藏内部实现,最小特权(最小暴露)原则。
  • 规避冲突。避免同名标识符之间的冲突。

块级作用域

在js中函数是最常见也是我们最熟悉的块作用域单元,但是不是出了函数还有一些其他的方式书写块作用域。在js中有以下几种方式:

  • with
  • try/catch
  • let
  • const

深入理解变量提升

作用域与申明变量的位置有一种微妙的联系,若研究清楚其中联系有助于了解变量提升概念。在说明之前先观察下面两段代码。

a = 2
var a 
console.log(a) //10

如果按照正常的思想来说,代码是一行一行的执行,在第二行重新生命变量a,应该输出初始值undefined

console.log(a) // undefined
var a = 10

按照正常的思想来看,第一行对a进行RHS,然后抛出异常。

以上两段代码都和我们所想的都有出入,出现这样的情况是编译阶段发生了点什么?

编译器

编译阶段引擎会找到所有的申明,并将它们与合适的作用域关联起来。在这一个阶段变量和函数都会先进行处理。所以这里就分为了两个阶段(编译、执行)。之前的两段代码可以变成下面的形势。

var a
a = 2
console.log(a)

将变量申明提升自然输出2

var a 
console.log(a)
a = 10

对a进行RHS的时候在全局环境中存在变量a, 只是此时a还没有赋值,所以就输出undefined

函数&&函数表达式提升

函数申明会被提升, 函数表达式不会被提升

foo()
function foo() {
    console.log(...)
}

正常执行,函数被提升

foo()
var foo = function() {
    console.log('xx')
}

// 可变为以下形式:
var foo
foo()
foo = function(){
    console.log('xx')
}

以上代码不会报TypeError,因为执行函数的时候还没有对函数赋值,foo它还不是一个函数。

另外还要注意函数优先这一点。