有同学看见标题会想:这不就是简简单单的声明了一个变量并赋值吗?这篇文章将带你看看背后隐藏了多少细节。
1. JavaScript 执行机制概览
在 JavaScript 中, 可以分为 编译阶段 和 执行阶段,程序的执行由三者进行协作
引擎 从头到尾负责整个 JavaScript 程序的编译及执行过程。
编译器 引擎的好朋友之一,负责语法分析及代码生成等脏活累活。
作用域 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
例如,以下代码:
var a = 1;
引擎认为这里有两个完全不同的声明,一个由编译器在编译时处理 在编译阶段,编译器 会处理为:
var a; // 声明提升
此时变量 a 已经被提升到作用域的顶部,但它的值 1 还没有赋给它。赋值操作在执行阶段进行。
事实上,他们在背后还做了很多工作。 当编译器碰到一个变量声明,他会在当前作用域中查找是否已经有一个该名称的变量存在于同一个作用域的。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a。
接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 1 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续沿着作用域链查找该变量。
我们发现无论是执行阶段还是编译阶段,当发现变量时都需要对他进行查找操作,但是这两个查找是不同的,我们把他分成LHS查找和RHS查找
LHS查找和RHS查找
- 左值(LHS) :表示赋值操作中存储数据的地方。左值通常是一个可以被修改的变量。
- 右值(RHS) :表示要赋给变量的值。在赋值语句中,右值可以是任何表达式,表示要赋的值。
简单来说,就是等号左边的变量会使用LHS查找,等号右边的变量会使用RHS查找。
-
LHS 查找:
- 发生在赋值操作时。
- 会沿着 作用域链 向外查找,直到找到变量或者到达全局作用域。
- 如果找不到变量,在严格模式下会抛出 ReferenceError(引用错误),在普通模式下会自动创建全局变量。
function foo(){
a = 100;
}
foo();
console.log(a);// a
-
RHS 查找:
- 发生在获取变量值时。
- 会沿着 作用域链 向外查找,直到找到变量或者到达全局作用域。
- 如果找不到变量,同样会抛出 ReferenceError。
2. 作用域与作用域链
2.1 作用域的定义
在 JavaScript 中,作用域(Scope)是指一个变量、函数或者对象可以被访问的范围。JavaScript 是一种词法作用域(Lexical Scope)语言,这意味着作用域是由代码中出现的地方决定的,而不是由代码的执行顺序决定的。
作用域可以分为以下几种:
- 全局作用域:任何地方都可以访问的作用域。浏览器中的全局作用域是
window对象。 - 函数作用域:每个函数都有自己的作用域,变量只在函数内部有效。
- 块级作用域(ES6 引入):通过
let和const声明的变量有块级作用域,即它们只在所在的代码块内有效。
2.2 作用域链
作用域链是 JavaScript 用来查找变量的规则。如果在当前作用域中找不到某个变量,它会去父级作用域中查找,直到查找到全局作用域为止。如果最终没有找到该变量,则会抛出 ReferenceError 错误。
例如,考虑以下代码:
var a = 1;
function outer() {
var b = 2;
function inner() {
var c = 3;
console.log(a, b, c);
}
inner();
}
outer();
- 当
inner()函数执行时,它会先查找自己作用域中是否有a,b, 和c变量。 c是在inner函数内部找到的。b在外部的outer函数中找到。a在全局作用域中找到。
这个查找过程就是 作用域链:inner -> outer -> 全局作用域。
2.3 作用域的查找规则
作用域链遵循以下查找规则:
- 查找当前作用域(当前函数或代码块)是否有对应的变量。
- 如果没有,在当前作用域的父级作用域中查找。
- 直到查找到全局作用域,若仍未找到,则抛出
ReferenceError。