hello!大家好,我是一名正在乱学前端技术的大学生,今天聊聊JavaScript中的闭包,如有讲错的地方麻烦提出指正。
什么是闭包?
在JavaScript中,闭包是一个非常重要的概念,它涉及到函数作用域和变量访问。具体来说,闭包是指一个函数能够访问并操作其外部作用域的变量的能力。当两个函数彼此嵌套时,内部的函数就可以被认为是闭包,因为它可以访问外部函数的变量和参数,即使外部函数已经执行完毕。
闭包的形成通常涉及以下要素:
- 函数嵌套:一个函数内部定义了另一个函数,这是形成闭包的基础。
- 作用域链:内部函数可以通过作用域链访问外部函数的变量和参数。
- 外部引用:外部函数的变量或参数被内部函数引用,并且内部函数被外部环境的某个变量所引用,从而形成一个闭环。
闭包的主要特点
- 封装性:闭包可以封装私有变量和方法,保护内部数据不被外部访问。这是通过内部函数访问外部函数的局部变量来实现的,而这些局部变量在外部函数中是不可见的。
- 持久性:闭包可以保存函数创建时的外部变量状态,即使外部变量发生变化,闭包仍然可以访问原始状态。这是因为闭包会将外部函数的上下文环境保存下来,形成一个封闭的作用域。
- 函数工厂:闭包可以作为函数工厂,创建具有特定功能的函数。这些函数可以共享闭包中的变量和方法,从而实现某种特定的功能或行为。
闭包在JavaScript中有广泛的应用,例如:
- 数据隐藏:通过闭包,可以创建私有变量和方法,防止外部直接访问和修改,从而提高代码的安全性和可维护性。
- 回调函数:在异步编程中,回调函数经常需要使用到闭包来访问外部变量和上下文信息。
- 模块模式:闭包是实现JavaScript模块模式的关键技术之一。通过闭包,可以创建具有私有和公共接口的对象,从而实现代码的模块化和封装。
闭包实现数据私有
function createCounter() {
// 私有变量
let count = 0;
// 返回一个对象,该对象包含操作私有变量的公共方法
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
// 使用createCounter函数创建一个计数器对象
const counter = createCounter();
console.log(counter.getCount()); // 输出: 0
console.log(counter.increment()); // 输出: 1
console.log(counter.increment()); // 输出: 2
console.log(counter.decrement()); // 输出: 1
// 尝试直接访问私有变量会导致错误
// console.log(counter.count); // 输出: undefined(不会报错,但也不会得到期望的值)
// 如果尝试通过其他方式访问(比如闭包之外的代码),则会因为作用域限制而无法访问到。
// 尝试直接访问私有变量会导致错误
// console.log(counter.count); // 输出: undefined(不会报错,但也不会得到期望的值)
// 如果尝试通过其他方式访问(比如闭包之外的代码),则会因为作用域限制而无法访问到。
原生函数柯里化实现
//函数柯里化 使接受多个参数的函数转换成接受一个参数函数
//意味着我们可以先传递一部分参数,然后传递剩余的参数,或者分别传递参数,以此灵活地处理函数的调用
//通过柯里化后变为const add = (a)=>(b)=>a+b
//即add(3,5) == add(3)(5)
//具体实现
function curry (fn, args) {
let length = fn.length
args = args || []
return function () {
let subArgs = args.slice(0)
//拼接所有参数
for (let i = 0; i < arguments.length; i++) {
subArgs.push(arguments[i])
}
//判断参数长度是否已经满足函数所需参数的长度
if (subArgs.length >= length) {
//如果 subArgs 的长度足够,调用 fn 并将 subArgs 中的所有参数传入,使用 apply 保持上下文 this
return fn.apply(this, subArgs)
} else {
//如果参数不够,返回一个新的函数,等待下一部分参数
return curry.call(this, fn, subArgs)
}
}
}
function add (x, y, z) {
return x + y + z
}
const curryAdd = curry(add)
let f1 = curryAdd(3)
console.log(f1) //返回一个新的函数等待新的参数进行拼接直到参数长度满足函数所需参数的长度
const res = f1(2)(3)
console.log(res) //8
const res2 = curryAdd(1)(2)(3) //6 函数柯里化使得函数参数允许一部分一部分传入
console.log(res2)
const res3 = curryAdd(1, 2, 3) //6
console.log(res3)
柯里化实现闭包的详细解释
-
定义多参数函数:
首先,你有一个接受多个参数的函数。这个函数可能是执行某种计算或操作的函数,它依赖于所有这些参数来产生结果。 -
柯里化转换:
接下来,你将这个多参数函数转换为柯里化形式。这意味着你将原始函数分解为一个接受单个参数的函数,该函数返回一个接受下一个参数的函数,依此类推,直到所有参数都被接受并处理。 -
闭包的形成:
在柯里化的过程中,每个内部函数都会访问其外部函数的作用域。这是因为内部函数在定义时捕获了其外部函数的执行上下文,包括参数和局部变量。这种捕获行为正是闭包的核心。 -
参数累积与结果计算:
随着每个内部函数的调用,它们会累积参数,并在累积了所有必要的参数后计算结果。这个累积过程是通过闭包来实现的,因为每个内部函数都可以访问并修改其外部函数作用域中的变量(即累积的参数)。 -
最终结果的返回:
当所有参数都被传递并累积完毕后,最后一个内部函数将计算最终结果并返回。
闭包的缺点
然而,闭包也存在一些潜在的问题,例如内存泄漏和性能问题。如果闭包中引用的外部变量没有被及时释放,就可能导致内存泄漏。此外,过度使用闭包也可能导致代码变得复杂和难以维护。因此,在使用闭包时需要注意其潜在的风险和副作用。
一般情况下一个函数进行完,其会立即被垃圾回收机制回收,而闭包会保留对其外部作用域的引用,像上面数据私有的例子一样,即使外部函数已经返回,其内部变量也不会立即被垃圾回收器回收。如果这些变量占用了大量内存,并且闭包长时间存在,那么就会导致内存占用过多。
而像函数柯里化,如果需提交很多次参数,将会形成一条很长的作用域链,虽然现代JavaScript引擎对作用域链查找进行了优化,但在某些情况下,这种查找仍然可能成为性能瓶颈。
总的来说,闭包是JavaScript中一个强大而灵活的概念,它允许函数访问和操作其外部作用域的变量。通过合理使用闭包,可以实现数据的封装、状态的持久化以及函数工厂等功能。然而,也需要注意其潜在的风险和副作用,以确保代码的安全性和可维护性。