JS 变量和作用域

JS 变量与作用域知识点大全详解

一、变量声明的三种方式:var /let/const

1. var(ES5 旧声明方式)

特性
  1. 变量提升 :声明会提升到当前作用域顶部,初始值 undefined

    console.log(a); // undefined
    var a = 10;

  2. 函数作用域:仅区分函数,无块级作用域(if/for 块内声明全局可见)

    if(true){
    var num = 100;
    }
    console.log(num); // 100 块外可访问

  3. 允许重复声明:同一作用域重复声明不报错,会覆盖值

    var x = 1;
    var x = 2;
    console.log(x); // 2

  4. 挂载到全局 window:全局 var 变量会成为 window 属性

    var test = 99;
    console.log(window.test); // 99

2. let(ES6 块级变量)

  1. 块级作用域{}、if、for、while 内部声明,块外无法访问

  2. 存在变量提升,但暂时性死区 TDZ:声明前访问直接报错,不能提前使用

    console.log(b); // Uncaught ReferenceError
    let b = 20;

  3. 禁止重复声明:同一作用域重复声明直接报错

  4. 全局 let 不会挂载到 window 对象

3. const(ES6 常量)

  1. 拥有 let 全部特性:块级作用域、暂时性死区、不可重复声明

  2. 声明时必须赋值,不赋值直接报错

    const c; // 语法错误

  3. 不能直接重新赋值 :基础类型值无法修改;引用类型(对象 / 数组)地址不变即可修改内部属性

    // 基础类型 不可修改
    const age = 18;
    age = 20; // 报错

    // 引用类型 内部可修改
    const user = { name:"小明" };
    user.name = "小红"; // 合法,只是修改属性,地址没变
    user = {}; // 报错,重新赋值改变地址

var /let/const 对比速查表

表格

特性 var let const
作用域 函数级 块级 块级
变量提升 ✅ 提升,值 undefined ✅ 提升,TDZ 死区 ✅ 提升,TDZ 死区
重复声明 允许 禁止 禁止
全局挂载 window
重新赋值 ✅ 任意修改 ✅ 任意修改 ❌ 不可重赋值
初始赋值 可选 可选 必须赋值

二、变量提升(Hoisting)

1. 概念

JS 代码执行前会进行预编译,扫描当前作用域所有变量、函数声明,提前存入内存,这就是变量提升。 赋值操作不会提升,只有声明提升。

2. 提升优先级

函数声明 > var 变量声明

复制代码
console.log(fn); // 完整函数
var fn = 10;
function fn(){}

3. 暂时性死区 TDZ(let/const 独有)

从作用域顶部到 let/const 声明语句之间的区域,称为暂时性死区,该区域内禁止访问变量,触发引用错误。 目的:杜绝变量提前使用,规范代码书写顺序。

三、作用域

1. 作用域定义

变量、函数可被访问的有效代码区域,用来隔离变量,不同作用域同名变量互不干扰。

2. 四大作用域分类

(1)全局作用域
  • 页面打开自动生成,整个 JS 文件都能访问;
  • 直接在 script 顶层声明的变量 / 函数属于全局;
  • 生命周期:页面关闭才销毁;
  • 弊端:全局变量过多容易出现命名冲突、污染全局。
(2)函数作用域(ES5)
  • 每个函数调用时生成独立作用域;
  • var 变量仅在当前函数内可用,函数外部无法访问;
  • 函数执行完毕,内部变量自动销毁,节省内存。
(3)块级作用域(ES6 新增,let/const)

{} 包裹的代码块(if、for、while、单独大括号)形成独立作用域,仅 let/const 生效。

复制代码
{
  let msg = "块内";
}
console.log(msg); // 报错,块外不可访问
(4)模块作用域(ES6 Module)

每个 .js 文件导入导出后自成模块,内部变量默认隔离,需 export 导出才能外部访问。

3. 作用域链

形成规则

函数嵌套时,内层函数作用域保存外层函数作用域引用,一层一层向上查找,串联形成作用域链

变量查找规则(就近原则)
  1. 优先在当前作用域查找变量,找到直接使用;
  2. 当前没有,沿着作用域链向上一层外层作用域查找;
  3. 一直找到全局作用域,仍未找到则报错 ReferenceError

示例:

复制代码
let a = 10; // 全局
function outer(){
  let a = 20; // 外层函数
  function inner(){
    console.log(a); // 20,优先取就近外层,不会找到全局
  }
  inner();
}
outer();

四、执行上下文(作用域运行载体)

1. 概念

代码执行时创建的环境载体,作用域是静态代码区域,执行上下文是代码运行时动态环境。 分两类:

  1. 全局执行上下文:页面加载自动创建,唯一;
  2. 函数执行上下文:每次调用函数都会新建,调用结束销毁。

2. 执行上下文三部分

  1. 变量对象 VO:存放当前作用域变量、函数、形参;
  2. 作用域链:自身 VO + 所有外层作用域 VO;
  3. this 指向:全局 window、函数调用者、构造函数实例。

3. 调用栈(执行栈)

先进后出,存放所有正在运行的执行上下文:

  1. 全局上下文最先入栈,永远在栈底;
  2. 调用函数,函数上下文压入栈顶;
  3. 函数执行完毕,上下文弹出销毁;
  4. 页面关闭,全局上下文出栈。

五、自由变量 & 闭包(作用域延伸重点)

1. 自由变量

一个变量在当前作用域没有定义,去外层作用域取值,该变量就是自由变量。 自由变量取值定义时的作用域,和调用位置无关。

2. 闭包

定义

内层函数能够访问外层函数作用域变量,且内层函数在外层函数执行完毕后依然被外部引用,形成闭包。 形成条件:函数嵌套 + 内层函数引用外层变量 + 内层函数外部执行。

闭包作用
  1. 保存私有变量,实现数据私有化;
  2. 延长变量生命周期,外层函数销毁,变量依然存在;
  3. 模块化、防抖节流、封装工具函数。
闭包弊端

变量长期驻留内存,不及时释放易造成内存泄漏,不用时手动置空引用销毁。

示例:

复制代码
function count(){
  let num = 0; // 私有变量
  return function(){
    num++;
    console.log(num);
  }
}
const add = count();
add(); //1
add(); //2

六、变量生命周期

  1. 全局变量:页面初始化创建,页面关闭销毁;长期占用内存,少用。
  2. 函数局部变量(var/let/const):函数调用时创建,函数执行结束自动销毁。
  3. 闭包内变量:函数执行完不销毁,直到闭包引用断开才释放。
  4. 块级变量:代码块执行结束立刻销毁。

七、污染全局作用域的场景与解决方案

污染场景

  1. 直接使用 var 声明全局变量;

  2. 函数内不声明直接赋值(隐式全局变量)

    function fn(){
    test = 100; // 未声明,自动挂载window,全局污染
    }

  3. 无模块化时多个 JS 文件同名变量覆盖。

解决方案

  1. 全部使用 let / const 替代 var;
  2. 严格模式 use strict,禁止隐式全局变量(未声明直接赋值直接报错);
  3. 使用 IIFE 立即执行函数隔离作用域;
  4. ES6 Module / CommonJS 模块化开发;
  5. 统一命名空间封装变量。

八、严格模式 use strict 对变量的限制

  1. 禁止不声明直接赋值创建隐式全局变量;
  2. 禁止删除变量 delete x
  3. 函数参数不能重名;
  4. 静态绑定作用域,不允许 with 篡改作用域链。

九、高频面试核心总结

  1. 声明优先级:const > let 优先使用,减少 var;常量用 const,可变变量用 let。
  2. 作用域查找遵循就近原则,沿着作用域链向上查找。
  3. 变量提升仅提升声明,赋值不动;let/const 存在暂时性死区,不能提前访问。
  4. 作用域是静态代码划分,作用域链在函数定义时生成,不是调用时。
  5. 闭包本质是作用域链的保留,可私有化数据,但需注意内存泄漏。
  6. var 无块级作用域,容易全局污染,ES6 开发完全弃用 var。