第三章-闭包和高阶函数
# 闭包
闭包的形成与变量的作用域以及变量的生存周期密切相关,所以在讲解闭包之前,我们先来学习一下这两个知识点。
# 变量的作用域
变量的作用域,指的就是变量的有效范围。当在函数中声明一个变量时,如果该变量前面没有带上关键字 var,这个变量就会成为全局变量。如果在函数中使用 var 声明的变量,这时候的变量就是局部变量,只有在该函数内部才能访问到这个变量,在函数外部是访问不到的,如下:
var func = function() {
var a = 1;
console.log(1); // 1
}
func();
console.log(a); // Uncaught ReferenceError: a is not defined
2
3
4
5
6
7
# 变量的生存周期
全局变量的生存周期是永久的,但是函数内部的变量会随着函数调用的结束而被销毁。现在看看下面这段代码:
var func = function() {
var a = 1;
return function() {
a++;
console.log(a);
}
};
var f = func();
f(); // 2
f(); // 3
f(); // 4
2
3
4
5
6
7
8
9
10
11
12
跟我们之前的推论相反,在函数退出后,函数内的变量并没有被销毁,而是一直存活着。这是因为在执行 var f = func();时,f 返回了一个匿名函数,它可以访问到 func() 被调用时的环境,而局部变量 a 一直处于这个环境中。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的里有。在这里产生了一个闭包结果,局部变量的生存周期看起来被延续了。
下面的代码就是利用了闭包的特性来判断变量类型:
var Type = {};
for (var i = 0, type; type = ['String', 'Array', 'Number'][i++];) {
(function(type){
Type['is' + type] = function(obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
}
})(type)
}
console.log(Type.isArray([])); // true
console.log(Type.isString('str')); // true
2
3
4
5
6
7
8
9
10
11
12
# 闭包的更多作用
# 封装变量
闭包可以把一些不需要暴露在全局的变量封装成"私有变量"。如下示例有一个计算乘积的函数:
var mult = function() {
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
}
console.log(mult(2, 3, 4)); // 24
2
3
4
5
6
7
8
mult 函数可以接收一些 number 类型的参数,并返回这些参数的乘积。但是对于相同的参与来说,每次都要重新计算,显然有点浪费资源,于是我们可以加入缓存机制来提高这个函数的性能:
var cache = {}
var mult = function() {
var args = Array.prototype.join.call(arguments, ',');
if (cache[args]) {
return cache[args];
}
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return cache[args] = a;
}
console.log(mult(2, 3, 4)); // 24
console.log(mult(2, 3, 4)); // 24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到 cache 这个变量仅仅在 mult 函数内部使用了,与其让它暴露在全局作用域下,不如把它封闭在 mult 函数内部,这样可以减少页面中的全局变量,以避免这个变量在其他地方被修改而导致错误,修改后的代码如下:
var mult = (function () {
var cache = {};
return function () {
var args = Array.prototype.join.call(arguments, ',');
if (cache[args]) {
return cache[args];
}
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return (cache[args] = a);
};
})();
console.log(mult(2, 3, 4)); // 24
console.log(mult(2, 3, 4)); // 24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 闭包和面向对象设计
看看下面这段跟闭包相关的代码:
var extent = function () {
var value = 0;
return {
call: function () {
value++;
console.log(value);
},
};
};
var instance = extent();
instance.call(); // 1
instance.call(); // 2
instance.call(); // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
如果换成面向对象的写法,就是:
var extent = {
value: 0,
call: function() {
this.value++;
console.log(this.value)
}
}
extent.call(); // 1
extent.call(); // 2
extent.call(); // 3
2
3
4
5
6
7
8
9
10
11
或者是这样的:
var Extent = function() {
this.value = 0;
}
Extent.prototype.call = function() {
this.value++;
console.log(this.value)
}
var instance = new Extent();
instance.call(); // 1
instance.call(); // 2
instance.call(); // 3
2
3
4
5
6
7
8
9
10
11
12
13
# 用闭包实现命令模式
没有使用闭包之前的代码如下:
var TV = {
open: function () {
console.log('打开电视机');
},
close: function () {
console.log('关闭电视机');
},
};
var OpenTvCommand = function (receiver) {
this.receiver = receiver;
};
OpenTvCommand.prototype.execute = function () {
this.receiver.open();
};
OpenTvCommand.prototype.undo = function () {
this.receiver.close();
};
var setCommand = function (command) {
document.getElementById('execute').onclick = function () {
command.execute();
};
document.getElementById('undo').onclick = function () {
command.undo();
};
};
setCommand(new OpenTvCommand(TV));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
使用闭包之后的代码:
var TV = {
open: function () {
console.log('打开电视机');
},
close: function () {
console.log('关闭电视机');
},
};
var createCommand = function(receiver) {
var execute = function() {
receiver.open();
}
var undo = function() {
receiver.close()
}
return {
execute: execute,
undo: undo
}
}
var setCommand = function (command) {
document.getElementById('execute').onclick = function () {
command.execute();
};
document.getElementById('undo').onclick = function () {
command.undo();
};
};
setCommand(new createCommand(TV));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 闭包与内存管理
闭包是一个非常强大的特性,但是人们对闭包有时会产生误解,比如一种说法是闭包会产生内存泄漏,所以要尽量减少闭包的使用。
其实正常的使用情况下,是不会产生内存泄漏的。但是如果使用闭包的作用域中保存着一些 DOM 节点,这时候就可能会造成内存泄漏。但是这并非是闭包的问题。在 IE 浏览器中,由于 BOM 和 DOM 中的对象都是使用 C++ 以 COM 对象的方式实现的,而 COM 对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收。如果要解决训话引用带来的内存泄漏问题,我们只需要把循环引用中的变量设为 null 即可。
# 高阶函数
高阶函数是指至少满足下列条件之一的函数:
- 函数可以作为参数被传递;
- 函数可以作为返回值输出;
# 函数作为参数传递
回调函数
var getUserInfo = function(userId, callback) { $.ajax('http://xxx.com/getUserInfo' + userId, function(data) { if (typeof callback === 'function') { callback(data); } }) } getUserInfo(123, function(data) { console.log(data.userName); });
1
2
3
4
5
6
7
8
9
10
11Array.prototype.sort
Array.prototype.sort 接受一个函数当做参数,在这个函数里面封装了数组元素的排序规则。从 Array.prototype.sort 的使用可以看到,我们的目的是对数组进行排序。但是使用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里,动态传入 Array.prototype.sort,使 Array.prototype.sort 方法成为了一个非常灵活的方法,代码如下:
// 从小到大排序 [1, 4, 3].sort(function(a, b) { return a - b; }); // 输出 [1, 3, 4] // 从大到小排序 [1, 4, 3].sort(function(a, b) { return b - a; }); // 输出 [4, 3, 1]
1
2
3
4
5
6
7
8
9
10
11
12# 函数作为返回值输出
判断数据类型
我们可能会写如下代码来判断:
var isString = function(obj) { return Object.prototype.toString.call(obj) === '[object String]'; } var isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }
1
2
3
4
5
6
7改造后
var isType = function(type) { return function(obj) { return Object.prototype.toString.call(obj) === '[object ' + type + ']'; } } var isString = isType('String'); var isArray = isType('Array'); console.log(isArray([1, 2, 3])); // true
1
2
3
4
5
6
7
8
9
10甚至我们还可以利用循环来批量注册 isType 函数,代码在上面部分有;
getSingle
下面是一个单例模式的例子,这里只需要大概了解其实现,后续章节会详细介绍
var getSingle = function(fn) { var ret; return function() { return ret || (ret = fn.apply(this, arguments)); } }
1
2
3
4
5
6这个高阶函数的例子,既把参数当做函数传递,又让函数执行后返回了另一个函数,我们可以来看看这个函数运行的结果。
var getUser = getSingle(function(){ return {name: 'jack'}; }); var user1 = getUser(); var user2 = getUser(); console.log(user1 === user2); // true
1
2
3
4
5
6
7
8# 高阶函数实现 AOP
AOP 是面向切面编程,主要作用就是把与核心业务逻辑模块无关的功能抽离开来,例如:日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再使用"动态插入"的方式加入到业务逻辑模块中。主要做的好处是可以保持业务逻辑模块的纯净和高内聚,其次也可以很方便的复用日志统计等模块的功能;
Function.prototype.before = function (beforefn) { var _self = this; // 保存原函数的引用 return function () { // 返回包含了原函数和新函数的"代理"函数 beforefn.apply(this, arguments); // 执行新函数,修正 this return _self.apply(this, arguments); // 执行原函数 }; }; Function.prototype.after = function (afterfn) { var _self = this; return function () { var ret = _self.apply(this, arguments); afterfn.apply(this, arguments); return ret; }; }; var func = function () { console.log(2); }; func = func .before(function () { console.log(1); }) .after(function () { console.log(3); }); func();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30