一文讲透JavaScript闭包与立即执行函数表达式(IIFE)

2023-11-22 11:49:35 浏览数 (3)

引言

闭包是一种函数的特性,用于捕获和保存其所在作用域的变量,而IIFE是一种用来创建函数作用域的模式。在JavaScript中,我们可以将闭包和IIFE结合使用,但它们并不是彼此依赖的概念。

虽然我们可以在IIFE中使用闭包,但是闭包并不依赖于IIFE的存在。闭包可以与任何函数一起使用,不管是普通函数还是IIFE

关于闭包和IIFE,本文将分别讨论它们在JavaScript开发中的应用场景和好处。这样可以更清楚地理解它们的作用和关系,并有效地运用它们来提升代码质量和可维护性。

一、深入闭包的理解

1.1、闭包的概念

闭包(closure)是指一个函数可以访问并操作其自身作用域以外的变量。换句话说,闭包是一种能够访问其父函数作用域中的变量的函数。

JavaScript中,当一个函数内部定义了另一个函数,并且内部的函数引用了外部函数的变量时,就创建了一个闭包。内部函数可以访问外部函数的变量,即使外部函数已经执行完毕,这些变量仍然可以在内部函数中使用。

闭包的一个常见用途是创建私有变量。通过使用闭包,可以在函数内部定义一个变量,使其在外部无法访问。这样可以提供更好的封装和数据隐藏。

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

代码语言:javascript复制
function outerFunction() {
  var outerVariable = 'Hello';

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

var closure = outerFunction();
closure(); // 输出:Hello

在上面的例子中,outerFunction内部定义了outerVariable变量和innerFunction函数。innerFunction函数引用了outerVariable变量,并且作为一个闭包被返回出来。当我们调closure时,它保留了对outerVariable的引用,因此可以在执行时访问并打印出Hello

1.2、闭包的特性

JavaScript之所以有闭包,是因为它采用了词法作用域的函数定义方式。

闭包的存在有以下几个重要原因:

  1. 保护变量:闭包可以创建私有变量,通过将变量封装在函数内部,外部无法直接访问,从而实现信息隐藏和保护变量的安全性
  2. 实现数据封装:闭包提供了一种封装数据的方式,在函数内部定义的变量只能在函数内部访问,外部无法修改或者获取,从而实现了数据私有化。
  3. 延长变量的生命周期:当函数执行完毕后,其作用域中的变量通常会被销毁,但是闭包可以延长变量的生命周期。内部函数仍然可以引用外部函数中的量,因此这些变量不会被垃圾回收机制销毁,可以在内部函数中继续使用。
  4. 创建回调和异步操作:闭包可以用于创建回调函数,通过将函数作为参数传递给其他函数,实现函数的延迟执行。

总的来说,闭包在JavaScript中具有重要的作用,可以提供更强大的编程能力,实现数据封装、变量保护、延长变量生命周期等功能。

构建函数工厂

比如有这么一个场景,如何去写一个sum(1)(2) = 3的函数?

分析一下,(sum(1))(2)显然第一个括号执行之后仍然应该是个函数,然后再把第二个参数2传进去。

代码语言:javascript复制
function sum(x){
      return function(y){
          return x y; 
      }
 }

 sum(1)(2); // 输出结果为 3
 
 // 问题,如果第一个数我们需要确定呢?
 var add1 = sum(1);
 var add2 = sum(2); 

 add1(5); // 输出结果为 6
 add2(6); // 输出结果为 8

我们可以将sum看作是一个函数工厂,你可以用这个工厂创建出你需要的各种函数。

构建私有变量

由于ES6之前的JavaScript是没有类的概念,我们用函数来模拟类。会一点OOP的应该都知道,有些类中的变量我们需要保护不被外界访问到,就有了私有变量的概念。

有种简单的创建类的方式如下

代码语言:txt复制
function Person() {
      this.name = 'anyup1';
      this.getName = function(){
      return this.name; 
    }
 }

 var person = new Person();
 person.getName(); // 'anyup1'

 person.name = 'anyup2';
 person.getName(); // 'anyup2'

首先定义了一个Person的类构造器,实例化出一个person对象。但是可以直接被修改内部的变量name,使得人的名字被修改了。我们当然不希望我们的名字被修改。

此处我们换种方式,将name设置为私有变量

代码语言:txt复制
 function Person() {
    var name = 'anyup';
    this.getName = function(){
       return name; 
    }
 }

 var me = new Person();
 person.getName(); // 'anyup'

 person.name = 'anyup2'; // 你仍然可以设置person.name属性,但是这个对象内部的name值是保持不变的。
 person.getName(); // 'anyup'

分析一下,为什么说上述的是闭包呢?首先getName函数是包含在Person函数里面,但是看起来好像没有返回。我们来看下me = new Person()做了什么,它其实是创建了一个对象,并且返回。也就是说getName是在此时返回的。然后me.getName()就能使用了。

变量不被回收

由于JavaScript的垃圾回收机制,普通函数执行完之后,变量就会被直接回收。但是,闭包的方式可以让变量一直存在,不被回收。我们来看一个简单的计数器例子。

代码语言:javascript复制
 function Counter(){
      var count = 0;
      return function(){
           return count  ;
      }
 }

 var counter = Counter(); 
 counter(); // 输出 0
 counter(); // 输出 1  

正是由于这种变量不被回收的机制,这样我们就能实现每次执行counter()的时候count就会在原来的基础上增加1。

1.3、闭包的副作用

由于JavaScript闭包是指函数能够访问其外部函数范围内定义的变量,即使外部函数已经执行完毕。尽管闭包在某些情况下非常有用,但它也可能带来一些副作用。

以下是一些JavaScript闭包可能引发的副作用:

  1. 内存泄漏:由于闭包保持对外部变量的引用,这些变量可能会一直存在于内存中,即使它们已经不再需要。如果闭包过多或闭包引用的数据过大,可能会导致内泄漏,影响程序性能。
  2. 变量生命周期延长:使用闭包可以使变量的生命周期超过它们通常在函数执行结束后被销毁的范围。这可能导致变量长时间占用内存空间,增加内存使用量。
  3. 性能损失:闭包需要维护对外部变量的引用,当闭包被频繁调用时,会增加额外的性能开销。这是因为每个闭包都需要在内存中保存对外部变量的引用,而且包访问外部变量的速度相对较慢。

出于以上原因,在编写代码时,应该谨慎使用闭包。确保确实需要使用闭包,并注意处理闭包带来的副作用。对于不再使用的闭包,及时释放相关资源,以避免内存泄漏。同时,尽量简化闭包的使用场景,以提高代码可读性和维护性。

以下是一个简单的示例,说明闭包内存泄漏的风险:

代码语言:javascript复制
function Person(name ) { 
    this.getName = function(){
       return name; 
    }

    this.sayHi = function(){
         alert("say Hi");
        }
    }

 var me  = new Person('me');
 var you = new Person('you');

上面的示例代码每创建一个对象,都有创建出一个相同的sayHello方法。这个方法并没有用到私有变量name,其实就根本不需要在Person内部去定义这样的一个闭包。更好的方式是将这个方法添加在Person的原型链上,如下图所示:

优化后的代码如下所示:

代码语言:txt复制
 function Person(name ) { 
    this.getName = function(){
       return name; 
    } 
 }

  Person.prototype.sayHi = function(){
      alert("say Hi");
 }

1.4、闭包的经典场景

经典的JavaScript闭包应用场景中,使用闭包在for循环中是一个常见的例子。在循环中使用闭包可以避免变量共享和作用域问题,确保在异步操作中使用正确的值。

考虑以下示例,我们使用for循环创建了多个定时器,每隔一秒输出对应的数字:

代码语言:javascript复制
for (var i = 1; i <= 5; i  ) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

如果直接运行这段代码,你可能会期望在1秒后依次输出1、2、3、4、5,但实际上输出的结果却是6个6。

这是因为setTimeout的回调函数是在循环结束后才执行的,此时i已经变成了6,所以无论定时器运行多长时间,都会输出6。

要解决这个问题,可以利用闭包来创建一个新的作用域,捕获每次循环的变量值。我们可以通过立即执行函数表达式(IIFE)来创建闭包:

代码语言:javascript复制
for (var i = 1; i <= 5; i  ) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, j * 1000);
  })(i);
}

在这个例子中,我们使用立即执行函数表达式将i的值传递给匿名函数的参数j。每次循环时,都会创建一个新的作用域,保留了当前循环的变量值。

这样,每个setTimeout回调函数都捕获了对应的j值,从而实现了按照预期顺序输出1、2、3、4、5。

通过使用闭包,我们解决了在for循环中使用异步操作所遇到的问题,确保了每次循环中的正确值被定时器回调函数所使用。这是一个非常常用的闭包应用场景。

二、深入IIFE的理解

2.1、IIFE的概念

IIFE是立即执行函数表达式(Immediately Invoked Function Expression)的缩写。它是一种特殊的函数调用方式,也是一种用来创建函数作用域的模式。

JavaScript中,IIFE通过将函数用括号包裹,并在后面立即调用它来创建一个函数作用域。这样做的好处是可以在函数内部定义变量和函数,而不会对外部的全局作用域造成污染。

IIFE的基本语法如下:

代码语言:javascript复制
(function() {
  // 在这里编写你的代码
})();

在上面的语法中,我们使用了一个匿名函数,并将其用括号包裹起来。紧接着,在括号的最后加上一对空括号,表示立即调这个函数。

2.2、IIFE的特性

IIFE的作用包括:

  1. 避免全局命名冲突:在IIFE内部定义的变量和函数都是在函数作用域内,不会与全局作用域中的变量冲突。
  2. 创建闭包IIFE能够捕获并保存外部作用域的变量,从而创建闭包,实现更复杂的编程技巧。
  3. 封装代码:一些库和框架通过使用IIFE来封装其代码,以隐藏内部的实现细节,提供干净的接口。

在模块化设计中,它是最经典的存在。如下所示:一个经典的jQuery插件

代码语言:javascript复制
(function($, window){
     // 具体代码写在这里
 })(jQuery, window, undefined)

2.3、IIFE的经典场景

IIFEfor循环中的应用是其中一个经典的场景。在传统的for循环中,由于JavaScript中只有函数作用域和全局作用域,没有块级作用域,所以在循环体内部定义的变量会被循环体外部的代码共享,可能导致意想不到的结果。

为了解决这个问题,我们可以使用IIFE来创建一个立即执行的函数作用域,并在其中定义循环体内部的变量,从而避免变量共享和污染全局作用域。以下是一个简单的示例:

代码语言:javascript复制
for (var i = 0; i < 5; i  ) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 1000);
  })(i);
}

在上面的代码中,我们使用一个IIFE来创建一个函数作用域,并将循环变量i作为参数传入其中。在IIFE的内部,我们使用j来接收传入的参数i,这样就创建了一个函数作用域内部的变量j,它与外部的循环变量i是相互独立的。

IIFE内部,我们通过setTimeout函数来模拟一个异步操作,将每个循环迭代的j的值输出到控制台。由于每个循环迭代都有一个独立的函数作用域和变量j,所以它们的值都可以被正确地输出。

这种使用IIFE的方式,在循环体内使用一个立即执行的函数作用域,可以有效避免循环变量共享和闭包问题。这在处理异步操作、事件处理等场景中非常有用。

需要注意的是,ES6引入了letconst关键字,它们具有块级作用域,可以直接在循环中定义新的变量,避免了使用IIFE的需求。所以,在使用较新版本的JavaScript时,可以优先考虑使用letconst来替代IIFE解决循环作用域的问题。

总结起来,IIFE在循环中的常见应用是创建函数作用域,避免循环变量的共享和污染全局作用域。它能够有效地解决传统for循环中的闭包问题,特别是在处理异步操作时非常实用。

结语

在本文中,我们详细解释了JavaScript闭包和立即执行函数表达式(IIFE)的概念、特点和用法。闭包是JavaScript中一个强大的特性,它可以让函数保留对其作用域外部变量的引,并且在函数执行完毕后仍然可以访问这些变量。使得我们可以创建私有变量、实现模块化和封装等功能。

然而,闭包也可能引发一些副作用,如内存泄漏和性能损失。因此,在使用闭包时,我们需要谨慎考虑其影响,并及时释放不再使用的闭包。

相对而言,IIFE是一种特殊的函数表达式,它可以立即执行并创建一个独立的作用。通过将代码封装在IIFE内部,我们防止污染全局命名空间,并且可以将变量和函数限定在私有作用域中。

在编写JavaScript代码时,了解闭包和IIFE的概念和用法,能够帮助我们更好地设计和组织代码结构,提高代码可维护性和可读性。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞