一、函数表达式

1、 函数提升

javascript 众所周知的特点就是会进行函数提升,但是函数提升的前提是函数是通过 function 关键字 声明的才可以。

使用函数表达式声明的函数是不能进行函数提升的

因此下面的做法是错误的

func(); // error

var func  = function () {
    console.log(1);
}

2、函数表达式和function函数声明

下面的示例参考《javascript高级程序设计》,但是书里面的一些文字不是最新的浏览器,因此我全部测试了一下

1) function函数声明示例 :

下面的做法是错误的:

一些浏览器会直接返回第二个函数,而不会根据flag进行判断

  • 我使用最新的 chrome 和 firefox 浏览器能够根据flag进行判断,返回正确的func()函数
  • 我使用360就是直接返回第二个,flag判断是无效的
  • IE11可以根据flag判断返回正确的函数,但是IE10以及之前的浏览器都直接返回第二个函数
    var flag = true;
    if (flag) {
        function func() {
            console.log(true);
        }
    } else {
        function func() {
            console.log(false);
        }
    }
    func();

2)函数表达式示例:

使用函数表达式就能够正确的根据flag返回正确的函数。

    var flag = true;
    if (flag) {
        var func2 = function () {
            console.log(true);
        }
    } else {
        var func2 = function () {
            console.log(false);
        }
    }
    func2(); // true

二、闭包

1、闭包示例

这个示例参考自红宝书

目的:对某个对象的某个属性进行比较.

函数 createCompairisonFunction() 传入一个属性的名字,然后返回一个匿名函数。

由于函数能够访问全局变量,而闭包函数也是如此(这里的全局变量是指能够访问外层的变量)

    // 闭包
    function createCompairisonFunction(propertyName) {
        console.log(arguments);
        return function (object1, object2) {
            console.log(arguments);
            var value1 = object1[propertyName];
            var value2 = object2[propertyName];
            if (value1 < value2) {
                return -1;
            } else if (value1 > value2) {
                return 1;
            } else {
                return 0;
            }
        }
    }
    var compairAge = createCompairisonFunction("age");
    function Person(age) {
        this.age = age;
    }
    var p1 = new Person(20);
    var p2 = new Person(21);
    console.log(compairAge(p1, p2)); // -1

两个arguments的打印:

1-1.jpg

2、 闭包的本质

这个本质是我自己的理解,而非官方的解释

闭包最大的特点就是在返回匿名函数之后,在新的函数中依旧能够使用外层函数的变量。

  • 上面的示例中能够看到,在创建了compairAge()之后,依旧能够访问变量 age

闭包的本质是基于作用域链

函数的作用域链:

函数调用的时候会创建一个执行环境和作用域链。 通过作用域链查找变量过程中还有一个概念叫做活动对象

活动对象是处于作用域链中的每一个层次,从内到外。

在作用域链中:

  • arguments创建的局部变量 会处于第一位
  • 外部函数的活动对象处于第二位
  • 外部函数的外部函数的活动对象处于第三位
  • 以此类推直到全局变量

一般函数在执行之后,局部的活动对象会销毁,只保存全局的活动对象(全局作用域)

对于闭包来说:

返回的匿名函数实际上包含了外部函数的活动对象全局变量对象。 因此匿名函数能够调用外部函数定义的变量和全局变量对象。

同时,由于返回的匿名函数依旧在引用外部函数的活动对象,因此外部函数也就是createCompairisonFunction()的活动对象也没有销毁,因此还在compairAge的引用链中。

只有当匿名函数被销毁之后,才会结束匿名函数的作用域链,此时外部函数的活动对象也会被标记为清楚,垃圾回收机制在下一次回收过程中会回收这些内存中的对象

三、闭包可能存在的问题

1、内存占用

由于闭包,会导致外部函数的作用域(活动对象)依旧会留存在内存中,闭包用多了之后就会导致内存占用过多。(V8优化了,会尝试回收闭包占用的内存----只是尝试)

2、变量的副作用

下面的代码会出现问题,导致所有的函数输出都是 3 而不是与其的 0 1 2.

    var arr = [];
    function outFunc() {
        for (var i = 0; i < 3; i++) {
            arr.push(function inFunc() { console.log(i); });
        }
    }
    outFunc();
    arr[0](); // 3
    arr[1](); // 3
    arr[2](); // 3

出现问题的原因是:

闭包只能取得包含函数(外部函数)中任何变量的最后一个值

因此实际上包含函数中的 i 的最后一个值是 3 ,因此所有的匿名函数返回的时候,匿名函数引用的是同一个活动对象
i,实际上都是 3

下面的代码可以解决这个问题:

    var arr2 = [];
    function outFunc2() {
        for (var i = 0; i < 3; i++) {
            arr2.push(
                (function (num) {
                    return function () {
                        console.log(num);
                    };
                })(i)
            );
        }
    }
    outFunc2();
    arr2[0](); // 0
    arr2[1](); // 1
    arr2[2](); // 2

上面的代码中,没有直接将 i 在匿名函数中返回,而是创建了一个新的匿名函数,并且这个匿名函数是一个立即执行函数,将 本次的 i 传递给参数 num ,把 num 作为最终函数需要返回的内容。

这样做的目的是:

函数传值参数时是按值传递,而且立即执行,因此传入的 i 就是实时的i。通过新的立即执行函数 function (num) {} () 每个return中是 num 的副本,从而返回实时 i

3、IE9的内存泄露

由于IE9的垃圾回收机制,导致当闭包中存在一个HTML元素的引用时,(该HTML元素处于闭包的作用域链中),该元素将无法销毁

下面示例来自红宝书

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

如果要避免这种情况出现,需要对assignHandler进行改变:

将需要的内容单独拿出来作为一个局部变量,然后HTML元素使用过后,手动元素进行销毁。

闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用.

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

四、块级作用域

javascript没有块级作用域,只能模拟块级作用域。

我个人理解块级作用域无非就是利用函数的私有作用域来实现,最常用的方式就是立即执行函数

(function(){
    // 块级作用域
})();

上面通过立即执行函数模拟了一个块级作用域,其中匿名函数是立即执行的,因此在执行之后,匿名函数内的局部变量都会被销毁。

将函数声明转换为函数表达式需要在function(){}两边加上括号,而后面的()可以为匿名函数传递参数,就像上面【闭包可能存在的问题-变量的副作用】中提到的做法。

块级作用域能够使得不同的开发者在协作的时候,不用考虑全局变量的命名冲突的问题。可以发现很多jquery的插件也是这样做的。

    // 块级作用域
    function outputNumbers(num) {
        (function (num2) {
            for (var i = 0; i < num2; i++) {
                console.log(i);
            }
        })(num);
        // console.log(i); // 导致错误
    }