• 理解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/
  • 相关阅读:
    How can I pretty-print JSON in python?
    怎样取出cobbler kopts中设置的参数?
    提问的智慧
    mysql中binary相加的问题
    sql之left join、right join、inner join的区别
    js中数组遍历for与for in区别(强烈建议不要使用for in遍历数组)
    python setup.py uninstall
    Caused by: org.hibernate.exception.SQLGrammarException: could not extract ResultSet
    idea中的插件,可以快速将类中的属性转换成Json字符串
    上传照片时候,可以通过配置文件和写配置类,实现控制上传文件的大小
  • 原文地址:https://www.cnblogs.com/nullcc/p/5827064.html
Copyright © 2020-2023  润新知