躲不掉的函数和闭包

定义函数javascript

在 JS 中定义函数的方式有两种:函数声明函数表达式java

函数声明 的语法为:数组

function functionName(arg0, arg1, arg2) {
    // 函数体;
}
复制代码

函数声明的一个重要特征就是 函数声明提高 ,即在执行代码以前会先读取函数声明,这意味着能够把函数声明放在调用它的语句后面:浏览器

sayHi(); // 'hi';
sayHi() {
    alert('hi');
}
复制代码

第二种建立函数的形式是 函数表达式 。函数表达式有建立几种不一样的语法形式,下面是最多见的一种:闭包

var functionName = function(arg0, arg1, arg2) {
    // 函数体;
}
复制代码

这看来好像是常规的变量复制语句,即建立一个函数并将它赋值给变量 functionName,在这种状况下建立的函数叫作 匿名函数 ( 也称为 Lambda 拉姆达函数 ),在使用前必须先赋值。以下面的代码会报错:app

sayHi(); // 错误,函数还不存在;
var sayHi = function(){
    alert('hi');
}
复制代码

理解函数提高的关键,就是理解函数声明与函数表达式之间的区别。例如,执行如下代码的结果可能会让人意想不到:函数

if(condition) {
    function sayHi() {
        alert('hi');
    }
} else {
    function sayHi() {
        alert('Yo~');
    }
}
复制代码

表面上看,上述代码会在 condition 为 true 时使用一个 sayHi() 的定义,不然就使用另外一个定义。实际上,这在 ECMAScript 中属于无效语法,JavaScript 引擎会尝试修正错误,将其转换为合理的状态,大多数浏览器会返回第二个声明,忽略 condition。ui

不过,若是是使用函数表达式,那就没什么问题:this

var sayHi;
if(condition) {
    sayHi = function() {
        alert('hi');
    }
} else {
    sayHi = function() {
        alert('Yo~');
    }
}
复制代码

这样不一样的函数将根据不一样的 condition 被赋值给 sayHi。spa

递归

递归函数是在一个函数经过名字调用自身的状况下构成的:

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

这是一个经典的递归阶乘函数。虽然这个函数表面上看起来没有问题,但下面的代码却可能致使它出错:

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); // error!
复制代码

在调用 anotherFactorial() 时,因为必须执行 factorial(),而此时 factorial 已再也不是函数,因此会致使错误。

在这种状况下,可使用 arguments.callee 解决问题。

  • arguments 是函数内部对象,它是一个类数组对象,包含着传入函数中的全部参数。arguments 对象有一个 callee 属性,该属性是一个指针,指向 arguments 所在的函数。

所以能够用它来实现对函数的递归调用:

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}
复制代码

经过使用 arguments.callee 代替函数名,能够确保不管怎样调用函数都不会出问题。所以,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。

不过在严格模式下,不能经过脚本访问 arguments.callee,访问这个属性会致使错误。but~ 咱们可使用命名一个函数表达式来完成一样的效果:

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

这样递归调用仍能正常完成。

闭包

很多童鞋老是会混淆 匿名函数闭包 这两个概念。 匿名函数 是没有实际名字的函数;而 闭包 是指有权访问另外一个函数做用域中的变量的函数。

而建立闭包的常见方式,就是在一个函数内部建立另外一个函数:

function compare(name) {
    return function(obj1, obj2) {
        var value1 = obj1[name]; // 能够访问到外部函数中的变量 name;
        var value2 = obj2[name];
        
        if (value1 < value2) {
            return -1;
        } else {
            return 1
        }
    };
}
复制代码

之因此还可以访问这个变量,是由于这个内部函数的做用域链中包含 compare() 的做用域。

而了解做用域的细节,对完全理解闭包相当重要:

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

在函数执行过程当中,为读取和写入变量的值,就须要在做用域链中查找变量,来看下面的例子:

function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else {
        return 1;
    }
}

var result = compare(5, 10);
复制代码

上述代码首先定义了 compare() 函数。而后又在全局做用域中调用了它。

当第一次调用 compare() 时,会建立一个包含 this、arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 this、result 和 compare)在 compare() 执行环境的做用域中则处于第二位。

下图展现了包含上述关系的 compare() 函数执行时的做用域链:

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

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

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

不管何时在函数中访问一个变量时,都会从做用域链中搜索具备相应名字的变量。通常来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域。

但,闭包的状况又有所不一样:

function compare(name) {
    return function(obj1, obj2) {
        var value1 = obj1[name]; // 能够访问到外部函数中的变量 name;
        var value2 = obj2[name];
        
        if (value1 < value2) {
            return -1;
        } else {
            return 1
        }
    };
}
复制代码

在匿名函数从 compare() 中被返回后,它的做用域链被初始化为包含 compare() 函数的活动对象和全局对象。

更为重要的是,compare() 函数在执行完毕后,其活动对象也不会被销毁,由于匿名函数的做用域链仍在引用这个活动对象,直到匿名函数被销毁后,compare() 的活动对象才会被销毁。

// 建立函数;
var compareNames = compare('name');

// 调用函数;
var result = compareNames({name: 'Fly_001'}, { name: 'juejin' });

// 解除对匿名函数的引用,以便释放内存;
compareNmaes = null;
复制代码

Tips: 因为闭包会携带包含它的函数的做用域,所以会比其它函数占用更多的内存,因此过分使用闭包可能会致使内存占用过多。

闭包与变量

做用域链的这种配置机制引出了一个值得注意的反作用,即闭包只能取得包含函数中任何变量的最后一个值。

别忘了闭包保存的是整个变量对象,而不是某个特殊的变量。

下面是一个经典的例子:

function createFunctions() {
    var result = [];
    
    for (var i = 0; i < 10; i ++) {
        result[i] = function() {
            return i;
        };
    }
    
    return result;
}
复制代码

表面上看,彷佛每一个函数都应该返回本身的索引值,但实际上,每一个函数都返回 10。

由于每一个函数的做用域链中都保存着 createFunctions() 函数的活动对象,因此它们引用的都是同一个变量 i。

当 createFunctions() 函数返回时,变量 i 的值是 10,此时每一个函数都引用着变量 i 的同一个变量对象,因此在每一个函数内部 i 的值都是 10。

不过,咱们能够经过建立另外一个匿名函数强制让闭包的行为符合预期:

function createFunctions() {
    var result = [];
    
    for (var i = 0; i < 10; i ++) {
        result[i] = function(num) {
            return function() {
                return num;
            };
        }(i);
    }
    
    return result;
}
复制代码

在这个版本中,咱们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将当即执行该匿名函数的结果赋给数组。

这里的匿名函数有一个参数 num,也就是最终要返回的值。

在调用每一个匿名函数时,咱们传入了变量 i,并会将变量 i 的当前值复制给参数 num,而在这个匿名函数内部,又建立并返回了一个访问 num 的闭包。

因此 result 数组中的每一个函数都有本身 num 变量的一个副本,所以就能够返回各自不一样的数值了。

另外,咱们如今能够用 ES6 中的 let 命令实现上述效果:

function createFunctions() {
    var result = [];
    
    for (let i = 0; i < 10; i ++) {
        result[i] = function() {
            return i;
        };
    }
    
    return result;
}
复制代码

Tips: 由于 let 声明的变量只在所在的块级做用域有效,因此每一次循环的变量 i 都是一个新的变量。

闭包中的 this 对象

咱们知道,this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window;而当函数被做为某个对象的方法调用时,this 等于那个对象。

不过, 匿名函数的执行环境具备全局性, 所以其 this 对象一般指向 window (在经过 call() 或 apply() 改变函数执行环境的状况下,this 就会指向其它对象)。

但有时因为编写闭包的方式不一样,这一点可能不会那么明显:

var name = 'The Window';

var object = {
    name: 'My Object',
    
    getName: function() {
        return function() {
            return this.name;
        };
    }
};

alert(object.getName()()); // 'The Window', ( 在非严格模式下 )
复制代码
  • 因为 getName() 会返回一个函数,因此调用 object.getName()() 就会当即调用它返回的函数。

不过,把外部做用域中的 this 对象保存在一个闭包可以访问到的变量里,就可让闭包访问该对象了:

var name = 'The Window';

var object = {
    name: 'My Object',
    
    getName: function() {
        var that = this;
        return function() {
            return that.name;
        };
    }
};

alert(object.getName()()); // 'My Object';
复制代码

在定义匿名函数以前,咱们把 this 对象赋值给 that 变量,且闭包也能够访问这个变量,即便在函数返回后,that 也仍然引用着 object,因此调用 object.getName()() 就返回了 'My Object'。

JavaScript 中的函数表达式和闭包都是极其有用的特性,利用它们能够实现不少功能。

不过,由于建立闭包必须维护额外的做用域,过分使用它们可能会占用大量内存,因此不要为了闭包而闭包~

关于函数和闭包的浅薄知识就先讲到这里,若有不正确的地方,欢迎各位指正。【比心】

相关文章
相关标签/搜索