讲一讲闭包

本文解释了JavaScript闭包的概念,包括自由变量、理论上所有函数都是闭包,以及实践中的闭包定义。通过实例讲解了执行上下文、变量对象和作用域链在闭包中的关键作用。最后,提供了一个简单的闭包示例来说明闭包如何在内存中持久化变量。

闭包的定义

MDN 对闭包的定义为:闭包就是那些能够访问自由变量的函数。

什么是自由变量呢?

自由变量是指在函数中使用,但既不是函数参数也不是函数局部变量的变量。

由此可见闭包包含两部分:函数+函数能够访问的自由变量,从技术的角度讲,所有的JavaScript函数都是闭包

var a = 1;

function foo() {
    console.log(a);
}

foo();

但这只是理论上的闭包,还有实践角度上的闭包:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域;
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回);
    2. 在代码中引用了自由变量;

这就涉及到两个知识点:执行上下文和变量对象

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

举一个简单的例子:

var scope = "global scope"
function checkscope(){
	var scope = "local scope"
	function f(){
		console.log(scope)
	}
	return f
}
checkscope()()

执行过程:

  1. 创建全局执行上下文,全局执行上下文压入执行上下文栈,全局执行上下文初始化,初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]];
ECStack = [
	globalContext
]
globalContext = {
	VO: [global],
	Scope: [globalContext.VO],
	this: globalContext.VO
}
checkscopeContext: {
	scope: [globalContext.VO]
}
  1. 执行checkscope函数,创建checkscope 执行上下文,被压入执行上下文栈;
  2. checkscope函数执行上下文初始化,创建变量对象、作用域链、this等,同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]];
ECStack = [
	checkscopeContext,
	globalContext
]
checkscopeContext: {
	AO: {
		arguments: {
			length: 0
		},
		scope: undefined,
		f: reference to function f(){}
	},
	Scope: [AO, globalContext.VO],
	this: undefined
}
  1. checkscope函数执行完毕,从执行上下文栈弹出;
ECStack = [
	globalContext
]
  1. 执行 f 函数,创建 f 执行上下文,被压入执行上下文栈;
ECStack = [
	fContext,
	globalContext
]
fContext = {
	AO: {
		arguments: {
			length: 0
		},
	},
	Scope: [AO, checkscopeContext.AO, globalContext.VO],
	this: undefined
}
  1. f 函数执行完毕,从执行上下文栈弹出;
  2. 全局执行上下文从执行上下文栈弹出。

f 函数在执行的时候, f 执行上下文维护了一个作用域链,

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

写一个简单的闭包

var arr = []
for(var i = 0; i < 3; i++){
	arr[i] = function(i){
		return function(){
			console.log(i)
		}
	}(i)
}
arr[0]()
arr[1]()
arr[2]()
### JavaScript 中的闭包概念 闭包(Closure)是 JavaScript 中的种特性,它允许个函数访问并记住其词法作用域,即使该函数在其作用域外执行。换句话说,闭包种函数与其词法环境的绑定,使得函数能够访问并操作定义在其外部作用域中的变量。这种特性使得闭包在实现数据封装、模块化代码、回调函数和事件处理等方面具有重要作用[^3]。 ### 闭包的工作原理 闭包的工作原理基于 JavaScript 的作用域链机制。当个函数在 JavaScript 中被创建时,它会自动获取个作用域链,其中包含其自身的作用域、外部函数的作用域以及全局作用域。当函数执行时,JavaScript 引擎会沿着作用域链查找变量。如果个内部函数引用了外部函数的变量,并且该内部函数在其外部函数执行结束后仍然存在(例如被返回或赋值给外部变量),那么该外部函数的变量不会被垃圾回收机制回收,从而形成闭包。 以下是个简单的闭包示例: ```javascript function outerFunction() { let count = 0; return function innerFunction() { count++; console.log(count); }; } const counter = outerFunction(); counter(); // 输出 1 counter(); // 输出 2 ``` 在这个例子中,`innerFunction` 是闭包,它能够访问并修改 `outerFunction` 中的 `count` 变量。即使 `outerFunction` 已经执行完毕,`count` 变量依然保留在内存中,因为 `innerFunction` 仍然引用了它[^1]。 ### 闭包的使用场景 闭包JavaScript 中有广泛的应用场景,主要包括以下几个方面: #### 1. 数据封装与私有变量 闭包可以用于创建私有变量,防止全局变量的污染。例如,以下代码通过闭包实现了个计数器: ```javascript function createCounter() { let count = 0; return { increment() { count++; }, decrement() { count--; }, getCount() { return count; } }; } const counter = createCounter(); counter.increment(); counter.increment(); console.log(counter.getCount()); // 输出 2 ``` 在这个例子中,`count` 变量只能通过返回的对象方法进行访问和修改,从而实现了数据的封装[^3]。 #### 2. 回调函数与事件处理 闭包在异步编程中非常有用,尤其是在处理回调函数和事件时。例如,以下代码使用闭包来保存按钮的状态: ```javascript function setupButtonHandler(button) { let clickCount = 0; button.addEventListener('click', function() { clickCount++; console.log(`按钮被点击了 ${clickCount} 次`); }); } const button = document.getElementById('myButton'); setupButtonHandler(button); ``` 在这个例子中,`click` 事件的处理函数是闭包,它能够访问 `setupButtonHandler` 函数中的 `clickCount` 变量。每次按钮被点击时,`clickCount` 的值都会递增,并且不会被外部代码直接修改[^4]。 #### 3. 模块化代码 闭包可以用于实现模块模式,将相关的功能封装在个模块中,避免全局变量的污染。例如: ```javascript const myModule = (function() { let privateVariable = '私有变量'; function privateMethod() { console.log('这是个私有方法'); } return { publicMethod: function() { console.log('访问私有变量: ' + privateVariable); privateMethod(); } }; })(); myModule.publicMethod(); // 输出 "访问私有变量: 私有变量" 和 "这是个私有方法" ``` 在这个例子中,`myModule` 是个模块,它通过闭包实现了私有变量和私有方法的封装,外部只能通过返回的 `publicMethod` 来访问模块的功能。 #### 4. 链式调用 闭包还可以用于实现链式调用,例如在对象的方法中返回 `this`,以便连续调用多个方法: ```javascript function chain() { let value = 0; return { add(n) { value += n; return this; }, mul(n) { value *= n; return this; }, get() { return value; } }; } console.log(chain().add(3).mul(2).get()); // 输出 6 ``` 在这个例子中,`add` 和 `mul` 方法都返回 `this`,使得可以连续调用这些方法,而 `value` 变量则通过闭包被保留在内存中[^5]。 ### 闭包的优点与缺点 闭包的优点包括: - **重复使用变量**:闭包可以重复使用变量,而不会造成变量污染,因为变量不会被外部代码随意修改。 - **数据封装**:闭包可以实现私有变量和私有方法的封装,提高代码的安全性和可维护性。 然而,闭包也有些缺点: - **内存消耗**:闭包会使函数中的变量被保留在内存中,导致内存消耗增加。如果滥用闭包,可能会引发性能问题,甚至内存泄漏。 - **调试困难**:由于闭包的存在,变量不会被垃圾回收机制回收,这可能导致调试时难以追踪变量的状态。 为了避免内存泄漏,可以在退出函数之前将不再使用的变量手动删除,例如将变量设置为 `null` 或 `undefined`[^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值