JavaScript中变量的作用域

作为一个稍微有点编程经验的程序员,在学习一门新的编程语言时通常都不用特别关注作用域的问题,因为绝大部分的编程语言中变量的作用域遵循的规则都差不多,开发人员能顺其自然预见一个变量在何处可见,在何时销毁。但是到了JavaScript中,如果再用其他语言的作用域规则往上套,有时会得到让人很迷糊的结果。我觉得这主要是因为JavaScript过渡依赖全局变量,和var声明的变量不具有块级作用域导致的。

本文将总结一下笔者对JavaScript中变量的作用域的理解,如果想深挖其背后的运行机制,推荐阅读《你不知道的JavaScript(上卷)》,这本书几乎用了几乎半本书的内容来讲解作用域。

全局作用域

script标签直接包围的区域就是JavaScript中的全局作用域,定义在全局作用域中的变量叫作全局变量,全局变量在所有作用域中都可见。全局变量在浏览器窗口打开时创建,在浏览器关闭时销毁。在JavaScript中有3种方式定义全局变量。第1种是在任何函数之外放置一个var语句:

1
var foo = value;

第2种是直接给全局对象添加一个属性。全局对象是所有全局变量的容器。在Web浏览器里,全局对象是window:

1
window.foo = value;

在全局作用域中定义的函数同样也会绑定为window对象的方法,所以当不确定变量和函数是否属于全局作用域时,可以打印window来看一下。

第3种是直接使用未经声明的变量,这被称为隐式的全局变量:

1
foo = value;

还记得大学时刚开始写JavaScript代码,也不管三七二十一,就在一个.js文件写完所有的逻辑,变量也是定义一次恨不得从头用到尾。但是过渡使用全局变量会导致代码的可读性、复用性、可维护性大大降低,还可能导致与引用的其他.js文件中的变量发生命名冲突。在《JavaScript语言精粹》这本书中作者直接把全局变量列在了JavaScript最糟糕的特性第一位。

块作用域

一个代码块(扩在一对花括号中的一组语句)会创建一个作用域,这个作用域就是块级作用域。代码块中声明的变量在其外部是不可见的,然而JavaScript中在代码块中用var声明的变量,在包含此代码块的函数的任何位置都是可见的。

1
2
3
4
5
6
function foo() {
for (var i = 0; i < 5; i++) {

}
console.log(i) // 仍然能访问到i,并且值为5
}

好在ES6引用了新的关键字letconst来定义变量,letconst会为其声明的变量隐式地绑定到变量所在的作用域,包括块级作用域。将上面例子中的var改成let,再运行就会发现报错了,Uncaught ReferenceError: i is not defined

块作用域很明确地告诉引擎,变量何时可以创建,何时可以销毁,我们可以利用这一点来释放占用大量内存空间的临时变量,比如《你不知道的JavaScript(上卷)》中给出的这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function process(data) {
// 在这里做点有趣的事情
}

// 在这个块中定义的内容可以销毁了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );

理论上process(…)执行后,someReallyBigData就可以销毁了,但由于click函数形成了一个覆盖整个作用域的闭包,JavaScript引擎极有可能依然保存着这个结构(取决于具体实现)。所以不如在这里显示告诉引擎,这个变量已经不在需要了。

我觉得这是很有用的一点,在开发时可以使用。

函数作用域

JavaScript中还是存在函数作用域的,我们要牢牢记住两点,一是定义在函数中的参数和变量在函数外部是不可见的,不管是varlet还是const定义的变量,在函数外部都是不可见的。二是在一个函数内部任何位置定义的变量,在该函数内部任何地方都可见,所以对于定义在函数内部的函数,也是可以访问到其外部函数中的参数和变量,(这里为闭包先铺个垫)。

1
2
3
4
5
6
7
8
9
10
11
12
var foo = function () {
var a = 3, b = 5;
var bar = function () {
var b = 7, c = 11;
// 此时,a=3 b=7 c=11
a += b + c;
// 此时,a=21 b=7 c=11
};
// 此时,a=3 b=5
bar();
// 此时,a=21 b=5
}

有一个很好玩的点,对于函数本身,在其内部是可见,在其外部是不可见的。

1
2
3
4
5
6
function foo() {
// 打印该function
console.log(foo)
}
// 打印undefined
console.log(foo)

总结

从词法作用域来讲,可以将作用域再分为全局作用域、块作用域、函数作用域。我们要小心意义在全局作用域中定义变量和函数,因为过多的全局变量会让代码变得很糟。我们要擅于利用块作用域,来显示销毁临时变量,释放内存。函数这里涉及到闭包和箭头函数时,情况就变得复杂了,等到总结闭包和箭头函数时再细说。

还有一点,如果能确定一个变量就是局部变量,那最好用let来定义,这样在dev tools中debug时,比较容易观察,用var定义的变量如果跑到了全局作用域,那还要展开window,一点点找。

参考资料

【1】 《JavaScript语言精粹》

【2】 《你不知道的JavaScript》

本文作者:意林
本文链接:http://shinancao.cn/2019/07/21/JS-Scope/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!