闭包是前端绕不开的话题。实际上我们可能在不经意间使用了很多次闭包,但是印象里只留下了节流和防抖函数。
深入了解闭包之前我们需要了解一个概念 —— 作用域
作用域简单理解可以说是变量的有效范围。
作用域分为:
-
全局作用域:全局作用域是指在程序中最外层定义的变量或函数所处的作用域,它们在整个代码中都是可访问的,从声明开始直到程序结束都存在。在 JavaScript 中,全局作用域中的变量会挂载在全局对象上(浏览器中是
window,Node.js 中是global)。在非严格模式下,在函数中直接声明的变量也可以在全局中访问(不用var let 声明)// 这是全局变量 var globalVar = "I am global"; function showGlobal() { globalNonVar = "I am global non-var"; // 👈 这是全局变量 console.log(globalVar); // ✅ 可以访问全局变量 } showGlobal(); console.log(globalVar); // ✅ 依然可以访问 console.log(globalNonVar); // ✅ 可以访问全局变量 -
函数作用域:在一个函数内部声明的变量(使用
var、let、const),只能在该函数内部访问,在函数外部是无法访问的。使用var定义的变量没有块级作用域,会被函数作用域提升,即在声明之前可以被访问到,只不过值为undefinedfunction testFunctionScope() { console.log(innerVar) // 可以访问到,只不过值为undefined var innerVar = "I am inside the function"; console.log(innerVar); // ✅ 可以访问函数内部变量 } testFunctionScope(); console.log(innerVar); // ❌ 报错:innerVar is not defined -
块级作用域:块级作用域是编程语言中一个重要的概念,指的是在代码块(通常由花括号
{}包围的区域)内部声明的变量只能在该代码块内部访问,而在代码块外部无法访问这些变量。在 JavaScript 中,let和const关键字声明的变量具有块级作用域,而var声明的变量则是函数作用域。{ let blockLet = "I am in a block"; const blockConst = "I am also in a block"; console.log(blockLet); // ✅ 正常访问 console.log(blockConst); // ✅ 正常访问 } console.log(blockLet); // ❌ 报错:blockLet is not defined console.log(blockConst); // ❌ 报错:blockConst is not defined
作用域链
作用域链 是一种查找变量的机制,当代码在某个作用域中访问一个变量时,JavaScript 引擎会:
- 先从当前作用域查找该变量;
- 如果找不到,就向上层作用域查找;
- 一直查找到全局作用域为止;
- 如果仍找不到,就报错(
ReferenceError)。
这个“层层向上查找”的过程,就是所谓的 作用域链。
需要注意的是⚠️
-
作用域链基于词法结构(定义时)而非调用位置!
-
也就是说,变量查找的链条是根据“函数在哪里定义”决定的,而不是“在哪里调用”。
闭包的本质,就是函数“记住”了它定义时的作用域链,即使函数在定义所在作用域之外执行,也能访问到原来的变量。
function outer() {
let a = 10;
return function inner() {
console.log(a); // 🔥 访问 outer 的变量 a
};
}
const fn = outer(); // outer 执行完毕,但 a 没有被销毁
fn(); // 输出:10
函数在执行时,会创建一个函数作用域,其中的变量只在其作用域内生效,在作用域外是无法访问的。在函数执行结束的时候,里面的局部变量应该就会被垃圾回收机制回收掉。但在闭包中,在函数作用域之外保留了对函数作用域内变量的引用,所以这个变量就不会主动被垃圾回收机制释放。
也因此,闭包有可能会造成内存泄漏,因为无法自动被垃圾回收机制回收,但是通过手动把引用置为null,即解除对变量的引用,就可以被回收了
很多人把 作用域链 和 this 混为一谈,但它们是完全不同的机制! 作用域链基于定义时的作用域,而this的指向是调用时决定的。
因为 this 是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候
this 的绑定规则
this指向主要分为以下几种情况
- 大多数时候,this都指向实际调用它的对象
- 被call/apply/bind显式调用的话会指向绑定的对象
- 如果有 new 关键字,this 指向 new 出来的实例对象,但如果构造器显式地返回一个
对象,那么this是指向返回的这个对象的,注意这里返回一个普通类型的值是不影响this指向的let MyClass=function(){ this.name='xiaohu' } let obj=new MyClass() console.log(obj.name) // xiaohulet MyClass=function(){ this.name='xiaohu' return { name:'xiaolong' } let obj=new MyClass() console.log(obj.name) // `xiaolong` - 普通函数的this指向的是window 在es6中 指向undefined 因为有严格模式
- 箭头函数体内的 this 对象就是定义时所在的对象,而不是使用时所在的对象。
- 箭头函数的外层如果有普通函数,那么箭头函数的 this 就是外层普通函数的this
- 箭头函数的外层如果没有普通函数,那么箭头函数的 this 就是全局变量
闭包
定义:当一个函数在其定义的作用域之外被调用时,它仍然保留对定义时作用域中变量的访问能力,这就形成了一个闭包。
创建闭包的方式
function outer() {
let secret = "secret value";
return function inner() {
console.log(secret);
};
}
const fn = outer(); // outer() 执行完毕,secret 应该被销毁
fn(); // 但 inner() 还能访问 secret,这就是闭包
闭包的使用
// 防抖 在delay时间内触发多次都会重新计时,最终delay时间后执行
const throttle = function (fn, delay) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 节流 在delay时间内只会执行一次
const debounce = function (fn, delay) {
let timer = null
return function (...args) {
if (timer) return
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
闭包的小试牛刀
function createCounter() {
let count = 0
return function () {
return ++count
}
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 输出 0 到 4,每隔 1 秒打印一个数字,但不能用 let
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j);
}, j * 1000)
})(i);
}
// 在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
// 实现一个 createBankAccount(initialBalance) 函数,返回对象提供以下方法:
// deposit(amount):存钱
// withdraw(amount):取钱
// getBalance():查看余额
function createBankAccount(initialBalance) {
let balance = initialBalance
this.deposit = function (amount) {
balance += amount
}
this.withdraw = function (amount) {
if (amount <= balance) {
balance -= amount
} else {
console.log("余额不足")
}
}
this.getBalance = function () {
return balance
}
return this
}
const account = createBankAccount(100);
account.deposit(50); // +50
account.withdraw(30); // -30
console.log(account.getBalance()); // 输出 120