手撕闭包

| 字数 1190
  1. 1. 闭包是什么?
  2. 2. 闭包产生过程详解
  3. 3. 闭包应用场景
    1. 3.1. 私有变量 —— 限制变量作用域
    2. 3.2. 回调(callback)与计时器(timer)
    3. 3.3. 即时函数与闭包的组合
  1. 1. 闭包是什么?
  2. 2. 闭包产生过程详解
  3. 3. 闭包应用场景
    1. 3.1. 私有变量 —— 限制变量作用域
    2. 3.2. 回调(callback)与计时器(timer)
    3. 3.3. 即时函数与闭包的组合

既然说到了手撕那么我们就按照下列顺序一步步来看。

  1. 闭包是什么?
  2. 闭包产生过程详解
  3. 闭包常用场景示例

闭包是什么?

  1. 高程三:闭包是指有权访问另一个函数作用域中的变量的函数。
  2. Javasript Ninja:闭包是一个函数在创建时允许该自身函数访问并操作该自身函数之外的变量时所创建的作用域。

  3. 总结一下,闭包是有权操作除自己外函数作用域中变量的函数。

这里对于上面的总结我们需要注意的有三点,也就是闭包的三个特点。首先闭包是一个 函数,其次闭包具有操作(读写)除自己外函数作用域的能力,最后闭包可以操作(读写)的是 变量

闭包产生过程详解

这里还是取大家常看的高程三上的例子来举例说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createComparisonFunction (propertyName) {
return function (obj1, obj2) {
var val1 = obj1[propertyName];
var val2 = obj2[propertyName];

if (val1 < val2) {
return -1;
} else if (val1 > val2) {
return 1;
} else {
return 0;
}
}
}

var compareNames = createComparisonFunction("name");
var result = compareName({ name: "Nicholas" }, { name: "Greg" });

下列 AO,代表执行环境中创建的 activity object,可以对应 variable object(VO) 来记忆。此时 js 引擎中的处理顺序如下图:

此时匿名函数中的val1/val2 获得了 propertyName ,拥有了读的能力,所以形成了闭包。而且在匿名函数的执行环境中不仅包括了自己的 AO 还包括了 createComparsonFunction 执行环境的 AO(分别有 createComparson AO&Global AO),如下图:

什么是 VO? 什么是执行环境?可以参考我的上一篇文章手撕作用域&上下文
由上我们知道了 闭包其实在形成的过程中携带了包含它的函数的作用域,正因为如此,所以闭包才有读写除自己外函数作用域中变量的能力。但这样拥有过多作用域会占用大量内存 ,我们可以通过 compareNames = null;来手动释放内存。如果大量使用闭包的话,我们不可能一个个手动去释放内存,所以还是请大家慎用闭包。

闭包应用场景

私有变量 —— 限制变量作用域

说得简单点就是模拟其他面向对象语言中的变量修饰符 private,给变量增加权限控制。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
function Ninja () {
var feints = 0;
this.getFeints = function () {
return feints;
};
this.feint = function () {
feints++;
};
}
var ninja = new Ninja();
ninja.feint();
console.log(ninja.getFeints() == 1); // true

使变量 feints 私有,让外部不能直接访问。如果想访问只能通过 getFeints 方法访问,这里的 getFeints 方法即形成了闭包,因为在这个函数中拥有访问 feints 变量的能力。这里如果将 var feints = 0; 改为 this.feints = 0; 外部即可访问 =。= 聪明的你一定一眼就看出了为什么吧?就是因为上下文!可以参考我的上一篇文章 手撕作用域&上下文

回调(callback)与计时器(timer)

来看一个回调的例子。如下:

1
2
3
4
5
6
7
8
9
10
11
$('#btn').click(function () {
var elem = $('btnObject');
elem.html("Loading ...");

$.ajax({
url: 'test.html',
success: function (data) {
elem.html(data);
}
});
});

在 success 回调中有能力操作 elem ,即形成了闭包。计时器类似,大家可以自己去写写类似的。

即时函数与闭包的组合

  由于即时函数是立即执行,其内部所有的函数、所有的变量都局限于其内部作用域>。我们可以使用即时函数创建一个临时作用域,用于存储数据状态
。想起一到经典的 JS题目。代码如下:

1
2
3
4
5
6
7
8
9
function createFuntcions () {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i;
};
}
return result;
}

为什么 result 总是10?因为每个函数的作用域链中都保存着createFunction 的活动对象,所以他们引用的都是同一个变量 i 。当 createFunction 返回后 i 的值为 10 ,此时每个函数都引用着保存变量 i 的同一个变量对象,所以每个函数内部 i 的值都是10。(所有函数都拥有的是同一个词法作用域,要想使每一个闭包保留当时的对i的引用,我们需要对每一个闭包新建一个作用域。)可以使用立即执行函数&let创建函数作用域&块级作用域改进代码如下:

1
2
3
4
5
6
7
8
9
function createFuntcions () {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = (function (i) {
return i;
})(i);
}
return result;
}
1
2
3
4
5
for (let i = 0; i < 10; i++) {
result[i] = function (i) {
return i;
};
}