简单易懂!清晰地理解 JavaScript 中的作用域、作用域链和词法作用域

什么是作用域?

作用域(scope)是一套规则,规定了在哪里以及如何查找变量(包括函数),或者说规定了变量、函数声明的可访问范围。

1. 局部作用域

实例1:

function fn() {
    var str = "fn1";
    console.log(str);
    // 函数内可访问 str
}

fn();
console.log(str); //此处无法访问 str

实例1代码中,函数 fn 中声明了一个变量 str ,这种是只能在函数内部区域起作用的局部变量,而在函数外部是无法访问的。因此,由函数声明而界定的作用域也被称作函数作用域


实例2:

if (true) {
    let a;
}
console.log(a); // 报错,a 没有定义

while (true) {
    let b;
}
console.log(b); // 报错,b 没有定义

{
    let c;
}
console.log(c); // 报错,c 没有定义

function fn () {
    let d;
}
console.log(d); // 报错,d 没有定义; 这并不奇怪,var 声明也会导致报错

实例2代码中,所有变量都是 let关键字声明的。它们的作用域由最近的一对花括号{}界定,这种被称为块级作用域

const关键字声明的常量也是如此)

2. 全局作用域

在函数或者代码块({})最外层声明的变量是全局变量,在整个 JavaScript 脚本(包括函数和代码块)里都可使用,拥有全局作用域

实例:

var str = "fn1";
function fn() {
    console.log(str);
    // 此处可访问 str
}

fn();

console.log(str); //此处也可访问 str

作用域链

前面说过,作用域是一套根据标识符(变量名、函数名)查找变量的规则。实际开发中,通常会出现多个作用域。当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。

因此,在当前作用域中无法找到某个变量时,JavaScript 引擎就会往上一层作用域中继续查找,依次类推,直到找到该变量,或者到达最外层作用域(也就是全局作用域)为止,这条单向的索引路径就是作用域链(scope chain)

实例1:

var a = 1;
function fn1() {
	let b = 2;
    
	function fn2() {
		let c = b;
        b = a;
        a = c;
		console.log(a, b, c); // 这里可以访问 a、b 和 c 
	}
	
    // 这里可以访问 a 和 b,但不能访问 c
	fn2();
	console.log(a,b);
}

//这里只可以访问 a
fn1();

下图展示了实例 1 代码中的作用域链,以及各个层级作用域中的标识符:

作用域链图示


实例2:

var a = 1;
function fn() {
    let a = 2;
    console.log(a);
}
fn(); // 2

实例 2 代码中,在控制台输出的是 2 而不是 1,因为已经在函数作用域内找到了一个匹配的变量 a 。也就是说在查找到第一个匹配的标识符便会停止查找,这可叫作“短路效应”。因此在不同层级的作用域可以定义同名的标识符而不会被覆盖。

使用 var 在全局作用域中声明的变量会自动成为全局对象(比如浏览器中的 window 对象)的属性,因此可以间接地通过对全局对象属性的引用来对其进行访问。例:window.a;
(let、const 声明不会添加到全局对象中去)
通过这种方式可以访问那些被同名变量所阻断访问的全局变量。但非全局的变量如果被阻断了,无论如何都无法被访问到。


实例3:

function fn() {
    str = "fn1";
    console.log(str);
    // 函数内可访问 str 
}
fn();
console.log(str); //在全局范围也可访问 str

实例 3 代码中,在函数内省略了 var 直接给变量 str 进行了赋值,JavaScript 引擎会根据作用域链的查找规则查找这个变量的声明,如果最终在全局作用域中也找不到,这种情况在非严格模式下,会使得全局作用域中自动创建一个名为 str 的全局变量,但是在严格模式下会报错。在局部作用域中定义全局变量会很难维护,因为一下子很难断定是不是故意省略的,非严格模式下也不推荐使用。

函数参数也被当做函数中的变量,访问规则跟其他变量相同。

词法作用域

JavaScript 中的作用域模型是词法作用域,可以理解为静态作用域。也就是说变量的作用域在你写代码的时候就已经固定了,取决于函数声明和代码块({})的书写位置。

其他块级作用域显然是无法改变的,但是函数作用域呢?这里可以直接抛出结论:无论函数在哪里调用,被如何调用,它的作用域都限定于函数声明所处的位置。

实例1:

var a = 1;
function fn1(){
    console.log(a);  
}
function fn2(){
    let a = 2;
   	fn1(); // 在另一个函数内调用也不会改变作用域嵌套关系
}
fn1(); // 1
fn2(); // 1

词法作用域查找只会查找一级标识符,比如 a、b 和 c。如果代码中引用了obj.bar.beer, 词法作用域查找只会试图查找 obj 标识符,找到这个变量后,对象属性访问规则会分别接管对 bar 和 beer 属性的访问。

严格来说,词法作用域并不是绝对固定的。JavaScript 中有两个机制可以在运行时动态修改作用域:eval( ) 和 with。但都是弊大于利的操作,而且几乎没有必须要使用它们的情况,在严格模式下它们也会被限制,这里就不过多展开了。

总结

  1. 作用域是一套规则,限制着变量和函数声明的可访问范围。
  2. 作用域分为全局作用域、函数作用域和块级作用域。
  3. 作用域之间会发生嵌套关系,子作用域能够访问父作用域的变量,而父作用域不能访问子作用域中的变量。
  4. JavaScript 采用词法作用域,作用域固定在函数声明和代码块({})的书写位置,不会随着函数调用位置而改变。
- END -

感谢你花费宝贵的时间阅读本文,文章在撰写过程中难免有疏漏和错误,欢迎你在下方留言指出文章的不足之处;如果觉得这篇文章对你有用,也欢迎你点赞和留下你的评论哦。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值