【JavaScript】浅聊作用域和作用域链

772 阅读8分钟

作用域(Scope)


1.什么是作用域?

作用域直接影响代码的执行逻辑和变量的生命周期

作用域决定了程序中其中存放的变量、函数、对象是都可以在当前环境被访问或修改。

见名知其义,大白话讲:作用域就是一个代码区块中各种资源可以发挥作用的范围

在正式介绍作用域之前,我们先来看一个代码示例体会一下作用域的概念:

function outer(){
  var innerVar = '函数内部变量'
  console.log(innerVar) // '函数内部变量'
} 
outer() // 需要调用函数,不然不知到里面是什么
console.log(innerVar) // ReferenceError: innerVar is not defined

变量innerVar在全局范围内没有声明。所以在outer函数体范围外的全局作用域下取值会报错。

那么我们就可以如此理解:作用域相当于一块单独的区域,将各个区域的变量隔离开,防止暴露。同时避免变量数量非常庞大的情况下污染命名空间。


2.全局作用域

特点

  • 在任何地方都可以访问
  • 生命周期与应用程序相同,和页面同时被销毁
  • 使用var声明的全局变量会挂载到window对象作为其中的一个属性

注意事项

  • 全局变量过多会污染全局命名空间,增加命名冲突的风险,加剧调试、后期维护的难度

以下几种情况拥有全局作用域:

在所有函数和代码块之外声明的变量、函数、对象拥有全局作用域

// 全局作用域示例
var globalVar = '我是var全局变量'
let globalLet = '我是let全局变量,但不会挂到window上'
const globalConst = '我是全局常量'

function globalScope1() {    //最外层函数
    console.log(globalVar)        // "我是var全局变量"
    console.log(globalLet)        // "我let全局变量,但不会挂到window上"
    console.log(window.globalVar)      // "我是全局变量" (var特有)
    console.log(window.globalLet)      // undefined (let不会挂载到window)
    
    var innerVar = '我是内层变量'
    function innerFun() { //内层函数
        console.log(innerVar)
    }
}

globalScope1() //最外层函数,可访问
console.log(globalVar)  // "我是var全局变量"
console.log(innerVar)  // ReferenceError: innerVar is not defined
innerFun()  // innerFun is not defined

所有未定义直接赋值的变量自动声明为拥有全局作用域

function globalScope2() {
    variable = "未定义直接赋值的变量"
    var inVariable2 = "内层变量2"
}
globalScope2()  //要先执行这个函数,否则根本不知道里面是啥
console.log(variable)  //未定义直接赋值的变量
console.log(inVariable2)  // inVariable2 is not defined

所有window对象的属性拥有全局作用域

一般来说,window对象的内置属性例如window.namewindow.locationwindow.top等都拥有全局作用域。


3. 函数作用域

特点

  • 一般只能在创建函数内部访问
  • 每调用一次函数,都会创建新的作用域
  • 生命周期与创建函数相同,对应执行上下文结束后一起被销毁

注意事项

  • var声明的变量具有函数作用域,存在变量提升问题(变量声明提升,赋值不提升)
  • ES6标准发布后使用letconst的块级作用域可在函数中限制变量的作用范围
function functionScope() {
    var funcVar = '函数内的var变量';
    let funcLet = '函数内的let变量';
    
    if (true) {
        var innerVar = 'if块内的var变量';  // 实际上属于函数作用域
        let innerLet = 'if块内的let变量';  // 属于块级作用域
    }
    
    console.log(funcVar);    // "函数内的var变量"
    console.log(funcLet);    // "函数内的let变量"
    console.log(innerVar);   // "if块内的var变量" (可访问)
    console.log(innerLet);   // ReferenceError: innerLet is not defined
}

functionScopeDemo();
console.log(funcVar);  // ReferenceError: funcVar is not defined

看了上面的示例代码肯定有小伙伴要问了:为什么同样是在if代码块里,innerLet变量不可以在代码块外访问,innerVar变量就可以访问? 这个var关键字是不是在搞特殊?

那么我们接下来就要介绍从ES6标准发布后新增的两个关键字letconst,以及JS中的第三个作用域类型:块级作用域


4.块级作用域

特点

  • 禁止重复声明
  • 只能在指定代码块中被访问
  • 任何一对花括号{}包裹的代码块都有自己的作用域
  • 常用于在代码块中声明局部变量,特别是在循环 for 或条件 if 判断中。
  • 使用letconst声明的变量仅在块中生效,变量声明不会提升到代码块顶部

注意事项

  • var声明的变量不受块级作用域限制
function functionScope() {
    var funcVar = '函数内的var变量';
    let funcLet = '函数内的let变量';
    
    if (true) {
        var innerVar = 'if块内的var变量';  // 实际上属于函数作用域
        let innerLet = 'if块内的let变量';  // 属于块级作用域
    }
    
    console.log(funcVar);    // "函数内的var变量"
    console.log(funcLet);    // "函数内的let变量"
    console.log(innerVar);   // "if块内的var变量" (可访问)
    console.log(innerLet);   // ReferenceError: innerLet is not defined
}

functionScopeDemo();
console.log(funcVar);  // ReferenceError: funcVar is not defined

以下是几种块级作用域的代码示例:

if判断语句绑定块级作用域

if (true) {
    var blockVar = '块内的var变量';  // 实际上会提升到函数或全局作用域
    let blockLet = '块内的let变量';  // 真正的块级作用域
    const blockConst = '块内的const常量';
    
    console.log(blockVar);   // "块内的var变量"
    console.log(blockLet);   // "块内的let变量"
    console.log(blockConst); // "块内的const常量"
}

console.log(blockVar);   // "块内的var变量" (可访问)
console.log(blockLet);   // ReferenceError: blockLet is not defined
console.log(blockConst); // ReferenceError: blockConst is not defined

for循环语句绑定块级作用域

// for循环中的块级作用域
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 输出 0, 1, 2 (每个i有独立作用域)
}

for (var j = 0; j < 3; j++) {
    setTimeout(() => console.log(j), 100); // 输出 3, 3, 3 (共享同一个j)
}

PS: 知识点小拓展

以上例子应该有同学已经发现了,var声明的变量总是会在一些地方搞特殊,其实这一结果是由于var变量的变量提升let变量的暂时性死区这些特性。

举个🌰你就明白:

你写的var相关代码

function varExample() {
   if (true) {
     var x = 10; // 整个函数内都可用
   }
   console.log(x); // 输出 10
}

以上代码看似天衣无缝形成块级作用域,但使用var定义的变量会在JS引擎中编译成这样:

JS引擎编译出的相关代码

function varExample() {
   var x  // 变量声明提升到这
   if (true) {
     // 由于声明被提升到整个代码的顶部,整个函数内都可用var变量
     // 此时x的作用域变成了 函数作用域 而非 块级作用域
     x = 10; 
   }
   console.log(x); // 输出 10
}

由于声明被提升到整个代码的顶部,整个函数内都可用var变量。所以var定义的变量并不会被块级作用域影响而是保持全局作用域或函数作用域。

同时,var变量可以被重复声明和赋值,所以在实际开发中不提倡使用var进行变量声明。


let变量相关代码

function letExample() {
   if (true) {
     let y = 20;
     console.log(y); // 输出 20  
   }
   // let拥有块级作用域 只能在对应代码块生效,所以if代码块变为"暂时性死区",外部无法读取
   console.log(y); // 报错: y is not defined
}

let不存在变量提升,形成块级作用域且只在对应块中生效,外部无法访问。这就是暂时性死区

其中涉及到JS运行机制中的词法分析(Tokenizing)语法分析(Parsing) 生成AST以及代码生成 。这里就不过多赘述了。


作用域链  -- 超市购物为例

我们假设一个场景:

你要买一瓶酱油和一瓶料酒,你在调料货架的酱油区域找到了需要的酱油,但没有找到需要的料酒

接下来你该怎么办? 我们一起带着这个思考往下继续


1.什么是自由变量?

假设执行过程中使用了当前作用域没有定义的变量a,则a此时被称作自由变量。

由于我们现在在酱油区域前,在当前范围内没有摆放料酒,这个时候的料酒就相当于我们说的自由变量

取值需要去创建自由变量所在的函数的那个域 中寻找,而非调用的那个域。本文侧重点不在此,下文简单说成父级作用域


2.作用域链

料酒不能不要,按照生活惯性,我们会在 调料货架 这一上级大货架 (父级作用域) 中寻找目标,如果还是没找到,则询问工作人员寻找整个超市 (全局作用域) 是否有需要的目标

这样一层层向上寻找自由变量的关系就叫做作用域链

🌰

var a = 100
function F1() {
    var b = 200
    function F2() {
        var c = 300
        console.log(a) // 自由变量,顺作用域链向父作用域找
        console.log(b) // 自由变量,顺作用域链向父作用域找
        console.log(c) // 本作用域的变量
    }
    F2()
}
F1()

文章小结

  • 探讨比较了JS中作用域类型:全局作用域函数作用域块级作用域
  • 简单对变量声明的letconstvar关键字进行了异同比较
  • 介绍了作用域链的查找执行规则

通过这些作用域规则,可以更好地组织代码结构,避免变量污染和命名冲突,编写出更健壮、维护性强的JS代码。