代码改变世界

HTML5学习笔记(十八):闭包

2017-02-23 13:16  阿诚de窝  阅读(1080)  评论(0编辑  收藏  举报

高阶函数

JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,也可以返回一个函数,这种函数就称之为高阶函数。

函数作为参数

示例如下:

1 function absAdd(x, y, f) {
2     return f(x) + f(y);
3 }
4 console.log(absAdd(-1, 2, Math.abs)); // 3

函数作为参数的好处是我们可以通过修改参数就可以改变函数的行为。

函数作为返回值

示例如下:

 1 function arrSum(arr) {
 2     return function(){
 3         return arr.reduce(function (x, y) {
 4             return x + y;
 5         });
 6     };
 7 }
 8 
 9 var f1 = arrSum([1, 2, 3, 4, 5]);
10 var f2 = arrSum([2, 4, 6, 8, 10]);
11 
12 console.log(f1 === f2); // false
13 
14 console.log(f1()); // 15
15 console.log(f2()); // 30

每次调用arrSum方法返回的都是一个新创建的函数,所以判断是不相等的。

返回函数时,可以决定在何时执行该函数。

闭包

我们注意到上面例子里返回的函数在其定义内部引用了局部变量arr,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,这种情况就称为闭包。

我们看下一个例子:

 1 function foo() {
 2     var r = [];
 3     for (var i = 0; i < 3; i++) {
 4         r[i] = function() {
 5             return i;
 6         };
 7     }
 8     return r;
 9 }
10 
11 var arr = foo();
12 for (var i = 0; i < 3; i++) {
13     console.log( arr[i]() );
14 }
15 
16 // 3
17 // 3
18 // 3

我们希望打印0,1,2这几个数字,但是实际上打印的都是3,这是由于返回的函数保存的是变量i,实际上在循环之后变量i就变成了3,所以会出现这样的情况。

立即执行函数

那么如何才能打印出0,1,2这几个数字呢,这里需要用到立即执行函数,立即执行函数的意思是在定义好函数之后立即执行,一般这样的函数都是匿名函数。

格式如下:

1 (function (x) {
2     return x * x;
3 })(3);

即用一个括号将函数包含,后面紧跟另一个括号进行调用,同时可以进行参数传递。

我们再看下面的例子:

 1 function foo() {
 2     var r = [];
 3     for (var i = 0; i < 3; i++) {
 4         r[i] = (function(index) {
 5             return function() {
 6                 return index;
 7             };
 8         })(i);
 9     }
10     return r;
11 }
12 
13 var arr = foo();
14 for (var i = 0; i < 3; i++) {
15     console.log( arr[i]() );
16 }
17 
18 // 0
19 // 1
20 // 2

我们来看看这个例子,每个闭包函数实际上引用的是index参数,而index参数是在立即执行函数执行时传入的i,所以不存在改变的情况,就可以打印出对应的索引值了。

关于this对象

我们来看下面的例子:

 1 var name = "Window";
 2 
 3 var obj = {
 4     name: "Object",
 5     func: function() {
 6         return function() {
 7             return this.name;
 8         };
 9     }
10 };
11 
12 console.log( obj.func()() ); // Window
13 
14 var f = obj.func();
15 console.log( f() ); // Window

我们发现返回的是全局的name属性,而不是我们期望的obj的name属性。

我们知道每个函数在调用时都会获得this及arguments两个参数,而this参数指向调用该方法的对象。

所以我们可以看一下第14和15行,调用obj.func时this是指向obj对象的,返回的函数实际上被绑定到全局对象上了,所以当调用f函数时,实际上是window进行调用的,所以拿到的name就是window.name。

解决方法如下:

 1 var name = "Window";
 2 
 3 var obj = {
 4     name: "Object",
 5     func: function() {
 6         var that = this;
 7         return function() {
 8             return that.name;
 9         };
10     }
11 };
12 
13 console.log( obj.func()() ); // Object
14 
15 var f = obj.func();
16 console.log( f() ); // Object

利用了闭包会持有调用链上的变量的原理即可。

私有属性

在JavaScript中,没有私有属性的概念,所有属性都是公开的。

但是有私有变量的概念,在函数中声明的变量,都是该函数私有的,函数以外的地方不能访问。

我们利用闭包和私有变量的特性可以创建出类似于私有属性的变量。

 1 function Person(name) {
 2     // 私有变量
 3     var age = 0;
 4     // 私有函数
 5     function foo() {
 6         console.log("call private function!");
 7     }
 8 
 9     this.setName = function(value) {
10         name = value;
11         foo();
12     };
13     this.getName = function() {
14         return name;
15     };
16 
17     this.setAge = function(value) {
18         age = value;
19         foo();
20     };
21     this.getAge = function() {
22         return age;
23     };
24 }
25 
26 var p = new Person("Li Lei");
27 p.age = 28;
28 console.log(p.getAge()); // 0
29 p.setAge(30);
30 console.log(p.getAge()); // 30
31 console.log(p.age); // 28

我们会发现,在函数内部是直接使用age来访问私有变量的,而如果是this.age则表示当前对象的age公开属性,所以p.age和p.getAge会取得不同的数值。外部是无法访问到内部变量age和参数name的。

使用立即执行函数创建

我们发现上面的方法只能将所有代码都写在构造函数中才能访问到私有变量,其实还有一种写法:

 1 (function(){
 2     // 使用 var 定义的变量外部无法访问
 3     var _name;
 4     var age = 0;
 5     // 定义的函数外部无法访问
 6     function foo() {
 7         console.log("call private function!");
 8     }
 9 
10     // 不使用 var 定义的对象外部可访问
11     Person = function(name) {
12         _name = name;
13     }
14 
15     Person.prototype.setName = function(value) {
16         _name = value;
17         foo();
18     }
19     Person.prototype.getName = function() {
20         return _name;
21     }
22 
23     Person.prototype.setAge = function(value) {
24         age = value;
25         foo();
26     }
27     Person.prototype.getAge = function() {
28         return age;
29     }
30 })();
31 
32 var p = new Person("Li Lei");
33 p.age = 28;
34 console.log(p.getAge()); // 0
35 p.setAge(30);
36 console.log(p.getAge()); // 30
37 console.log(p.age); // 28

通过一个立即执行的匿名函数来包裹即可实现。

模块模式

模块模式可以实现对象的私有属性和方法,如下:

 1 var instance = function(){
 2     var name = "Han Meimei";
 3 
 4     function foo(){
 5         console.log("call private function");
 6     }
 7 
 8     return {
 9         setName: function(value) {
10             name = value;
11             foo();
12         },
13         getName: function() {
14             return name;
15         }
16     };
17 }();
18 
19 instance.name = "Li Lei";
20 console.log(instance.getName()); // Han Meimei
21 instance.setName("Uncle Wang");
22 console.log(instance.getName()); // Uncle Wang
23 console.log(instance.name); // Li Lei

当然,如果需要创建指定类型的实例,可以使用下面的代码:

 1 var instance = function(){
 2     var name = "Han Meimei";
 3 
 4     function foo(){
 5         console.log("call private function");
 6     }
 7 
 8     // 这里可以创建指定类型的实例
 9     var obj = new Object();
10 
11     // 添加方法
12     obj.setName = function(value) {
13         name = value;
14         foo();
15     };
16     obj.getName = function() {
17         return name;
18     };
19 
20     return obj;
21 }();
22 
23 instance.name = "Li Lei";
24 console.log(instance.getName()); // Han Meimei
25 instance.setName("Uncle Wang");
26 console.log(instance.getName()); // Uncle Wang
27 console.log(instance.name); // Li Lei