三分钟学JavaScript:词法作用域

274 阅读8分钟

词法作用域

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,另外一种叫作动态作用域。词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写 代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域 不变(大部分情况下是这样的)。无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

作用域

要理解JavaScript的执行发生了什么以及词法作用域首先得先知道下面几个概念:

  • JavaScript引擎: 负责JavaScript程序的编译与执行的过程
  • 编译器: 负责语法分析及代码生成的工作
  • 作用域: 负责收集维护声明的标识符组成的一系列查询,确定当前执行代码对这些标识符的访问权限

举个例子:当我们写下 var a = 1 时;编译器首先会将这代码分解成词法单元,遇到var a,编译器会询问作用域是否存在该名称的变量,如果是,编译器会忽略声明,继续编译;否则它会在当前作用域声明一个新变量,命名为a。接下来编译器会为引擎生成运行时所需代码,为变量a赋值。引擎运行时首先询问作用域,在当前作用域是否存在a变量,如果是,引擎就会使用这变量赋值,否则,引擎会继续逐级往上级作用域查找变量直到顶层作用域为止,如果找到则进行赋值操作,如果找到顶层作用域也不存在这变量,那么在顶层作用域创建a变量,并赋值(所以 要特别注意 b=1缺乏var的声明格式)。

LHS与RHS

引擎执行编译后的代码怎么去查找作用域的变量,影响着运行的结果。通常引擎查找变量有LHS查询与RHS查询两种。L与R分别表示赋值操作的左侧与右侧。通俗讲,当变量出现在赋值操作( = 号)的左侧时,进行LHS查询;当变量出现在赋值操作的右侧时,进行RHS查询。RHS为了查找某个变量的值,而LHS为了找到变量的容器本身

LHS :

a = 2        //为2找到赋值目标 即变量容器

RHS:

console.log(a)    //查找 a 的值

综合:

function foo(a) {
var b = a; // RHS、LHS
return a + b; // RHS  
}
var c = foo( 2 );  // LHS 、RHS

作用域嵌套

image-20201107205704451.png

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用 域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。

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

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

注意两个异常:

  1. 如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。
  2. 如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError。

修改词法作用域

JavaScript 中有两种机制来实现运行时“修改”词法作用域,但这两种导致性能下降,并不提倡使用。

  1. eval

JavaScript的eavl函数接收一个字符串为参数,转化为执行的JavaScript代码,就好像代码就写在那个位置。

function foo(str, a) {
eval( str ); // 插入代码
console.log( a, b ); //输出插入的b值
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3 

当在严格模式下,eval运行的代码有自己的作用域,这种写法就无效了。

2.with

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域。with 通常被当作重复引用同一个对象中的多个属性的快捷方式。

function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!

同样,在严格模式下,with无法发挥作用,通常不推荐使用。

为什么性能差?

JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的 词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到 标识符。但如果引擎在代码中发现了 eval(..) 或 with,它只能简单地假设关于标识符位置的判断 都是无效的,因为无法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会 如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底 是什么。 最悲观的情况是如果出现了 eval(..) 或 with,所有的优化可能都是无意义的,因此最简 单的做法就是完全不做任何优化。

函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。通俗的讲,就是函数内部有着自己的作用域。函数通常有:匿名函数、具名函数、立即执行函数表达式(如 (function foo(){ .. })() )等形式。

块作用域

for (var i=0; i<10; i++) {
console.log( i ); //输出1010
}
//修改成块级作用域 i绑定到内部
for (let i=0; i<10; i++) {
console.log( i ); //输出09
}

先看看上面这一段代码,我们在 for 循环的头部直接定义了变量 i,通常是因为只想在 for 循环内部的上下文中使用 i,但 i 会被绑定在外部作用域(函数或全局)。块级作用域就是为了解决这样的问题,我们只要{}内部的作用域,这该如何处理呢。ES6给出的方法是let声明,绑定到块级作用域。

变量提升

eg1:
​
a = 2;
var a;
console.log( a ); // 2
​
eg2:
​
console.log( a ); //undefined
var a = 2;

先看上面的代码,也许你会觉得第一个结果为undefined,第二个为ReferenceError 。但现实并不如你想的那样。这牵扯到了JavaScript变量提升的问题。在JavaScript中,变量和函数在内的所有声明都会在任何代码被执行前首先被处理,也就是说:当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个 声明:var a; 和 a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。

上面eg1会被处理为:

var a;
a = 2;
console.log( a );

eg2为变为:

var a;
console.log( a );
a = 2;

这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动” 到了作用域的最上面,这个过程就叫作提升。(函数声明会被提升,但是函数表达式却不会被提升。 )同时,函数声明提升的优先级大于变量提升。

eg3:
​
foo();      // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};

这段代码被引擎理解为:

//  函数提升优先级高
function foo() {
console.log( 1 );
}
​
foo(); // 1
foo = function() {
console.log( 2 );
};

作用域闭包

闭包的定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 这听起来或许有点难理解,我们先来看看代码的例子:

function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 这就是闭包的效果。

baz函数调用foo,foo返回bar函数,使得它可以拿到foo函数内部的作用域变量,这就是闭包。bar() 显然可以被正常执行,但是在这个例子中,它在自己定义的词法作用域以外的地方执行,这也符合上面的定义。在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃 圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很 自然地会考虑对其进行回收。而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此 没有被回收。谁在使用这个内部作用域?bar() 本身在使用。

最后

​ 以上的一些总结,如有不足之处,还望指出交流。如果你觉得本文章对你有帮助,麻烦留下你的小心心~~,就是对我最大的支持。