4. JavaScript 作用域与闭包

154 阅读6分钟

JavaScript 的编译原理

JavaScript 是一门编译语言。

JavaScript 的编译是发生在代码执行前的几微米(甚至更短)的事件内,所以 JavaScript 没有其他语言那么多的时间来进行优化。

当 JavaScript 执行 var a = 2 时,并不是我们理解的直接创建一个变量 a,并赋值为 2,实际上它分成了两个步骤进行(这样就很好地解释了声明提升):var a 和 a = 2;

在执行 var a = 2 时,编译器首先把它分解成词法单元,然后把词法单元拆解成抽象语法树(AST)。

抽象语法树会有一个叫做 VariableDeclaration 的顶级节点;然后会有一个叫做 Identifier 的子节点,这个子节点的值是 a;以及一个叫做 AssignmentExpression 的子节点。而 AssignmentExpression 又会有一个 NumericLiteral 的子节点,这个子节点的值是 2。

当 JavaScript 进行编译的时候,编译器会询问当前作用域是否已经又一个名为 a 的变量,如果有,就忽略这个声明;否则就会在作用域的当前作用域的集合中声明一个新的变量,并命名为 a。

之后 JavaScript 引擎运行的时候,就会询问当前作用域集合中有没有一个叫 a 的变量,如果有,JavaScript 的引擎就会使用这个变量,如果没有,就会向上一个作用域继续寻找叫 a 的变量。如果找到了 a,就会给它赋值 2。如果一直没有找到,JavaScript 引擎就会抛出一个异常。

需要注意的点:

我们在理解:function foo(a){} 的时候,经常理解成 var foo,foo = function(a){},但实际上这是不对的,函数的声明并不能简单地以 LHS 查询和赋值的形式进行理解。

总结:

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。

= 操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(当前没找到),就会向上级作用域继续查找目标标识符(作用域链)。

不成功的 RHS 会导致抛出 ReferenceError 异常。不成功的 LHS 会自动隐式在全局作用域中创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符。(如果是严格模式下也会抛出 ReferenceError 异常)。

LHS 查询会找到变量的容器本身;

RHS 查询则是简单地查找到某个变量的值。

JavaScript 三种定义方式

var:会定义全局的变量,会声明提升

let:会定义一个有块级作用域的变量,会声明提升,但是在声明之前,它会处于临时性死区,不可调用

const:定义不可修改的变量(这里的不可修改指值类型,引用类型只是不能重新赋值)

但是,const 定义的对象并非绝对的不可修改:

const obj = {
    a:1,
    b:2
}

我们不能重新定义 obj,但我们可以修改 obj 里面定义的值。

如果不想让我们修改对象里面的值,我们可以只用 Object.freeze 来进行定义,这样就不允许改变属性的值了:

const jelly = Object.freeze(obj)

JavaScript的作用域与作用域链

ES6 到来 JavaScript 有全局作用域、函数作用域和块级作用域(ES6新增)。

作用域是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名的变量不会有冲突。

在介绍作用域链前,先了解下自由变量。所谓自由变量,就是当前作用域中没有定义的变量,这称为 自由变量。

var a = 100
function fn() {
    var b = 200
    console.log(a) // 这里的a在这里就是一个自由变量
    console.log(b)
}
fn()

自由变量的值是向父级作用域寻找到的,如果父级也没有,就继续向上寻找。这一层一层的关系,就是作用域链。

当自由变量从作用域中去寻找,依据的是函数定义时的作用域链,而不是函数执行时。

function F1() {
    var a = 100
    return function () {
        console.log(a)
    }
}
function F2(f1) {
    var a = 200
    console.log(f1())
}
var f1 = F1()
F2(f1) // 100 

**当一个作用域套在另一个作用作用域中时,就发生了作用域嵌套。因此,**JavaScript 引擎在当前作用域没有找到某个变量的时候,会在外层嵌套的作用域中继续查找,直到找到该变量,或者抵达最外层的作用域(全局作用域)为止,这就是我们说的作用域链。例如:

function foo(a){
	console.log(a + b);
}
foo(2);  // 4

当 JavaScript 引擎对 b 进行的 RHS 引用无法在函数 foo 内部完成,但可以在上一级作用域中完成(这个例子就是全局作用域)。

总结:JavaScriptm引擎的查找是从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域的时候,无论找到还是没找到,查找过程都会停止。

闭包

闭包就是用来访问其他函数中的变量的。因为作用域的关系,在函数的外部是无法获取函数内函数的值的,因为作用域链只能向上查找,不能向下查找。

闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

例如:

function f1(){
	var n=999;
	function f2(){
		alert(n); 
	}
	return f2;
}
var result=f1();
result(); // 999

或者:

function outer() {
  var num = 0 //内部变量
  return function add() {
    //通过return返回add函数,就可以在outer函数外访问了。
    num++ //内部函数有引用,作为add函数的一部分了
    console.log(num)
  }
}
var func1 = outer() //
func1() //实际上是调用add函数, 输出1
func1() //输出2
var func2 = outer()
func2() // 输出1
func2() // 输出2

要注意的是,闭包很容易造成内存泄漏,因为闭包最大的作用就是:

  • 读取函数内部的变量
  • 让这个变量的值长期保存在内存中,延长它的生命周期

为了避免内存泄漏影响网页性能,在闭包使用完成后,要立即释放资源,将变量指向 null。(这里涉及到 JavaScript 的垃圾回收机制)

参考

  • 《你不知道的JavaScript》