闭包与立即调用函数表达式IIFE

半年前(刚工作)就说要写技术博客,现在才开始真正做起来。第一篇博客,没经验,随便写写吧。就是自己对这些知识的梳理和总结吧,自己写过之后印象会更加深刻。

P.S. 由于这是基于本人理解之后的梳理,可能会有理解错的地方。欢迎指正。

JavaScript中闭包和立即执行函数是老生常谈的问题。本人对于很多知识只是过一篇,从未好好总结过。本文是在本人阅读了众多的技术文档后的梳理总结。为什么把闭包和立即调用函数表达式放在一块?因为我总有某个瞬间,会搞不清闭包和立即执行函数的关系,感觉它们好像是紧密联系的。所以在本文中一起说清楚。

总体来看,本文主要按照以下思路来展开的。

  • 什么是闭包,为什么要有闭包(优点),闭包的副作用,经典应用场景
  • 什么是IIFE,为什么要有IIFE(优点) ,经典应用场景
  • 两者之间什么关系

    什么是闭包

说起闭包,可能大家的第一印象是函数中包着函数。知道个大概,却不能用严谨的术语描述出来。我觉得比较专业且好理解的定义是,一个能在自己被创建时的作用域之外使用的函数。分解一下这个定义,需要有的条件有:1. 该函数被嵌套在另外一个函数中; 2. 该函数会被返回到它的父函数所在的作用域下。

上代码

function dad(){
     var sonNum = 2;
     function getSonNum(){
          console.log(sonNum++);
     }

     return getSonNum; //将内部函数返回
}

var dad1 = dad();
dad1(); // 2
dad1(); // 3

为什么要有闭包

函数工厂

思考一个小问题,如何去写一个sum(2)(4) = 6的函数。分析一下,(sum(2))(4)显然第一个括号执行之后仍然应该是个函数,然后再把实参4传进去。

上代码

function sum(x){
     return function(y){
         return x+y; 
     }
}

sum(2)(4); //6
//再延伸一下,如果第一个数我们需要确定呢?
var add2 = sum(2);
var add4 = sum(4); 

add2(5); //7
add4(6); //10

此时,我们可以看到sum相当于一个函数工厂,你可以用这个工厂创建出你需要的各种函数。

构建私有变量

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

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

function Person() {
     this.name = 'Yecao';
     this.getName = function(){
       return this.name; 
   }
}

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

me.name = 'Xiaoxiaofen';
me.getName(); // 'Xiaoxiaofen'

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

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

function Person() {
   var name = 'Yecao';
   this.getName = function(){
       return name; 
   }
}

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

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

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

变量不被回收

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

function Counter(){
   var count = 0;
   return function(){
       return count++;
   }
}

var myCounter = Counter(); 
myCounter(); //0
myCounter();   //1  

这样我们就能实现每次执行myCounter()的时候count就会在原来的基础上增加1。

闭包的副作用

正因为闭包的形式会让某些变量永驻内存,这是把双刃剑,用得不好就会造成性能和内存问题。举个栗子。

function Person(name ) { 
      this.getName = function(){
  return name; 
   }

   this.sayHello = function(){
        alert("say Hello");
   }
}

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

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

function Person(name ) { 
    this.getName = function(){
        return name; 
    } 
}

Person.prototype.sayHello = function(){
    alert("say Hello");
}

闭包经典实用场景

在为什么有闭包这一小节中,其实也涉及到了一些闭包的经典应用场景。但还有个相当经典的场景,它在面试中经常会被提及,那就是for循环中的函数。

var arr = [];
for(var i = 0; i< 5; i++){
     arr[i] = function(){
          return i;
     }
}

arr[0](); // 5 but expected 0

在这个问题中,i 这个变量是被共享的。当循环结束之后,i 已经变成5了。所以arr0输出的是5。要解决问题,需要在定义arr[i]的匿名函数时,就需要记住 i 的值,而不能让它一直是变动的。用闭包的思路是每个闭包都创建了一个新的作用域,每个作用域的i是独立不影响的,也就避免了共用 i,而 i 到最后都是5的问题。代码如下。

var arr = [];
for(var i = 0; i< 5; i++){
     arr[i] = (function(i){
          return function(){
               return i;
          }
     })(i)
}

arr[0](); // 0

什么是IIFE

立即调用函数表达式,Immediately-Invoked Function Expression,简写为IIFE。顾名思义,这个函数定义之后马上被执行。比如一般情况下

function commonFun(){ //函数定义
     var a = 1;
     return a;
}

commonFun(); //函数执行

而立即执行函数就是将上述两步操作函数的定义和函数的执行合为一步,以上代码等价于

(function (){
     var a = 1;
     return a; 
})()

需要注意的是,立即调用函数表达式的形式不仅有以上这种,只是以上这种更为常见而已。一般情况下function为关键词开头的都会被默认为函数申明,除非存在一些表达式的标记, 比如

//用括弧转化为表达式
(function(){   //some code }())

//前者为表达式,后者也会是表达式
var a = function(){}();
true && function(){}()
0, function(){}()

//一元表达式
 !function(){}()
~function(){}()
+function(){}()
-function(){}()

为什么有IIFE

IIFE最大的好处在创建出一个新的作用域来,因为JavaScript中只有全局作用域和函数作用域。为了避免变量的污染,尽可能地少设置全局变量。所以我们就用立即调用函数表达式。在模块化设计中,它是最经典的存在。比如设计一个jquery的插件

(function($, window){
     // your code here
})(jQuery, window, undefined)

IIFE经典场景

还是上面那个经典的for循环问题,其实也能用立即执行函数表达式来解决,原理是相同的。

var arr = [];
for(var i = 0; i< 5; i++){
     (function(i){
          arr[i] = function(){
               return i;
          } 
     })(i)
}

arr[0](); // 0

闭包与立即调用函数表达式的关系

理解清楚了二者之间的概念,就肯定不会混淆。那它们之间到底是什么关系呢?它们是合作的关系,经常你会看到立即调用函数中会存在函数,那这个函数就是闭包了。上述的两个经典场景中,其实两个方法都涉及到,只是以哪个为主要解决方法而已。

参考文献: