作用域(Scope)
在 JavaScript 中,什么是作用域( Scope )?
我们不妨看看以下代码:
(function(){
var text = "Hello,world!";
})();
console.log(text);
当我们试图访问立即执行函数((function(){....})())
中声明的 text 变量时,造成了ReferenceError: text is not defined
错误。
可见,一个函数内部的变量是不能被函数外部访问的,而变量的可访问性就是作用域了。
目前 JavaScript 中一共有三种作用域,分别是:
- 全局作用域
- 函数作用域
- 块级作用域
顾名思义,全局作用域作用于全部代码,处于全局作用域的对象可以在任何地方被访问。
以下几种情形的变量拥有全局作用域:
一种情况是:定义在最外层函数的变量
var text = "Hello,world!"
function print(){
console.log(text);
}
print();
console.log(text);
输出:
Hello,world!
Hello,world!
同时,text
变量也自动成为了 window
对象中的成员。
<script>
var text = "Hello,world!"
console.log(window.text);
</script>
输出:
Hello,world!
并且,所有 window
对象的属性都拥有全局作用域。
还有一种情况,未被定义但被赋值的变量自动拥有全局作用域:
<script>
(function(){
text = "Hello,world!";//可以对照下开头的例子:var text = "Hello,world!";
})();
console.log(text);
console.log(window.text);
</script>
输出:
Hello,world!
Hello,world!
函数作用域是指定义在某个函数中所拥有的作用域。文章开头的示例,正是函数外部访问函数内部变量造成的错误。
(function(){
var text = "Hello,world!";
(function(){
console.log(text);
text = "Hi!";
console.log(text);
})();
})();
输出:
Hello,world!
Hi!
尝试外部访问:
(function(){
var text = "Hello,world!";
})();
console.log(text);//ReferenceError: text is not defined
作用域好比套娃,层层嵌套。同时,内部作用域可以访问外部作用域的对象,反之则不行。
值得注意的是,JavaScript 不同其他语言,用 var 定义的变量并不具有块级作用域。
// if 中的 text 变量 可以被外部访问
if(true)
{
var text = "Hello,world!";
}
console.log(text);
输出:
Hello,world!
显然,如果没有块级作用域的话很容易造成变量的命名冲突。好在 E6 之后,JavaScript 提供了 let、const 关键字来让变量具有块级作用域。
// 将 var 替换为 let
if(true)
{
let text = "Hello,world!";
}
console.log(text);
ReferenceError: text is not defined
- 不同于 var 声明的变量,let/const 声明的变量不会有变量提升:
if(true)
{
console.log(text);
let text = "Hello,world!";
}
ReferenceError: Cannot access 'text' before initialization
如果你需要让 let/const 声明的变量能在整个块级作用域被访问,最简单的方法就是将声明变量的代码提至块级作用域的顶端。这里不再演示了。
- 不允许重复声明:
let text = "Hello,world!";
var text = "Hello,world!";//let text = "Hello,world!"; 同理
SyntaxError: Identifier 'text' has already been declared
但是它允许在不同作用域重复声明:
var text = "Hello,world!";//let text = "Hello,world!"; 同理
if(true){
let text = "Hello,world!";
}
console.log(text);
输出:
Hello,world!
猜一猜以下代码的输出:
var events = new Array();
for(var i = 0;i < 3;i++){
events[i] = function(){console.log(i)};
}
events.forEach(e => {
e();
});
0,1,2 或 3,3,3 ?
仔细分析,events
每个方法输出的是变量 i
,方法里面没有变量 i
,遵循作用域的原则,向上寻找变量 i
,找到了循环变量 i
,而 i
最后值为 3,所以输出结果应该为:3,3,3。
这似乎不是我们想要的,如何变成 0,1,2 呢?我们可以将它包裹在立即执行函数内:
var events = new Array();
for(var i = 0;i < 3;i++){
(function(n){
events[i] = function(){console.log(n)};
})(i);
}
events.forEach(e => {
e();
});
因为每个变量 i
都是独立的,所以最终的输出为:0,1,2。
更为快捷的方法是直接将 var
替换为 let
。
为何这是可行的呢?哪怕 let i 处于块级作用域,按照作用域原则,访问 i 时,它还是为 3。
JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量 i 时,就在上一轮循环的基础上进行计算。
我们可以认为 let 声明的循环变量,JavaScript 每次都会独立产生一个块级作用域,所以,每个 i
是处于不同作用域的,使得输出结果不同。
for 循环部分的模拟代码可以看作:
//……
let i = 0;
if(i < 3){
let k = i;
events[i] = function(){console.log(k)};
}
i++;
if(i < 3){
let k = i;
events[i] = function(){console.log(k)};
}
i++;
if(i < 3){
let k = i;
events[i] = function(){console.log(k)};
}
i++;
//……
作用域链(Scope chain)
前面简单介绍了:内部作用域内可以访问外部作作用域的对象。
这种不在内部作用域但可以访问的变量叫做自由变量。
var text = "Hello,world!"
function print(){
console.log(text);//这里的 text 就是自由变量
}
print();
上述代码中,print
方法中并没有定义 text
变量,于是在访问它时,将向上一层作用域寻找 text
,并找到了它。
作用域链表示的便是作用域之间的层级关系。
自由变量的取值值得考量:
var text = "Hello,world!"
function print(){
console.log(text);
}
function test(){
var text = "Hi!";
print();
}
test();
如果自由变量的取值是简单地按照代码向上一级的作用域寻找变量,那么输出结果无疑是:Hi!。但在这里他的输出是:
Hello,world!
所以,自由变量的取值需要找到创建函数的作用域然后再向上寻找,而非调用函数的作用域。
var i = 10;
function sum(){
var j = -10;
return (function(){
i += j;
});
}
var j = 10;
sum()();
console.log(i);
0 或 20?
注意,它的流程为:调用 sum()()
> i += j
> 变量 j
为自由变量 > 从创建该函数的上一级( sum )寻找 > var j = -10
,所以,结果为 0。