前言
关于作用域的有关知识点有全局作用域、局部作用域、函数作用域、块级作用域、词法作用域、作用域链。
作用域
作用域就像是一个教室,上课时教室里面的人互相可见,A 教室里的人不可以看见 B 教室里的人。作用域决定了代码生效的区域以及资源(变量、函数)可见的区域。
function fun() {
let a = 20;
}();
console.log(a); // Uncaught ReferenceError: a is not defined
无法获得fun
函数中定义的变量a
。
全局作用域
全局作用域的范围比其他的作用域的范围更大,关系就像是一切 JavaScript 对象的顶层都是 Object。<script>
或.js
可以算作是一个全局作用域,定义在全局作用域的变量叫全局变量。
在全局作用域声明的变量,其他的作用域都可以访问:
let a = 20;
function fun() {
console.log(a) // => 20
}
fun();
局部作用域
定义在局部作用域里面的变量就是局部变量。局部变量只可以在局部作用域生效,局部作用域可以访问到全局作用域的变量,或是比局部作用域大一点的父作用域(嵌套作用域)。
局部作用域有块级作用域、函数作用域。
块级作用域
在 ES5 及以前,块级作用域受var
影响是无效的,具体请看ES6 关键字 let 和 ES5 及以前关键字 var 的区别。
{
var x = 10;
}
console.log(x) // => 10
for (var i = 0; i < 10; i++) {
// ...
}
console.log(i); // 10
最后打印for
语句的变量 i,得到 10,实际上在语句内部打印最终循环的结果是 9。ES6 之后的关键字let
声明的变量,外部想要使用块级作用域的变量x
就会报错:
{
let x = 10;
}
console.log(x); // Uncaught ReferenceError: x is not defined
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i); // Uncaught ReferenceError: i is not defined
函数作用域
关于函数作用域,有一个面试题,需要结合下面的词法作用域进行分析。
let a = 123;
function fun1() {
console.log(a);
}
function fun2() {
let a = 456;
fun1();
}
fun2();
最终的结果是 123。这是因为词法作用域已经决定了函数fun1
引用的外部作用域的变量是全局作用域中的变量 a,而非函数fun2
定义的局部变量a
。
词法作用域
函数引用变量的静态性
词法作用域(静态作用域)是一种就近原则,也就是在我们写下代码的时候就已经决定了函数引用的变量应该是按照就近原则来引用的:
词法作用域的静态性原则,规定函数引用变量必须按照代码书写的顺序来,即便是函数被其他函数调用了,这个函数的作用域也不会发生变化,也不会因此变成了嵌套函数。
函数自身局部作用域内没有定义变量 a,而在全局作用域中,定义了变量 a,根据就近原则,所以引用的是a = 123
。
let a = 123;
function fun() {
console.log(a); // => 123
}
如果函数体内有一个变量 a,结果就是:
let a = 123;
function fun() {
let a = 456;
console.log(a); // => 456
}
总而言之,函数引用变量时是按照一种自上而下,顺序来的。函数引用一个变量,前提是变量不能在函数声明之后出现。
错误:
fun();
function fun() {
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
}
let a = 123;
正确:
let a = 123;
fun();
function fun() {
console.log(a); // 123
}
这里有一个悬念,为什么调用函数可以在函数声明之前?
函数调用的动态性
函数要成功引用作用域外的变量必须是变量声明在函数之前,但是函数调用可以在函数声明之前,但也必须是在变量声明之后。
在函数调用时体现出作用域的动态性,函数引用变量就体现出作用域的静态性。
function f() { g(); }
function g() {}
f();
当我们调用 f(),它会调用 g()。在执行期间,g 被 f 调用代表了一种动态的关系。
作用域链
当在函数使用一个变量的时候,首先 Javascript 会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。
把作用域比喻成一个建筑,这份建筑代表程序中的嵌套作用域链,第一层代表当前的执行作用域,顶层代表全局作用域。
根据词法作用域静态性的原则,函数引用变量不会因为调用顺序和位置从而改变当前的作用域。所以查找变量的位置按照代码书写的位置来看。
面试题
现在可以回答那一道面试题了函数作用域:
let a = 123;
function fun1() {
console.log(a);
}
function fun2() {
let a = 456;
fun1();
}
fun2();
函数fun2
声明了一个与全局作用域同名的变量 a,根据词法作用域的静态性原则,函数调用不会改变函数的作用域。函数fun1
作用域内没有定义变量a
,它的上级作用域就是全局作用域,而全局作用域中声明了变量 a,所以最终打印结果是 123。
引用文献
- 《JavaScript 权威指南》- 第 3 章 变量作用域;
- 《深入理解 JavaScript》- 第 16 章 变量:作用域、环境和闭包;
- web前端面试 - 面试官系列 - 面试官:说说你对作用域链的理解。