[前端]Javascript中闭包详解(很全!!!) 闭包是由函数以及创建该函数时的作用域组合而成的。这意味着,即使函数在其词法作用域之外被调用,它仍然可以访问那个作用域中的变量。

基础

在JavaScript中,闭包(Closure)是一个非常重要的概念,它允许你访问函数内部定义的变量,即使这个函数已经执行完毕。闭包是由函数以及创建该函数时的作用域组合而成的。这意味着,即使函数在其词法作用域之外被调用,它仍然可以访问那个作用域中的变量。

闭包的基本特性

  1. 函数嵌套:闭包通常涉及函数内部的函数。
  2. 作用域链:内部函数可以访问其外部函数(以及更外层)的作用域链中的变量。
  3. 持久化状态:即使外部函数已经返回,内部函数仍然可以访问其变量,因为这些变量被保存在内存中的闭包结构中,简单来说就是内部函数调用了外部函数的变量,变量还被调仍未释放.

代码示例

下面是一个简单的闭包示例:

function createCounter() {
let count = 0; // 这是一个局部变量,通常会在函数执行完毕后被销毁
return function() {
count++; // 但由于闭包,这个函数可以访问并修改 count
return count;
};
}
const counter = createCounter();
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2
console.log(counter()); // 输出: 3

在这个例子中,createCounter 函数返回了一个内部函数。这个内部函数形成了一个闭包,因为它可以访问 createCounter 函数的局部变量 count。即使 createCounter 函数已经执行完毕,count 变量仍然被保存在内存中,并且可以通过调用 counter 函数来访问和修改它。

另一个示例:私有变量

闭包还可以用于模拟私有变量和方法:

javascript复制代码

function Person(name) {
let _name = name; // 私有变量
this.getName = function() {
return _name;
};
this.setName = function(newName) {
_name = newName;
};
}
const person = new Person('Alice');
console.log(person.getName()); // 输出: Alice
person.setName('Bob');
console.log(person.getName()); // 输出: Bob

在这个例子中,_name 是一个私有变量,因为它不能直接从 Person 对象的外部访问。但是,通过闭包,我们创建了可以访问和修改 _name 的公共方法 getName 和 setName

注意事项

  • 存闭包可能会导致内泄漏,因为被闭包引用的变量不会被垃圾回收机制回收,直到闭包本身不再被引用。
  • 过度使用闭包可能会导致代码难以理解和维护,因为它们增加了作用域的复杂性和变量的隐藏性。

闭包是JavaScript中一个强大且有用的特性,但应该谨慎使用,以避免上述潜在问题。

闭包的优点

  1. 封装:闭包允许将变量和方法封装在一起,形成一个私有作用域,从而避免全局命名冲突和数据污染。这是模块化编程的基础。
  2. 保持状态:闭包可以保持其创建时的外部变量的状态,即使外部变量在闭包外部发生了变化,闭包内部仍然可以访问到原始的变量值。
  3. 实现工厂函数:通过闭包,可以创建具有私有变量和方法的函数工厂,根据不同的参数生成不同的函数实例。
  4. 记忆化:闭包可以用于记忆化函数,将函数的计算结果缓存起来,避免重复计算,从而提高性能。
  5. 回调函数和异步操作:在JavaScript中,闭包常用于回调函数和异步操作中,以保持数据的状态和上下文。

闭包的缺点

  1. 内存泄漏:如果闭包引用的外部变量不再需要,但由于闭包的存在而无法被垃圾回收机制回收,就会导致内存泄漏。因此,在使用闭包时,需要确保在不再需要闭包时将其引用置为null,以释放内存。
  2. 性能影响:由于闭包涉及作用域链的查找,相比普通函数,闭包的执行速度可能较慢。在性能敏感的场景中,过度使用闭包可能会影响代码的执行效率。

闭包的应用场景

  1. 模块化编程:通过闭包可以创建模块,将相关的函数和数据封装在一起,避免全局命名冲突,实现模块化开发。
  2. 事件处理程序:在DOM事件处理程序中,闭包常用于保持事件处理函数的上下文和状态。
  3. 回调函数:在异步操作中,闭包常用于回调函数中,以保持异步操作完成后的结果和上下文。
  4. 动态函数创建(柯里化):通过闭包可以动态生成函数,每个函数都有自己的独立作用域和状态。

进阶 

闭包与垃圾回收

在JavaScript中,垃圾回收机制会定期清理不再被引用的内存对象。然而,由于闭包的存在,一些外部变量可能会被闭包引用而无法被垃圾回收。因此,在使用闭包时,需要注意内存管理,确保在不再需要闭包时将其引用置为null,以释放内存。

闭包与this关键字

在JavaScript中,this关键字的值取决于函数的调用方式,而不是函数被定义的位置。因此,在闭包中使用this时,需要特别注意其指向。如果需要在闭包中保持对外部函数this的引用,可以使用箭头函数(ES6引入)或在外部函数中保存this的引用(例如使用var self = this;)。

function Person(name) {
 this.name = name;
 this.sayHello = function() {
 console.log("Hello, my name is " + this.name);
 };
 this.getGreeting = function() {
 // 这是一个闭包,它记住了外部的词法作用域(即Person的实例)
 return function() {
 console.log("Greeting from " + this.name); // 注意这里的this
 };
 };
}
const person = new Person("Alice");
person.sayHello(); // 输出: Hello, my name is Alice
const greeting = person.getGreeting();
greeting(); // 输出: Greeting from undefined(因为这里的this不指向person对象)

在上面的例子中,getGreeting方法返回了一个闭包。然而,当这个闭包被调用时(greeting()),它内部的this并不指向person对象,而是指向了全局对象(在严格模式下是undefined

可利用箭头函数|bind|self=this解决

闭包示例:函数柯里化

函数柯里化是将一个多参数函数转换为一系列接受单个参数的函数的技术。通过闭包,可以实现函数柯里化:

javascript复制代码

function curry(fn) {
// 接收函数fn的第一个参数,并返回一个新的函数
return function curried(...args) {
// 如果传入的参数数量小于fn期望的参数数量,则继续返回一个新的函数
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
// 否则,返回一个新的函数,该函数接收剩余的参数,并调用fn
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
};
}
// 示例函数:接收两个参数并返回它们的和
function add(a, b) {
return a + b;
}
// 对add函数进行柯里化
const curriedAdd = curry(add);
// 使用柯里化后的函数
console.log(curriedAdd(2)(3)); // 输出: 5
console.log(curriedAdd(1, 4)); // 输出: 5

在这个例子中,curry函数接受一个函数fn作为参数,并返回一个新的函数curriedcurried函数根据传入的参数数量,要么直接调用fn,要么返回一个新的函数来接收剩余的参数。通过这种方式,实现了函数的柯里化。

闭包示例:实现工厂函数

 闭包可以用来实现工厂函数(也称为工厂方法或构造函数工厂)。工厂函数是一种创建对象的方法,它使用函数来封装创建对象的细节,并返回新创建的对象。通过闭包,工厂函数可以保持私有变量和方法,从而提供更高级别的封装和模块化。

function createPersonFactory(defaultName) {
 // 私有变量,用于存储默认名称
 let defaultNameStorage = defaultName;
 // 工厂函数,用于创建Person对象
 return function(name = defaultNameStorage) {
 // 私有变量,仅在当前Person对象的作用域内可用
 let _name = name;
 // 私有方法,用于获取名称
 function getName() {
 return _name;
 }
 // 公开方法,用于设置名称
 this.setName = function(newName) {
 _name = newName;
 };
 // 返回包含公开方法的对象,模拟一个类的实例
 return {
 getName: getName,
 // 可以添加其他公开方法...
 };
 };
}
// 创建一个Person工厂,默认名称为"John Doe"
const PersonFactory = createPersonFactory("John Doe");
// 使用工厂函数创建Person对象
const person1 = PersonFactory();
console.log(person1.getName()); // 输出: John Doe
person1.setName("Alice");
console.log(person1.getName()); // 输出: Alice
// 创建另一个Person对象,这次指定名称
const person2 = PersonFactory("Bob");
console.log(person2.getName()); // 输出: Bob

闭包示例:记忆化(缓存结果,用于递归|动态规划)

闭包记忆化是一种优化技术,它利用闭包的特性来缓存函数调用的结果,从而在后续调用中能够直接返回缓存的结果,避免重复计算,提高性能。这种技术特别适用于那些计算成本高昂且结果可能多次被使用的函数。

工作原理

  1. 创建缓存:首先,需要创建一个缓存对象来存储函数调用的结果。这个缓存对象通常是一个简单的键值对集合,其中键是函数调用的参数,值是对应的计算结果。
  2. 检查缓存:在每次函数调用之前,先检查缓存中是否已经存在相同的参数和结果。如果存在,则直接返回缓存的结果,避免重复计算。
  3. 计算并缓存结果:如果缓存中不存在相同的参数和结果,则执行函数计算,并将结果存储到缓存中。这样,在后续调用中就可以直接使用缓存的结果了。
function memoize(fn) {
 // 创建一个缓存对象来存储函数调用的结果
 const cache = {};
 // 返回一个闭包函数,该函数将执行记忆化逻辑
 return function(...args) {
 // 将参数转换为字符串作为缓存的键
 const key = JSON.stringify(args);
 // 检查缓存中是否存在相同的参数和结果
 if (cache[key]) {
 // 如果存在,则直接返回缓存的结果
 return cache[key];
 }
 // 如果不存在,则执行函数计算,并将结果存储到缓存中
 const result = fn(...args);
 cache[key] = result;
 // 返回计算结果
 return result;
 };
}
// 示例函数:计算斐波那契数列
function fibonacci(n) {
 if (n 

应用场景

闭包记忆化技术广泛应用于各种需要优化性能的场景,如:

  • 递归函数:对于递归函数,特别是那些具有重叠子问题的递归函数,记忆化可以显著减少计算量。
  • 昂贵计算:对于计算成本高昂的函数,如复杂的数学运算、数据库查询或网络请求等,记忆化可以缓存结果并避免重复计算。
  • 动态规划:在动态规划问题中,记忆化常用于存储中间结果,以便在后续计算中直接使用。

总之,闭包记忆化是一种强大的优化技术,它利用闭包的特性来缓存函数调用的结果,从而提高性能。在需要优化性能的场景中,可以考虑使用这种技术来减少计算量并提高代码效率。

闭包示例:回调函数和异步操作

回调函数

回调函数是一个作为参数传递给另一个函数的函数。当异步操作完成时,被传递的函数会被调用,以处理操作的结果。闭包在这里的作用是确保回调函数能够访问到定义时的作用域中的变量。

function fetchData(callback) {
 // 假设这是一个异步操作,比如从服务器获取数据
 setTimeout(() => {
 const data = "some data from the server";
 // 调用回调函数,并传递数据
 callback(data);
 }, 1000); // 模拟1秒的异步延迟
}
function processData(data) {
 // 这是一个闭包,它访问了定义时的作用域(这里是全局作用域)
 console.log("Processing data:", data);
}
// 使用回调函数进行异步操作
fetchData(processData);
异步操作与Promise

虽然回调函数是处理异步操作的一种基本方式,但它们可能会导致“回调地狱”(callback hell),即多层嵌套的回调函数,使得代码难以阅读和维护。为了解决这个问题,JavaScript引入了Promise对象。

闭包在Promise中同样扮演着重要角色,因为Promise的thencatch方法通常会接收函数作为参数,这些函数(即回调)也是闭包。

function fetchDataAsync() {
 return new Promise((resolve, reject) => {
 // 假设这是一个异步操作
 setTimeout(() => {
 const success = true; // 模拟操作的成功或失败
 if (success) {
 const data = "some data from the server";
 resolve(data); // 操作成功,使用resolve传递结果
 } else {
 const error = "Failed to fetch data";
 reject(error); // 操作失败,使用reject传递错误
 }
 }, 1000); // 模拟1秒的异步延迟
 });
}
// 使用Promise处理异步操作
fetchDataAsync()
 .then(data => {
 // 这是一个闭包,它访问了定义时的作用域(Promise链的作用域)
 console.log("Data received:", data);
 // 可以返回新的值或Promise,以链式调用下一个then
 return fetchDataAsync(); // 假设我们想要链式调用另一个异步操作
 })
 .then(newData => {
 console.log("Additional data received:", newData);
 })
 .catch(error => {
 // 处理任何在Promise链中抛出的错误
 console.error("Error:", error);
 });
异步/等待(async/await)

最后,JavaScript还引入了asyncawait关键字,它们提供了一种更简洁的方式来处理异步操作,并避免了回调地狱。尽管async/await在语法上不是闭包,但它们底层仍然依赖于Promise,并且闭包的概念在async函数中仍然适用。

async function fetchAndProcessData() {
 try {
 const data = await fetchDataAsync(); // 等待Promise解析
 console.log("Data received:", data);
 // 可以继续处理数据或进行其他异步操作
 const newData = await fetchDataAsync(); // 再次等待Promise解析
 console.log("Additional data received:", newData);
 } catch (error) {
 // 处理错误
 console.error("Error:", error);
 }
}
// 调用异步函数
fetchAndProcessData();

作者:GISer_Jinger原文地址:https://blog.csdn.net/m0_55049655/article/details/143593869

%s 个评论

要回复文章请先登录注册