【前端面试小册】JS-第11节:面试必会 - 作用域专题
一、什么是作用域
1.1 定义
作用域是一套规则,用于确定在何处以及如何查找变量,也就是确定当前执行代码对变量的访问权限。
类比理解:作用域就像房子的房间。每个房间(作用域)都有自己的物品(变量),你不能从客厅直接拿卧室里的东西,必须进入卧室才能拿到。
1.2 基本示例
function demo() {
var name = "成都巴菲特";
}
demo();
console.log(name); // Uncaught ReferenceError: name is not defined
分析:
name是在函数作用域中声明的- 全局作用域中直接取不到
- 作用域起到了隔离变量的作用
1.3 作用域的作用
- 隔离变量:不同作用域下的变量互不干扰
- 避免命名冲突:不同作用域下的同名变量不会冲突
- 控制变量生命周期:作用域决定变量的创建和销毁时机
1.4 作用域类型
作用域主要包含:
- 全局作用域
- 局部作用域:
- 函数作用域
- 块级作用域(ES6)
二、全局作用域
2.1 概念
全局作用域里面的变量(全局变量),你在任何地方都可以使用它,包括在函数内部。
2.2 注意事项
var声明的全局变量挂在window对象上(浏览器环境下)let声明的全局变量挂在script上,不是window上- 函数内部的变量,没有用
var关键字,该变量也是全局变量 - 全局变量在页面关闭后,所占用内存才会释放
2.3 全局变量示例
Demo 1:作用域链查找
var name = "成都巴菲特"; // 最外层变量
function demo() {
var name1 = "知识星球:前端职场圈";
function innerFn() { // 内层函数
console.log(name1); // 通过作用域链找到外层函数的 name1
}
innerFn();
}
console.log(name); // 成都巴菲特
demo(); // 输出:知识星球:前端职场圈
console.log(name1); // 输出:name1 is not defined(name1 是内部变量,外部无法直接访问)
作用域链查找流程:
graph TD
A[innerFn 查找 name1] --> B{innerFn 作用域有?}
B -->|无| C[demo 作用域]
C --> D{有 name1?}
D -->|有| E[返回 知识星球:前端职场圈]
D -->|无| F[全局作用域]
F --> G{有 name1?}
G -->|无| H[ReferenceError]
Demo 2:未声明直接赋值的变量
// 未定义直接赋值的变量会被声明为全局变量
function demo() {
name = "成都巴菲特"; // 没有 var/let/const,成为全局变量
var name1 = "公众号:前端面试资源";
}
demo(); // 必须先执行
console.log(name); // 成都巴菲特
console.log(name1); // name1 is not defined
关键点:
name = "成都巴菲特"没有使用var/let/const,成为全局变量- 必须执行
demo()后,name才会被声明
Demo 3:未执行函数的情况
function demo() {
name = "成都巴菲特";
var name1 = "公众号:前端面试资源";
}
console.log(name); // name is not defined
原因:因为没有执行 demo(),所以没有去声明 name 这个变量,也不会挂在 window 上,所以访问不到。
三、函数作用域
3.1 概念
属于这个函数的全部变量都可以在整个函数范围内使用。
函数作用域(局部作用域)的优势:
- 避免全局变量命名冲突
- 减少内存消耗(全局变量只会在页面销毁后所占内存才会释放,而函数作用域一般在函数执行后,局部变量会被销毁)
3.2 局部变量特点
- 只能在函数内部访问
- 不同函数可以使用相同名称变量(互不干扰)
- 在函数执行时创建,函数执行完以后自动销毁
3.3 面试题示例
Demo 1:函数内变量与外部变量重名
var name = '公众号:前端面试资源'
function demo() {
console.log(name) // undefined
var name = '知识星球:前端职场圈'
console.log(name) // 知识星球:前端职场圈
name = '成都巴菲特'
console.log(name) // 成都巴菲特
}
console.log(name) // 公众号:前端面试资源
demo()
输出顺序:
公众号:前端面试资源(全局作用域)undefined(函数作用域,变量提升)知识星球:前端职场圈(函数作用域,已赋值)成都巴菲特(函数作用域,重新赋值)
涉及知识点:
- 变量提升:
var声明的变量会提升到函数顶部 - 作用域链:优先查找当前作用域,再向上查找
等价代码:
var name = '公众号:前端面试资源'
function demo() {
var name; // 变量提升
console.log(name) // undefined
name = '知识星球:前端职场圈'
console.log(name)
name = '成都巴菲特'
console.log(name)
}
作用域链查找:
graph TD
A[demo 函数内查找 name] --> B{当前作用域有 name?}
B -->|有| C[使用当前作用域的 name]
B -->|无| D[向上查找全局作用域]
C --> E[变量提升,值为 undefined]
Demo 2:外部无法访问局部作用域
function demo() {
var name = '成都巴菲特'
console.log(name) // 成都巴菲特
}
demo()
console.log(name) // Uncaught ReferenceError: name is not defined
原因:函数内部定义的局部变量外部无法访问。
四、块级作用域
4.1 概念
函数不是唯一的作用域单位,还有块作用域。
块作用域:变量和函数可以属于所处作用域,也可以属于某个代码块(如 {}、if、for 等)。
注意:ES5 没有块级作用域,ES6 才有(通过 let、const 实现)。
4.2 块级作用域解决的问题
问题 1:变量提升导致的问题
// ❌ ES5 的问题
var name = '成都巴菲特'
function demo() {
console.log(name); // undefined(不是预期的 成都巴菲特)
if (0) {
var name = '前端职场圈';
}
}
demo(); // undefined
问题分析:
// 等价于(变量提升)
var name = '成都巴菲特'
function demo() {
var name; // 变量提升到函数顶部
console.log(name); // undefined
if (0) {
name = '前端职场圈';
}
}
原因:var 声明的变量属于函数作用域,即使写在 if 块中,也会提升到函数顶部。
解决方案:使用 let 创建块级作用域
// ✅ ES6 的解决方案
let name = '成都巴菲特'
function demo() {
console.log(name); // 成都巴菲特
if (0) {
let name = '前端职场圈'; // 块级作用域,不影响外层
}
}
demo(); // 成都巴菲特
问题 2:循环变量泄露
// ❌ ES5 的问题
var name = '成都巴菲特'
for (var i = 0; i < name.length; i++) {
console.log(name[i]);
}
console.log(i); // 5(变量泄露到全局)
问题分析:
- 因为 ES5 没有块级作用域,
var声明的变量,无论在哪里声明,最终都属于外部作用域 - 这里的外部作用域就是全局,所以
for循环完毕,变量i泄露为全局变量
解决方案:使用 let 创建块级作用域
// ✅ ES6 的解决方案
let name = '成都巴菲特'
for (let i = 0; i < name.length; i++) {
console.log(name[i]);
}
console.log(i); // ReferenceError: i is not defined
总结:由于 ES5 没有块级作用域,可能带来一些不合理的场景,所以 ES6 的块级作用域诞生了,可以解决这些问题。
五、面试题补充
5.1 Demo 1:let vs var 在块级作用域
{
let name = '成都巴菲特';
}
console.log(name); // ReferenceError: name is not defined
{
var name = '成都巴菲特';
}
console.log(name); // '成都巴菲特'
分析:
let声明的变量,只在他的代码块内有效var存在变量提升,相当于在全局声明var name = undefined;然后代码块内赋值:name = '成都巴菲特'
5.2 Demo 2:未声明的变量
console.log(name); // ReferenceError: name is not defined
{
name = '成都巴菲特';
console.log(name); // '成都巴菲特'
}
console.log(name); // '成都巴菲特'
分析:
- 块级作用域中默认声明的变量(不用
var、let、const),只有当代码执行完声明语句(name = '成都巴菲特')之后,才能访问该变量 - 默认声明的变量会被提升到全局作用域,但是要在声明语句之后才能被访问到
5.3 Demo 3:块级作用域中的函数声明
console.log(fn); // undefined
{
function fn(){}
}
分析:块级作用域的函数声明类似于 var 声明变量,会提升到全局作用域,同时函数声明还会提升到所在的块级作用域头部,所以是 undefined。
5.4 Demo 4:函数声明与变量声明的优先级
function demo() {
console.log(fn); // [Function: fn]
var fn = 1;
function fn() {}
console.log(fn); // 1
}
demo();
输出结果:
[Function: fn]1
关键点:
- 函数声明、变量声明(ES5)都会置顶
- 函数声明优先级高于变量声明
- 变量赋值语句(
var fn = 1)会被拆分成两部分:- 声明置顶:
var fn - 赋值部分:
fn = 1
- 声明置顶:
- 函数与变量重名时,函数优先级高于变量声明,但会被变量赋值覆盖
等价代码:
function demo() {
function fn() {} // 函数声明提升
var fn; // 变量声明提升(但不会覆盖已声明的函数)
console.log(fn) // [Function: fn]
fn = 1; // 赋值覆盖
console.log(fn) // 1
}
六、作用域链
6.1 概念
作用域链是 JavaScript 引擎查找变量的一套规则。
查找规则:
- 从当前作用域开始查找
- 如果找不到,向上一级作用域查找
- 一直找到全局作用域
- 如果全局作用域也没有,抛出
ReferenceError
6.2 作用域链示例
var global = 'global';
function outer() {
var outerVar = 'outer';
function inner() {
var innerVar = 'inner';
console.log(innerVar); // inner(当前作用域)
console.log(outerVar); // outer(向上查找)
console.log(global); // global(继续向上查找)
}
inner();
}
outer();
作用域链查找流程:
graph TD
A[inner 函数查找变量] --> B{inner 作用域有?}
B -->|有| C[使用当前作用域]
B -->|无| D[outer 作用域]
D --> E{outer 作用域有?}
E -->|有| F[使用 outer 作用域]
E -->|无| G[全局作用域]
G --> H{全局作用域有?}
H -->|有| I[使用全局作用域]
H -->|无| J[ReferenceError]
七、总结
核心知识点
-
作用域类型:
- 全局作用域
- 函数作用域(局部作用域)
- 块级作用域(ES6)
-
变量声明:
var:函数作用域,存在变量提升let/const:块级作用域,不存在变量提升
-
作用域链:
- 从当前作用域向上查找
- 直到找到或到达全局作用域
-
优先级:
- 函数声明 > 变量声明
- 变量赋值会覆盖函数声明
常见面试题
Q1: var、let、const 的区别?
答:
var:函数作用域,存在变量提升,可重复声明let:块级作用域,不存在变量提升,不可重复声明,存在暂时性死区const:块级作用域,不存在变量提升,不可重复声明,必须初始化,不可重新赋值
Q2: 什么是作用域链?
答:作用域链是 JavaScript 引擎查找变量的一套规则。从当前作用域开始,向上查找,直到找到或到达全局作用域。
Q3: 为什么需要块级作用域?
答:ES5 只有函数作用域,导致循环变量泄露、变量提升等问题。ES6 的块级作用域(let/const)可以解决这些问题。
实战建议
- ✅ 新项目优先使用
let/const,避免使用var - ✅ 理解作用域链有助于理解闭包
- ✅ 注意函数声明和变量声明的优先级
- ✅ 理解变量提升和暂时性死区的区别
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!
美的集团公司福利 780人发布