简单理解 JavaScript 的词法作用域
作用域
作用域就是一对{}
,是一个盒子,代码在盒子中运行,按理来说在里面声明的变量(这种变量叫作局部作用域)不能被外界访问,除非访问父盒子的变量。作用域决定了代码生效的区域以及资源(变量、函数)可见的区域。
专注于为中小企业提供网站设计、网站建设服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业丹棱免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了近1000家企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。
function fun() {
let a = 20;
}();
console.log(a); // Uncaught ReferenceError: a is not defined
fun
函数的作用域声明了变量 a,而函数的作用域与console.log(a)
所在的作用域不相同,因此,访问变量就报错了。
块级作用域
JS 中存在一个关键字var
,它也是用来声明变量的,是 ES5 之前的语法了。但是现在的教材和教程中存在其他两个变量声明的关键字:let
和const
。var 在函数作用域内声明变量,外界是不能访问的,但是对于 if、for 这类语句的作用域,var 就具有穿透性。
下面我写了一个作用域,我希望变量 x 是不能被外界访问的:
{
var x = 10;
}
console.log(x) // => 10
for (var i = 0; i < 10; i++) {
// ...
}
console.log(i); // 10
很明显,for 语句、以及单独的一对{}
这种作用域都能被外界访问。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
所以,块级作用域应该完全符合作用域的定义。因为历史原因,JS 还依旧可以使用var
关键字声明变量,上面已经说明这个关键字的弊端,如果坚持使用,除了函数作用域以外,作用域就不再具有块级作用域的功能。
全局作用域
全局作用域的范围比其他的作用域的范围更大。或
.js
可以算作是一个全局作用域,定义在全局作用域的变量叫全局变量。
在全局作用域声明的变量,其他的作用域都可以访问:
let a = 20;
function fun() {
console.log(a) // => 20
}
fun();
局部作用域
定义在局部作用域里面的变量就是局部变量。局部变量只可以在局部作用域生效,局部作用域可以访问到全局作用域的变量,或是比局部作用域大一点的父作用域(嵌套作用域)。
词法作用域
关于词法函数作用域这一块,有一个面试题:
let a = 123;
function fun1() {
console.log(a);
}
function fun2() {
let a = 456;
fun1();
}
fun2();
很容易得出答案是 456,然而,正确的但是 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 调用代表了一种动态的关系。
作用域链
当在函数使用一个变量的时候,首先 JS 会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。
把作用域比喻成一个建筑,这份建筑代表程序中的嵌套作用域链,第一层代表当前的执行作用域,顶层代表全局作用域。
根据词法作用域静态性的原则,函数引用变量不会因为调用顺序和位置从而改变当前的作用域。所以查找变量的位置按照代码书写的位置来看。
面试题
结合词法作用域和作用域链分析上面给出的面试题:
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前端面试 - 面试官系列 - 面试官:说说你对作用域链的理解。
网站栏目:简单理解 JavaScript 的词法作用域
转载源于:http://myzitong.com/article/dsojgjo.html