今天我们来聊聊JavaScript中最核心也最让人困惑的概念:作用域、作用域链、执行上下文和闭包。
这些都是面试中的高频考点,更是写出高质量代码的基石!
系列文章目录
解密JavaScript面向对象(一):从新手到高手,手写call/bind实战
解密JavaScript面向对象(二):深入原型链,彻底搞懂面向对象精髓
文章目录
引子
从一个经典面试题开始
先来看这段代码,你能准确说出输出结果吗?
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
如果你的答案是0, 1, 2,那么这篇文章正是你需要的!
让我们从基础开始,彻底搞懂这些问题。
一、作用域:变量访问的规则体系
什么是作用域?
简单来说,作用域就是变量的可访问范围。就像公司的部门权限:财务部的文件,技术部不能随便看。
JavaScript有三种作用域:全局作用域、函数作用域、块级作用域。下面代码中进行说明。
// 1. 全局作用域
var globalVar = "我是全局变量";
function test() {
// 2. 函数作用域
var functionVar = "我是函数内变量";
if (true) {
// 3. 块级作用域 (ES6+)
let blockVar = "我是块级变量";
const constVar = "我是常量";
}
console.log(globalVar); // 可访问
console.log(functionVar); // 可访问
console.log(blockVar); // 报错:blockVar is not defined
}
test();
console.log(functionVar); // 报错:functionVar is not defined
关键区别:var、let、const
// var 的变量提升
console.log(a); // undefined,不会报错
var a = 10;
// let/const 存在暂时性死区
console.log(b); // 报错:Cannot access 'b' before initialization
let b = 20;
// 循环中的差异
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i)); // 输出3次 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j)); // 输出0,1,2
}
二、作用域链:变量查找的"GPS导航"
当我们访问一个变量时,JavaScript会沿着作用域链逐级向上查找。
var global = "全局";
function outer() {
var outerVar = "外部";
function inner() {
var innerVar = "内部";
console.log(innerVar); // 当前作用域找到
console.log(outerVar); // 上级作用域找到
console.log(global); // 全局作用域找到
console.log(notExist); // 报错:沿链查找不到
}
inner();
}
outer();
查找顺序:inner作用域 → outer作用域 → 全局作用域
三、执行上下文:代码执行的"舞台环境"
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
var globalVar = "全局";
function first() {
console.log("first开始");
second();
console.log("first结束");
}
function second() {
var secondVar = "second变量";
console.log("second执行");
}
first();
console.log("全局结束");
我们来观察执行栈的变化:
1)全局上下文入栈
2)first()调用,first上下文入栈
3)second()调用,second上下文入栈
4)second()执行完,second上下文出栈
5)first()执行完,first上下文出栈
6)全局上下文出栈
当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。
对于每个执行上下文,都有三个重要核心(ES6):
1)变量环境:var声明的变量和函数声明
2)词法环境:let/const声明的变量
3)this绑定
PS:ES3规范提出的三个核心是:变量对象、作用域链、this绑定。
协助理解:
作用域链 ≈ 词法环境通过函数调用引用连成的链。
变量对象 ≈ 词法环境 + 变量环境 所管理的存储空间。
ES6的说法更符合其描述底层机制。
变量环境跟词法环境的拆分是为了实现 let/const 的暂时性死区等特性。
console.log(a); // undefined
console.log(b); // 报错
console.log(sayHi); // 函数体
var a = 10;
let b = 20;
function sayHi() {
console.log("Hi");
}
四、闭包(作用域的高级应用)
闭包就是能够访问其他函数作用域的函数。
简单说:函数嵌套函数,内部函数可以访问外部函数的变量。
function createCounter() {
let count = 0; // 私有变量
// 返回的函数形成了闭包
return function() {
count++;
console.log(count);
};
}
// 通过返回函数形成闭包后,可以访问createCounter中的变量count
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
闭包的实战应用场景:防抖
防抖的核心思想是:“等你讲完”。在事件被触发后,会开启一个定时器。如果在定时器到期之前,事件又被再次触发,则定时器会重置。只有当事件停止触发,并且定时器到期后,目标函数才会被执行。
延伸:****节流的核心思想是:“按规定节奏来”。它保证在一个固定的时间间隔内,函数最多只会被执行一次。无论在这段时间内事件触发了多少次,它都会按照既定的节奏去执行。
// 防抖
function debounce(fn, delay) {
let timer = null;
// 返回的函数形成了闭包
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 文字结束输入结束后等待300ms再进行检索
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(keyword) {
console.log('搜索:', keyword);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
五、作用域与面向对象的关联
作用域决定变量的可见性和生命周期,而面向对象通过封装、继承和多态来组织代码。
在面向对象编程中,作用域规则帮助我们实现信息隐藏和数据保护。
实现私有属性和方法
function Person(name) {
// 私有变量
// ES2022(ES13)引入了原生的私有变量支持,通过#前缀定义,仅支持类(class)中的字段和方法。
let _age = 0;
let _secret = name + "'s secret";
// (闭包)公有方法(特权方法) 获取私有变量
this.getName = function() {
return name;
};
this.getAge = function() {
return _age;
};
this.birthday = function() {
_age++;
console.log(`${name} 过生日,现在 ${_age} 岁`);
};
}
const person = new Person("小明");
console.log(person.getName()); // "小明"
person.birthday(); // 小明 过生日,现在 1 岁
console.log(person._age); // undefined (无法直接访问私有变量)
console.log(person._secret); // undefined
模块模式
// 计数期
const MyModule = (function() {
// 私有变量
let privateCounter = 0;
// 私有方法
function privateFunction() {
privateCounter++;
}
// 公有API
return {
increment: function() {
privateFunction();
},
getCount: function() {
return privateCounter;
},
reset: function() {
privateCounter = 0;
console.log('计数器已重置');
}
};
})();
// 使用
MyModule.increment();
MyModule.increment();
console.log(MyModule.getCount()); // 2
MyModule.reset(); // 计数器已重置
六、面试常见问题解析
面试题1:循环中的闭包
题目:以下代码输出什么?如何修改为期望的输出?
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
输出:
// 期望输出:0, 1, 2
// 实际输出:3, 3, 3
原因解析:
var 的作用域为函数作用域,此代码中i 被提升到该代码所在函数作用域或全局作用域中;
随之在事件for循环中,i值更新到3;
最后是三个 setTimeout 回调函数都形成了闭包,调用的是同一个i,即3。
解决方案:
方案1:使用let块级作用域
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
方案2:使用闭包捕获每次循环的i
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2
}, 100);
})(i);
}
方案3:setTimeout的第三个参数
for (var i = 0; i < 3; i++) {
setTimeout(function(j) {
console.log(j); // 0, 1, 2
}, 100, i);
}
面试题2:闭包的内存泄漏
题目:以下代码有什么问题?如何优化?
function createHeavyObject() {
const largeData = new Array(1000000).fill('data');
return function() {
console.log('数据长度:', largeData.length);
};
}
const heavyFunction = createHeavyObject();
存在问题:即使不再需要,largeData仍无法被垃圾回收
原因解析:JavaScript 的垃圾回收器会标记"不再被引用"的变量进行回收;而闭包中所调用变量,仍被间接引用,不会被回收
优化方案:
function createHeavyObject() {
const largeData = new Array(1000000).fill('data');
return {
process: function() {
console.log('处理数据:', largeData.length);
},
// 提供清理方法
cleanup: function() {
largeData.length = 0;
console.log('内存已释放');
}
};
}
const obj = createHeavyObject();
obj.process();
obj.cleanup(); // 手动释放内存
七、总结与最佳实践
核心要点回顾
1)作用域决定变量的可见性
2)作用域链决定变量的查找路径
3)执行上下文是代码执行的环境
4)闭包是函数+其创建时的作用域
最佳实践建议
✅ 推荐使用:
1)使用let / const 代替var
2)合理使用闭包进行数据封装
3)及时释放不再需要的闭包引用
❌ 避免使用:
I. 避免创建不必要的闭包
II. 避免闭包中持有DOM引用导致内存泄漏
III. 避免过深的嵌套作用域
下期预告
作用域和闭包都是JavaScript的同步编程概念。下一篇将深入探讨异步编程,这是JavaScript最核心也是最复杂的部分。
如果觉得有帮助,请关注+点赞+ 收藏,这是对我最大的鼓励!如有问题,可以评论区留言哟

被折叠的 条评论
为什么被折叠?



