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

一、什么是作用域

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

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

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

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

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

二、作用域的巧妙使用

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

示例1

js 复制代码
var a = 10; 
function fn() 
    { 
    console.log(a); 
    var a = 20; 
    } 
fn();

1. 代码中的作用域划分

javascript

php 复制代码
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

javascript 复制代码
undefined

6. 关键概念总结

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

7. 对比实验

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

javascript

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

javascript

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

总结

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

示例二:

第一步:定义对象和函数

javascript

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. 进入 withobjo1(包含 a: 1)。

  2. 执行 a = 2

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

    javascript

    css 复制代码
    { a: 2 }  // o1.a 被修改
第三步:调用 foo(o2)
  1. 进入 withobjo2(包含 b: 1)。

  2. 执行 a = 2

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

    javascript

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

javascript

arduino 复制代码
2  // 由 foo(o2) 隐式创建的全局变量

2. 关键知识点

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

3. 输出结果

javascript

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

4. 严格模式下的差异

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

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

5. 最佳实践

避免使用 with 语句

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

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

示例改写

javascript

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

示例三:

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

javascript

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

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

    javascript

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

    plaintext

    javascript 复制代码
    undefined

3. 对比实验:混合 letvar

javascript

ini 复制代码
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. 关键概念总结

特性 var let/const
作用域范围 函数作用域 块级作用域({} 内有效)
变量提升 声明会提升,值为 undefined 不存在提升(TDZ 限制访问)
重复声明 允许(后声明覆盖前声明) 不允许(SyntaxError)
全局变量绑定 成为 window 属性 不绑定
相关推荐
编程绿豆侠5 小时前
力扣HOT100之多维动态规划:62. 不同路径
算法·leetcode·动态规划
鑫鑫向栄5 小时前
[蓝桥杯]剪格子
数据结构·c++·算法·职场和发展·蓝桥杯
羊儿~5 小时前
P12592题解
数据结构·c++·算法
Wendy_robot5 小时前
池中锦鲤的自我修养,聊聊蓄水池算法
程序人生·算法·面试
.Vcoistnt5 小时前
Codeforces Round 1028 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
白熊1885 小时前
【机器学习基础】机器学习入门核心算法:层次聚类算法(AGNES算法和 DIANA算法)
算法·机器学习·聚类
晨曦学习日记6 小时前
力扣题解654:最大二叉树
数据结构·算法·leetcode
PXM的算法星球6 小时前
paoxiaomo的XCPC算法竞赛训练经验
c++·算法
孤独得猿6 小时前
高阶数据结构——并查集
数据结构·c++·经验分享·算法
一只鱼^_6 小时前
力扣第452场周赛
数据结构·c++·算法·leetcode·贪心算法·动态规划·剪枝