zoukankan      html  css  js  c++  java
  • js函数表达式重点知识

    参考书籍:《JavaScript高级程序设计(第3版)》

    函数表达式

    函数表达式是JavaScript中的一个即强大又容易令人困惑的特性。

    function functionName(arg0, arg1, arg2) {
        // 函数体
    }

    Firefox,Safari,Chrome和Opera都给函数定义了一个非标准的name属性 

    // 只在Firefox,Safari,Chrome,Opera有效
    console.log(functionName.name);
    // functionName

    关于函数声明,它的一个重要特征就是函数声明提升

    函数表达式

    var functionName = function (arg0, arg1, arg2) {
        // 函数体
    }

    function关键字后面没有标识符,这叫匿名函数。

    理解函数提升的关键,就是lijie 函数声明与函数表达式之间的区别。

    请看一个复杂的函数createComparisonFunction

    function createComparisonFunction (propertyName) {
        return function (object1, object2) {
            var value1 = object1[propertyName];
            var value2 = object2[propertyName];
    
            if (value1 < value2) {
                return -1;
            } else if (value > value2) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    createComparisonFunction()返回了一个匿名函数。返回的函数可能会被赋值给一个变量,或者以其他方式被调用。

    在把函数当成值来使用的情况下,都可以使用匿名函数。

    递归

    递归函数是在一个函数通过名字调用自身的情况下构成的。

    function factorial (num) {
        if (num <= 1) {
            return 1;
        } else {
            return num * factorial(num - 1);
        }
    }

    这是一个经典的递归阶乘函数。

    优化这个函数

    var factorial = (function f(num) {
        if (num <= 1) {
            return 1;
        } else {
            return num * f(num - 1)
        }
    })

    闭包

    闭包是指有权访问另一个函数作用域中的变量的函数。

    创建闭包的常见方式,就是在一个函数内部创建另一个函数。以前面的createComparisonFunction()函数为例。

    function createComparisonFunction (propertyName) {
        return function (object1, object2) {
            var value1 = object1[propertyName];
            var value2 = object2[propertyName];
    
            if (value1 < value2) {
                return -1;
            } else if (value > value2) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    在这个例子中,突出的那两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部函数中的变量propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含createComparisonFunction()的作用域。

    要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。

    当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。

    在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。

    来看下面的例子

    function compare (value1, value2) {
        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    }
    
    var result = compare(5, 10);

    以上代码先定义了compare()函数,然后又在全局作用域中调用了它。当调用compare()时,会创建一个包含arguments,value1,value2的活动对象。全局执行环境的变量对象(包含result和compare)在compare()执行环境的作用域链中则处于第二位。

     上图展示了包含上述关系的compare()函数执行时的作用域链。

    后台的每个执行环境都有一个表示变量的对象——变量对象。

    全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。

    作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含对象。

    一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是,闭包的情况又有所不同。

    在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。在createComparisonFunction()执行结束的时候,这个活动对象不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。

    匿名函数被销毁后,这个外部函数的活动对象才会被销毁。

    // 创建函数
    var compareNames = createComparisonFunction("name");
    
    // 调用函数
    var result = compareNames({ name: "Nicholas"}, { name: "Greg" });
    
    // 解除对匿名函数的引用(以便释放内存)
    compareNames = null;

    注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

    闭包与变量

    请看一个例子

    function createFunctions () {
        var result = new Array();
    
        for (var i = 0; i < 10; i++) {
            result[i] = function () {
                return i;
            }
        }
    
        return result;
    }
    
    var fns = createFunctions();
    
    console.log(fns[1]());

    这个函数会生成一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值,即fns[0]() 返回0,fns[1]() 返回1,但实际上,每个函数都返回10.

    因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以他们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部的i的值都是10.

    如何优化这个函数?

    通过创建另一个匿名函数强制让闭包的行为符合预期

    function createFunctions () {
        var result = new Array();
    
        for (var i = 0; i < 10; i++) {
            result[i] = function(num) {
                return function() {
                    return num;
                }
            }(i)
        }
    
        return result;
    }
    
    var fns = createFunctions();
    
    console.log(fns[1]());

    关于this对象

    在闭包中使用this对象可能会导致一些问题。

    var name = "The Window";
    
    var object = {
        name: "My Object",
    
        getNameFunc: function () {
            return function () {
                return this.name
            }
        }
    }
    
    var getName = object.getNameFunc();
    console.log(getName()); // The Window

    把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示。

    var name = "The Window";
    
    var object = {
        name: "My Object",
    
        getNameFunc: function () {
            var that = this;
            return function () {
                return that.name
            }
        }
    }
    
    var getName = object.getNameFunc();
    console.log(getName()); // My Object

    内存泄漏

    由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。

    具体来说,如果闭包的作用域链中保存着一个HTML元素,那么久意味着该元素将无法被销毁。来看下面的例子。

    function assignHandler () {
        var element = document.getElementById("someElement");
        element.onclick = function () {
            alert(element.id);
        }
    }

    以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就用于不会被回收。优化代码如下。

    function assignHandler () {
        var element = document.getElementById("someElement");
        var id = element.id;
    
        element.onclick = function () {
            alert(id);
        }
    
        element = null;
    }

    模仿块级作用域

    通常称为私有作用域

    (function(){
        // 这里是块级作用域
    })()

    请看下面一段代码:

    function outputNumbers(count) {
        (function(){
            for (var i = 0; i < count; i++) {
                console.log(i)
            }
        })();
        console.log(i); // 导致一个错误
    }
    
    outputNumbers(3);

    一般来说,我们都应该尽量少向全局作用域中添加变量和函数。

    另一个例子:

    (function () {
        var now = new Date();
        if (now.getMonth() == 0 && now.getDate() == 1) {
            console.log('Happy New Year!')
        } else {
            console.log("It's a normal day")
        }
    })()

    私有变量

    任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。

    私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。来看下面的例子。

    function add (num1, num2) {
        var sum = num1 + num2;
        return sum;
    }

    在这个函数内部,有3个私有变量:num1, num2,sum.在函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。

    我们把有权访问私有变量和私有函数的公有方法称为特权方法

    有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下:

    function myObject () {
        var privateVariable = 10;
    
        function privateFunction () {
            return false;
        }
    
        this.publicMethod = function () {
            privateVariable++;
            return privateFunction();
        }
    }
    
    var obj = new myObject();
    obj.publicMethod();

    利用私有和特权成员,可以隐藏那么不应该被直接修改的数据,例如:

     1 function Person (name) {
     2     this.getName = function () {
     3         return name;
     4     }
     5     this.setName = function (value) {
     6         name = value
     7     }
     8 }
     9 
    10 var a = new Person('cathy');
    11 console.log(a.getName());
    12 a.setName('nick');
    13 console.log(a.getName());

    构造函数模式的缺点是针对每个实例都会创建同样一组新方法。

    多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个明显的不足之处。

    模块模式

    为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。

    var singleton = {
        name: value,
        method: function () {
            // 这里是方法的代码
        }
    }

    模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:

     1 var singleton = function () {
     2     // 私有变量和私有函数
     3     var privateVariable = 10;
     4 
     5     function privateFunction () {
     6         return false;
     7     }
     8 
     9     return {
    10         publicProperty: true,
    11         publicMethod: function () {
    12             privateVariable++;
    13             return privateFunction();
    14         }
    15     }
    16 }

    这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。

    从本质上来讲,这个对象字面量定义的是单例的公共接口。

    这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。例如: 

    var application = function () {
        // 私有变量和函数
        var components = new Array();
    
        // 初始化
        components.push(new BaseComponent());
    
        // 公共
        return {
            getComponentCount: function () {
                return components.length;
            },
            registerComponent: function(component) {
                if (typeof component == 'object') {
                    components.push(component);
                }
            }
        }
    }

    在Web应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的application对象。在创建这个对象的过程中,首先声明了一个私有的components数组,并向数组中添加了一个BaseComponent的新实例。而返回对象的getComponentsCount和registerComponent方法都是有权访问数组components的特权方法。

    增强的模块模式

    有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。

    var singleton = function () {
    
        // 私有变量和私有函数
        var privateVariable = 10;
    
        function privateFunction () {
            return false;
        }
    
        // 创建对象
        var object = new CustomType();
    
        // 添加特权/公有属性和方法
        object.publicProperty = true;
    
        object.publicMethod = function () {
            privateVariable++;
            return privateFunction();
        }
    
        // 返回这个对象
        return object;
    } ();

    如果前面演示模块模式的例子中的application对象必须是BaseComponent的实例,那么就可以使用以下代码。

    var application = function () {
    
        // 私有变量和函数
        var components = new Array();
    
        // 初始化
        components.push(new BaseComponent());
    
        // 创建application的一个局部副本
        var app = new BaseComponent();
    
        // 公共接口
        app.getComponentCount = function () {
            return components.length;
        }
    
        app.registerComponent = function () {
            if (typeof component == 'object') {
                components.push(component)
            }
        }
    
        // 返回这个副本
        return app;
    }();

    在这个重写后的应用程序单例中,首先也像前面例子中一样定义了私有变量。主要的不同之处在于命名变量app的创建过程,因为它必须是BaseComponent的实例。这个实例实际上是application对象的局部变量版。此后,我们又为app对象添加了能够访问私有变量的公有方法。最后一步是返回app对象,结果仍然是将它赋值给全局变量application.

    小结

    在JavaScript编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名,从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用JavaScript函数的强大方式。以下总结了函数表达式的特点。

    在无法确定如何引用函数的情况下,递归函数就会变得比较复杂。

    当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量。原理如下:

    1. 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。

    2. 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。

    3. 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

    使用闭包可以在JavaScript中模仿块级作用域,要点如下:

    1. 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。

    2. 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。

    闭包还可以用于在对象中创建私有变量,相关概念和要点如下:

    1. 即使JavaScript中没有正式的私有变量对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。

    2. 有权访问私有变量的公有方法叫做特权方法。

    3. 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。

    JavaScript中的函数表达式和闭包都是及其有用的特性,利用它们可以实现很多功能。不过因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。

  • 相关阅读:
    数组和链表
    IAP升级
    使用Git和Github来管理自己的代码和笔记
    Eeprom和Flash的区别
    程序员的七种武器
    数据结构和算法的关系
    STVD、IAR两种编译器比较
    STM32与STM8操作寄存器的区别
    Linux用户身份与文件权限
    地图缩放到指定经纬度
  • 原文地址:https://www.cnblogs.com/cathy1024/p/12287544.html
Copyright © 2011-2022 走看看