深夜内耗不如爬起来学 JS:0 基础也能吃透作用域的“心理边界”

134 阅读7分钟

一、什么是作用域

作用域指一个变量的作用的范围。 简单来说,作用域的作用是存放变量的值,并能在之后对这个值进行访问和修改。作用域的使用可以提高程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。

在JavaScript中有两种作用域类型:

(1)全局作用域: 全局作用域是最外层的作用域,在浏览器中,全局作用域被认为是window对象

(2)函数作用域(局部作用域): 函数作用域是函数被调用时创建的作用域,函数执行完毕后,函数作用域就会被销毁

(3)块级作用域(es6新增): 块级作用域是由{}包括的作用域,在es6中,let和const命令都可以创建块级作用域

二、作用域的巧妙使用

接下来我们通过代码,边学边理解

示例1

var a = 10; 
function fn() 
    { 
    console.log(a); 
    var a = 20; 
    } 
fn();

1. 代码中的作用域划分

javascript

var a = 10;         // 全局作用域变量 a
function fn() {     // 函数作用域开始
    console.log(a); // 访问当前作用域的 a(变量提升但未赋值)
    var a = 20;     // 声明并赋值函数作用域变量 a
}                   // 函数作用域结束
fn();               // 调用函数

2. 全局作用域(Global Scope)

  • 变量 a:在全局作用域中声明并初始化为 10
  • 函数 fn:在全局作用域中定义,但函数内部的代码属于独立的函数作用域

3. 函数作用域(Function Scope)

  • 变量提升(Hoisting)
    var a 声明被提升到函数作用域的顶部,但赋值保留在原处。
    因此,函数内部的变量 a 在整个函数作用域内都存在,但在赋值前值为 undefined
  • 作用域屏蔽(Shadowing)
    函数内部声明的 a 与全局变量 a 同名但不同源,它会屏蔽(Shadow)全局变量的访问。

4. 执行流程详解

  1. 全局作用域初始化

    • 创建变量 a 并赋值为 10
    • 定义函数 fn,其内部代码形成独立作用域。
  2. 调用 fn()

    • 进入函数作用域,变量 a 被提升(值为 undefined)。
    • console.log(a):访问当前作用域的 aundefined)。
    • a = 20:将函数内部的 a 赋值为 20不影响全局变量
  3. 函数执行结束

    • 函数作用域销毁,全局变量 a 仍为 10

5. 输出结果

plaintext

undefined

6. 关键概念总结

概念解释
全局作用域代码最外层的作用域,变量和函数可被全局访问。
函数作用域每个函数创建独立作用域,内部变量无法被外部访问。
变量提升var 声明的变量会被提升到当前作用域顶部,但赋值不会提升。
作用域屏蔽函数内部声明的变量会屏蔽同名的全局变量(若同名)。

7. 对比实验

实验 1:移除函数内部的 var

javascript

var a = 10;
function fn() {
    console.log(a); // 访问全局变量 a(值为 10)
    a = 20;         // 修改全局变量 a
}
fn();
console.log(a); // 输出 20(全局变量被修改)
实验 2:使用 let/const 声明

javascript

var a = 10;
function fn() {
    console.log(a); // 报错:Cannot access 'a' before initialization(暂时性死区)
    let a = 20;     // let 声明的变量不提升赋值前不可用
}
fn();

总结

  • 全局作用域:变量和函数可被全局访问,但易被污染。
  • 函数作用域:通过 var 声明的变量具有函数作用域,内部变量会屏蔽同名全局变量。
  • 变量提升:导致函数内部的 var 变量在声明前已存在(值为 undefined)。
  • 最佳实践:优先使用 let/const 并避免变量命名冲突,减少对全局作用域的依赖。

示例二:

第一步:定义对象和函数

javascript

var o1 = { a: 1 };      // 全局作用域:创建对象 o1,包含属性 a=1
var o2 = { b: 1 };      // 全局作用域:创建对象 o2,包含属性 b=1

function foo(obj) {     // 全局作用域:定义函数 foo
    with(obj) {         // 将 obj 的属性添加到作用域链前端
        a = 2;          // 赋值操作:尝试在当前作用域链中查找变量 a
    }
}
第二步:调用 foo(o1)
  1. 进入 with 块obj 是 o1(包含 a: 1)。

  2. 执行 a = 2

    • with 块的作用域链前端是 o1,其中存在属性 a
    • 赋值操作直接修改 o1.a,值变为 2
  3. 输出 o1

    javascript

    { a: 2 }  // o1.a 被修改
    
第三步:调用 foo(o2)
  1. 进入 with 块obj 是 o2(包含 b: 1)。

  2. 执行 a = 2

    • with 块的作用域链前端是 o2,其中没有属性 a
    • JavaScript 继续在全局作用域查找 a,未找到。
    • 隐式全局变量:由于未使用 var/let/const 声明,a = 2 在全局作用域创建变量 a(值为 2)。
  3. 输出 o2

    javascript

    { b: 1 }  // o2 未被修改
    
第四步:输出全局变量 a

javascript

2  // 由 foo(o2) 隐式创建的全局变量

2. 关键知识点

with 语句的作用
  • 将对象的属性添加到作用域链的前端,允许直接访问对象属性而无需 obj. 前缀。
  • 危险特性:可能导致变量查找路径模糊,增加代码复杂性。
变量赋值规则
  1. 优先修改现有变量
    若作用域链中存在变量 a(如 o1.a),则直接修改。
  2. 隐式全局变量
    若作用域链中不存在 a,则在全局作用域创建新变量(非严格模式下)。

3. 输出结果

javascript

{ a: 2 }    // o1 被修改
{ b: 1 }    // o2 未被修改
2           // 全局变量 a 被隐式创建

4. 严格模式下的差异

若代码在严格模式('use strict';)下执行:

  • 错误foo(o2) 中 a = 2 会抛出 ReferenceError,因为无法隐式创建全局变量。
  • 安全性:严格模式禁止隐式全局变量,强制声明变量。

5. 最佳实践

避免使用 with 语句

  • 现代 JavaScript 已不推荐使用 with,因其会导致作用域链模糊,增加调试难度。

  • 改用显式对象属性访问(如 obj.a = 2)。

示例改写

javascript

function foo(obj) {
    obj.a = 2;  // 直接修改对象属性,无需 with
}

示例三:

1. 代码片段 1:块级作用域与 let

javascript

if (true) {
    let a = 10;       // 块级作用域变量(仅在 if 内部可见)
    console.log(a);   // 输出 10(访问块内变量)
}
console.log(a);       // 报错:ReferenceError: a is not defined
关键点
  • 块级作用域let/const 声明的变量仅在声明所在的块({})内有效。
  • 暂时性死区(TDZ) :块内的 let a 会屏蔽外部同名变量,且变量在声明前不可用。

2. 代码片段 2:变量提升与 var

javascript

var a = 1;
if (true) {
    console.log(a);   // 输出 undefined(变量提升,但未赋值)
    var a = 10;       // 变量声明被提升到函数/全局作用域顶部
}
执行流程
  1. 变量提升
    var a 被提升到全局作用域顶部,但赋值保留在原处。

    javascript

    var a;            // 提升声明(值为 undefined)
    a = 1;            // 初始赋值
    if (true) {
        console.log(a);  // 访问当前作用域的 a(undefined)
        a = 10;          // 修改当前作用域的 a
    }
    
  2. 输出结果

    plaintext

    undefined
    

3. 对比实验:混合 let 和 var

javascript

let a = 1;
if (true) {
    console.log(a);   // 报错:Cannot access 'a' before initialization
    let a = 10;       // 块内的 let a 屏蔽外部变量,形成 TDZ
}
错误原因
  • 块内的 let a 屏蔽了外部的 a,但 let 不存在变量提升,导致访问时处于 TDZ。

4. 关键概念总结

特性varlet/const
作用域范围函数作用域块级作用域({} 内有效)
变量提升声明会提升,值为 undefined不存在提升(TDZ 限制访问)
重复声明允许(后声明覆盖前声明)不允许(SyntaxError)
全局变量绑定成为 window 属性不绑定