zoukankan      html  css  js  c++  java
  • 理解JavaScript的立即调用函数表达式(IIFE)

    首先这是js的一种函数调用写法,叫立即执行函数表达式(IIFE,即immediately-invoked function expression)。顾名思义IIFE可以让你的函数立即得到执行(废话)。

    一般来说,IIFE有以下几种用途:

      1. 创建只使用一次的函数,并立即执行它。
      2. 创建闭包,保存状态,隔离作用域。
      3. 作为独立模块存在(例子如jQuery),防止命名冲突,命名空间注入(模块解耦)。


    1. 创建只使用一次的函数,并立即执行它

    创建只使用一次的函数比较好理解,在需要调用函数的地方使用IIFE,类似内联的效果:

    1 (function(){
    2     var a = 1, b = 2;
    3     console.log(a+b); // 3
    4 })();

    还可以传入参数:

    1 (function(c){
    2     var a = 1, b = 2;
    3     console.log(a+b+c); // 6
    4 })(3);

    IIFE比较常见的形式是匿名函数,但是也可以是命名的函数:

    1 (function adder(a, b){
    2     console.log(a+b); // 7
    3 })(3, 4);

    在js中应该尽量使用命名函数,因为匿名函数在堆栈跟踪的时候会造成一些不便。

    2. 创建闭包,保存状态,隔离作用域

    隔离作用域比较复杂一点,在ES6以前,JS没有块级作用域,只有函数作用域,作为一种对块级作用域的模拟就只能用function模拟一个作用域,比如如下代码:

     1 var myBomb = (function(){
     2     var bomb = "Atomic Bomb"
     3     return {
     4         get: function(){
     5             return bomb
     6         },
     7         set: function(val){
     8             bomb = val
     9         },
    10     }
    11 })()
    12 
    13 console.log(myBomb.get()) // Atomic Bomb
    14 myBomb.set("h-bomb")
    15 console.log(myBomb.get()) // h-bomb
    16 
    17 console.log(bomb) // ReferenceError: bomb is not defined
    18 bomb = "none"
    19 console.log(bomb) // none   

    可以看到一个比较奇特的现象,按照常理,一个函数执行完毕,在它内部声明的变量都会被销毁,但是这里变量bomb却可以通过myBomb.get和myBomb.set去读写,但是从外部直接去读和写却不行,这是闭包造成的典型效果。

    要清楚解释闭包到底是什么,这里有一篇文章学习Javascript闭包(Closure),上面的代码已经用到了闭包。所有闭包都有一个特点,就是可以通过导出方法从函数外部改变函数内部变量的值,因此可以利用这个特点来隔离作用域,模拟一种“私有”的效果。

    举一个IIFE保存变量的例子,我们要写入三个文件,先定义了一个内容数组,然后用for循环遍历这个数组写入文件,最后依次用for循环的下标打印出"File i is written.":

     1 var fs = require('fs');
     2 
     3 var fileContents = ["text1", "text2", "text3"];
     4 for (var i = 0; i < fileContents.length; i++) {
     5     fs.writeFile("file"+i+".txt", fileContents[i], function(err){
     6         if (err) {
     7             console.log(err)
     8         }
     9         console.log("File " + i + " is written.")
    10     })
    11 }    

    这段代码结果是:

    File 3 is written.
    File 3 is written.
    File 3 is written.

    很明显和我们的意愿相违背,打印了3次"File 3 is written."。
    我们希望的是每个文件的下标索引打印一次。

    原因在于写文件是个异步操作,在写完文件调用回调函数时,for循环已经遍历完毕,此时i=3。
    要解决这个问题,可以使用IIFE:

     1 var fs = require('fs');
     2 
     3 var fileContents = ["text1", "text2", "text3"];
     4 for (var i = 0; i < fileContents.length; i++) {
     5     (function(index){
     6         var fileIndex = index;
     7         fs.writeFile("file"+fileIndex+".txt", fileContents[fileIndex], function(err){
     8             if (err) {
     9                 console.log(err)
    10             }
    11             console.log("File " + fileIndex + " is written.")
    12         })
    13     })(i)
    14 }

    这次结果是正确的(尽管不是按序,这不在我们考虑范围内):

    File 1 is written.
    File 2 is written.
    File 0 is written.


    可以看到这里用IIFE做了一个变量捕获,或者说保存。

    再回到myBomb那个例子,这其中用到了一个模式,叫Module模式,很多js模块都是这么写,在IIFE中定义一些私有变量或者私有函数,然后在return的时候导出(一般用一个Object导出)需要暴露给外部的方法。另外在IIFE中定义的变量和函数也不会污染全局作用域,它们都通过统一的入口访问。

    3. 作为独立模块存在,防止命名冲突,命名空间注入(模块解耦)

    可以使用以下代码为ns这个命名空间注入变量和方法:

     1 var ns = ns || {};
     2 
     3 (function (ns){
     4     ns.name = 'Tom';
     5     ns.greet = function(){
     6     console.log('hello!');
     7 }
     8 })(ns);
     9 
    10 console.log(ns); // { name: 'Tom', greet: [Function] }

    还可以扩展到更多的用途:

     1 (function (ns, undefined){
     2     var salary = 5000; // 私有属性
     3     ns.name = 'Tom'; // 公有属性
     4     ns.greet = function(){ // 公有方法
     5         console.log('hello!');
     6     }
     7 
     8     ns.externalEcho = function(msg){
     9         console.log('external echo: ' + msg);
    10         insideEcho(msg);
    11     }
    12 
    13     function insideEcho(msg){ // 私有方法
    14         console.log('inside echo: ' + msg);
    15     }
    16 })(window.ns = window.ns || {});
    17 
    18 console.log(ns.name); // Tom
    19 ns.greet(); // hello
    20 ns.age = 25;
    21 console.log(ns.age); // 25
    22 console.log(ns.salary); // undefined
    23 ns.externalEcho('JavaScript'); // external echo: JavaScript/inside echo: JavaScript
    24 insideEcho('JavaScript'); // Uncaught ReferenceError: insideEcho is not defined
    25 ns.insideEcho('JavaScript'); // Uncaught TypeError: ns.insideEcho is not a function

    在这里,命名空间可以在局部被修改而不重写函数外面的上下文,起到了防止命名冲突的作用。

    注(如果不感兴趣可以直接忽略):还需要解释一下上面IIFE中第二个参数undefined。在js中,undefined表示值的空缺,是预定义的全局变量,它并不是关键字:

    1 console.log(typeof a); // undefined
    2 var a;
    3 console.log(a); // undefined

    undefined有多重含义,第一种是一个数据类型叫做undefined,另一种是表示undefined这个数据类型中的唯一值undefined。我们在js代码中看到的undefined一般是全局对象的一个属性,该属性的初始值就是undefined,另一种情况是,这个undefined是个局部变量,和普通变量一样,它的值可以是undefined,也可以是别的。

    在ECMAScript 3中undefined是可变的,这意味着你可以给undefined赋值,但在ECMAScript 5标准下,无法修改全局的undefined:

    1 console.log(window.undefined); // undefined
    2 window.undefined = 1;
    3 console.log(window.undefined); // undefined

    严格模式下则会直接报错:

    1 'use strict'
    2 
    3 console.log(window.undefined); // undefined
    4 window.undefined = 1;
    5 console.log(window.undefined); // Uncaught TypeError: Cannot assign to read only property 'undefined' of object '#<Window>'

    因此我们需要保护这个局部的undefined:

    1 (function (window, document, undefined) { 
    2     // ... 
    3 })(window, document);

    这时候就算有人给undefined赋值也没有问题:

    1 undefined = true; 
    2 (function (window, document, undefined) { 
    3     // undefined指向的还是一个本地的undefined变量 
    4 })(window, document);

    不过随着ECMAScript 5的普及(现在几乎没有哪款浏览器不支持ECMAScript 5了),这种担忧基本没有必要了,jQuery也是为了最大程度的兼容性才这么做。

    以上例子说明我们可以把命名空间作为参数传给IIFE,以对其进行扩展和装饰:

     1 (function (ns, undefined){
     2     var salary = 5000; // 私有属性
     3     ns.name = 'Tom'; // 公有属性
     4     ns.greet = function(){ // 公有方法
     5         console.log('hello!');
     6     }
     7 
     8     ns.externalEcho = function(msg){
     9         console.log('external echo: ' + msg);
    10         insideEcho(msg);
    11     }
    12 
    13     function insideEcho(msg){
    14         console.log('inside echo: ' + msg);
    15     }    
    16 })(window.ns = window.ns || {});
    17 
    18 (function (ns, undefined){
    19     ns.talk = function(){
    20         console.log(ns.name + ' says hello.');
    21         console.log(ns.name + ' says goodbye.');
    22         // 注意这里不能调用私有函数insideEcho,否则会报错,因为talk和insideEcho不在同一个闭包中
    23     }
    24 })(window.ns = window.ns || {});
    25 
    26 ns.talk(); // Tom says hello. Tom says goodbye.

    命名空间注入

    命名空间注入是IIFE作为命名空间的装饰器和扩展器的一个变体,使其更具有通用性。作用是可以在一个IIFE(这里可以把它理解成一个函数包装器)内部为一个特定的命名空间注入变量/属性和方法,并且在内部使用this指向该命名空间:

     1 var app = app || {};
     2 app.view = {};
     3 
     4 (function (){
     5     var name = 'main';
     6     this.getName = function(){
     7         return name;
     8     }
     9     this.setName = function(newName){
    10         name = newName;
    11     }
    12     this.tabs = {};
    13 }).apply(app.view);
    14 
    15 
    16 (function (){
    17     var selectedIndex = 0;
    18     this.getSelectedIndex = function(){
    19         return selectedIndex;
    20     }
    21     this.setSelectedIndex = function(index){
    22         selectedIndex = index;
    23     }
    24 }).apply(app.view.tabs);
    25 
    26 console.log(app.view.getName()); // main
    27 console.log(app.view.tabs.getSelectedIndex()); // 0
    28 app.view.tabs.setSelectedIndex(1); 
    29 console.log(app.view.tabs.getSelectedIndex()); // 1

    我们还可以写一个模块构造器来批量生产模块:

     1 var ns1 = ns1 || {}, ns2 = ns2 || {};
     2 
     3 var creator = function(val){
     4     var val = val || 0;
     5     this.getVal = function(){
     6         return val;    
     7     }
     8     this.increase = function(){
     9         val += 1;
    10     }
    11     this.reduce = function(){
    12         val -= 1;
    13     }
    14     this.reset = function(){
    15         val = 0;
    16     }
    17 }
    18 
    19 creator.call(ns1);
    20 creator.call(ns2, 100);
    21 console.log(ns1.getVal()); // 0
    22 ns1.increase();
    23 console.log(ns1.getVal()); // 1
    24 console.log(ns2.getVal()); // 100

    对某个私有变量,用API的形式对其进行读写,这其实就是OOP的一些思想在js的应用了。

    本blog已搬迁至https://nullcc.github.io/
  • 相关阅读:
    linux驱动开发学习一:创建一个字符设备
    如何高效的对有序数组去重
    找到缺失的第一个正整数
    .NET不可变集合已经正式发布
    中国人唯一不认可的成功——就是家庭的和睦,人生的平淡【转】
    自己动手搭建 MongoDB 环境,并建立一个 .NET HelloWorld 程序测试
    ASP.NET MVC 中如何用自定义 Handler 来处理来自 AJAX 请求的 HttpRequestValidationException 错误
    自己动手搭建 Redis 环境,并建立一个 .NET HelloWorld 程序测试
    ServiceStack 介绍
    一步一步实战扩展 ASP.NET Route,实现小写 URL、个性化 URL
  • 原文地址:https://www.cnblogs.com/nullcc/p/5827064.html
Copyright © 2011-2022 走看看