编译过程
在编译过程可以宏观的分为以下三个阶段
- 分词/词法分析。将代码分解成词法单元。
- 解析/语法分析。一步得到的词法单元组成抽象语法树(AST)。
- 代码生成:将抽象语法树转换为可执行代码
在编译这个过程中有三个角色会参与,他们各司其职保证程序能够正常运行。
- 引擎: 从头到尾负责js程序的编译与执行。
- 编译器: 负责词法分析与代码生成。
- 作用域:对标识符收集与查询。有一套严格的规则来保证当前代码的标识符权限。
通过分析 'var a = 2'这句简单的代码来分析这个过程。
- 遇到var a的时候编译器会询问作用域是否已经有一个该名称的变量,如果有则忽略继续编译。如果没有则在该作用域下新申明一个变量a
- 编译器为引擎生成所需代码,代码是用于处理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它还不是一个函数。
另外还要注意函数优先这一点。