作用域

动态作用域与词法作用域

1
2
3
4
5
6
7
8
9
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
var a = 2; bar();
//什么状况,为什么是2,而不是undifined?啊,我知道了,上面那条是由于a=2留在原地等待执行,console执行在前,在此例中执行bar执行在后,不是说关注声明么,是的,关注声明只是让你找对容器,而容器里装什么与声明无关,所以并不矛盾。

如果在你看来,此处应该输出2(当然此处涉及到对于变量提升的理解),则说明你关注的是foo()的声明;如果你认为会输出3,则你关注的是函数的调用。借此例引出词法作用域和动态作用域的主要区别:

词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定 的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

如何理解运行时确定这个概念,bar()调用foo(),动态生成调用栈,基于调用栈生成作用域链,而不是基于代码中的作用域嵌套生成作用域链(词法作用域的做法)。

对于变量的查找,js是遵循词法作用域的,并且现在的大多数语言都是遵循词法作用域的。为什么js不使用动态作用域?我觉得一方面是因为,静态的更好控制,动态的不好控制。javascript引擎会在在编译阶段进行数项性能的优化。其中一些优化依赖于能够根据代码的词法进行静态的分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。

作用域嵌套

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

1
2
3
4
5
6
7
8
//作用域嵌套实例
function bar() {
var a = 3;
(function foo() {
console.log( a );
})()
}
var a = 2; bar();//3

块级作用域

函数作用域和全局作用域,都很好观察和理解。那什么是块级作用域?let 关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说let为其声明的变量隐式地了所在的块作用域。同样的,const声明也可以创建块级作用域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//隐式创建块作用域
var foo = true;
if(foo){
let bar = foo*2;
console.log(bar);
}
console.log(bar);
//2 referrence error

//显示的创建块作用域
var foo = true;
if(foo){
{ let bar = foo*2;
console.log(bar);
}
}
console.log(bar);
//2 referrence error

推荐使用显示创建块作用域。在这个例子中,我们在 if 声明内部显式地创建了一个块,如果需要对其进行重 构,整个块都可以被方便地移动而不会对外部 if 声明的位置和语义产生任何影响。

变量提升

1
2
3
4
5
6
7
8
9
10
11
12
//例1
console.log(a);
var a=2;//undifined

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

//无关紧要的例3,非严格模式下的LSH查找,未查找到a的声明,会自动创建一个a的声明;
a=2;
console.log(a);//2

按照结果逆推,可以感觉到变量被声明到了顶部。实际上也是:引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。(所以函数声明也能被提升)值得注意的是:第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。所以是undifinded。

闭包

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意 识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境。

定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

1
2
3
4
5
6
7
8
9
function foo(){
var a = 2;

function bar(){
console.log(a);
};
bar();
}
foo(); //2

技术上来讲,也许是。但根据前面的定义,确切地说并不是。我
认为最准确地用来解释 bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却 是非常重要的一部分!)函数 bar() 具有一个涵盖 foo() 作用域的闭包。但是通过这种方式定义的闭包并不能直接进行观察,也无法明白在这个代码片段中闭包是 如何工作的。我们可以很容易地理解词法作用域,而闭包则隐藏在代码之后的神秘阴影 里,并不那么容易理解。

1
2
3
4
5
6
7
8
9
function foo(){
var a = 2;

function bar(){
console.log(a);
}
return bar;
}
var baz = foo(); //2

函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作 一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),实 际上只是通过不同的标识符引用调用了内部的函数 bar()。
bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃 圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很 自然地会考虑对其进行回收。上一个作用域嵌套实现的闭包,foo()的内部作用域会被销毁
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此 没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。
拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一 直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

0%