重学JavaScript之匿名函数
1. 什么是匿名函数?
匿名函数就是没有名字的函数,有时候也称为《 拉姆达函数》。匿名函数是一种强大的令人难以置信的工具。如下:
function a(a1, a2, a3) {
// 函数体
}
其他函数表达式
var a = function(a1, a2, a3) {
// 函数体
}
以上两个例子在逻辑上等价,其主要的区别是: 前者会在代码执行前被加载到作用域中,而后者则是在代码执行到那一行的时候才会有定义。另一个重要的区别就是:函数声明会给函数一个指定的名字,而函数表达式则是:创建一个匿名函数,然后将这个匿名函数赋给一个变量。
function(a1, a2, a3) {
// 函数体
}
上面例子也是完全可以的,但是却无法调用这个函数,因为没有指向这个函数的指针,但是可以将这个函数作为参数传入另外一个函数,或者从一个函数中返回另一个函数时就可以使用这种形式来定义匿名函数。
2. 递归
递归函数是在一个函数通过名字调用自身的情况下构成的
function f(num) {
if (num <= 1) {
retrun 1
} else {
return num * f(num - 1)
}
}
以上,这是一个经典的递归阶乘函数,表面上没有任何问题,但是却会被以下代码导致出错:
var a = f
f = null
console.log(a(4) // 报错
以上代码先把 f() 函数保存在变量 a 中,然后将f变量设置为 null ,结果指向原始函数的引用只剩下一个。但在接下来调用 a() 时,由于必须执行 f(),但 f 已经不是函数,所有就会报错。这个时候可以使用 arguments.callee
function f(num) {
if (num <= 1) {
return 1
} else {
return num * arguments.callee(num - 1)
// 通过 arguments.callee 代替函数名,可以保证不会出问题
}
}
var a = f
a = null
a(4) // 24
3. 闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的方式:在一个函数内部创建另一个函数。
function c(p) {
retrun function(o1,o2){
// var v1 = o1[p]
// var v2 = o2[p]
if (v1 < v2) {
return -1
} else if (v1 > v2) {
retrun 1
}else {
retrun 0
}
}
}
在上面代码中,有标记的两行是匿名函数中的代码。这两行代码访问了外部函数中的变量 p。即使这个内部函数被返回了,而且被其他地方调用了,但它仍然可以访问变量 p。之所以还能够访问这个变量,是因为函数的作用域链中包含了c()的作用域。
当某个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([Scope])。然后,使用 this、arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部活动对象处于第三位。直到作为作用域链重点的全局执行环境。
- 在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。后台的每个执行环境都有一个表示变量的对象--变量对象。
- 全局环境的变量对象始终存在,而局部环境的变量对象,则只在函数执行的过程中存在。我们在创建函数的时候会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[Scope]属性中,当调用函数时,会为函数创建一个执行环境,然后通过赋值函数的[Scope]属性中的对象构建起执行环境的作用域链。
- 如果这时候有一个变量对象被创建并被推入执行环境作用域链的前端,对于一开始创建的函数的执行环境而言,其作用域链中包含两个变量:本地活动对象和全局变量对象。所以,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
无论什么时候函数在访问一个变量时,就会从作用域链中搜索具有相同名字的变量,函数执行完成后,局部活动对象将被销毁,内存中仅保存全局作用域。但是由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
在一个函数内部定义的函数会将外部函数的活动对象添加到它的作用域链中。内部函数在外部函数中被返回后,它的作用域链被初始化为包含外部函数的活动对象和全局变量对象,这样内部函数就可以访问外部函数中定义的所有的变量。所以在外部函数执行结束后,它并不会被销毁,因为内部函数的作用域链还在引用这个活动对象。也就是说外部函数执行结束后,它的作用域链会被销毁,但是活动对象还在内存中,直到内部函数被销毁后。
3.1 闭包与变量
作用域链的这种配置引出了一个副作用,闭包只能取得包含函数中任何变量的最后一个值。
3.2 关于 this 对象
在闭包中使用this 也可能会导致一些问题。因为this对象是在运行时基于函数的执行环境绑定的。在全局函数中 this === window,函数被作为某个对象的方法调用时,this就等于那个对象。匿名函数的执行环境具有全局性,因此其this 对象通常指向window。但是这并不是绝对的。
在函数被调用的时候,其活动对象都会自动获得两个特殊变量:this 和 arguments。 内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。如果把外部作用域中的this对象保存在一个闭包能够访问的变量里,就可以让闭包访问该对象了。
3.3 内存泄露
由于IE对JS对象和 COM对象使用不同的垃圾收集例程,因此闭包在IE中会导致一些特殊的问题。也就是说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。
注意:闭包会引用包含函数的整个活动对象,而其中包含着变量,即使闭包不直接引用变量,包含函数的活动对象中也仍然会保存一个引用。因此把变量设置为 null ,这样就能够解除对DOM对象的引用,减少其引用数,确保正常回收其占用的内存。
4、 模仿块级作用域
vJS没有块级作用域的概念,这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。JS从来不会告诉你是否多次声明了同一个变量,它总是对后续的声明视而不见。我们可以通过匿名函数来模仿块级作用域从而避免这个问题。
(function () {
// 块级作用域
})()
5、私有变量
严格来说在JS中并没有私有成员的概念:所有对象属性都是公有的。不过倒是有一个私有变量的概念。任何在函数中定义的变量都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
在函数内部如果有私有变量,那么在函数内部可以访问这个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。
我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式,
第一种:在构造函数中定义
function m (){
let p = 10
function p (){
retrun false
}
// 特权方法
this.pb = function () {
p++
retrun p()
}
}
第二种:静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。和在构造函数中定义特权方法的区别在于私有变量和函数是由实例共享的,由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。
多查找作用域链中的一个层次,就会在一定程度上影响查找速度。这正是闭包和私有变量一个不足之处。
5.1 模块模式
指的是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例对象,按照惯例,JS是以对象字面量的方式来创建单例对象的:
var s = {
name : v,
method: function(){
// 方法的代码
}
}
6、总结
匿名函数,也称为拉姆达函数,是一种使用JS函数的强大方式。有如下特点:
- 任何函数表达式从技术上说都是匿名函数,因为没有引用它们的确定的方式
- 在无法确定如何引用函数的情况下,递归函数就会变得比较复杂
- 递归函数应该始终使用 argument.callee来递归地调用自身,不要使用函数名,因为函数名可能会发生变化。
当函数内部定义了其他函数时,就创建s了闭包,闭包有权访问包含函数内部的所有变量。
- 在后台执行环境汇总,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域;
- 通常,函数的作用域及所有变量都会在函数执行结束后被销毁
- 但是,如果函数返回了一个闭包时, 这个函数的作用域将会一直在内存中保存到闭包不存在为止
使用闭包可以在JS中模仿块级作用域
- 创建并立即调用一个函数,这样即可以执行其中的代码,又不会在内存中留下对该函数的引用
- 结果就是函数内部的所有变量都会被立即销毁--除非将某些变量赋值给了包含作用域中的变量
闭包可以用于对象中创建私有变量
- 即使JS中没有正式的私有对象属性概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。
- 有权访问私有变量的公有方法叫做 特权方法
- 可以使用构造函数、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。
JS中的匿名函数和闭包都是非常的特性,但是要注意使用场景和方法。
欢迎关注 前端公众号【小夭同学】