JavaScript中的闭包与匿名函数
知识内容:
1.预备知识 - 函数表达式
2.匿名函数
3.闭包
一、函数表达式
1.定义函数的两种方式
函数声明:
1 function func(arg0, arg1, arg2){ 2 // 函数体 3 }
函数表达式:
1 var func = function (arg0, arg1, arg2){ 2 // 函数体 3 }
2.注意事项
函数表达式使用前必须赋值!像下面的代码是错误的:
1 say() 2 var say = function(){ 3 console.log("123") 4 }
函数表达式可以作为一个普通变量在分支中根据条件来赋值:
1 // 下面的代码将根据不同的condition 将不同的函数表达式赋给sayHi 2 var sayHi 3 var condition = true // Hi! 4 // var condition = false // Hello! 5 6 if(condition){ 7 sayHi = function(){ 8 console.log("Hi!") 9 } 10 } 11 else{ 12 sayHi = function(){ 13 console.log("Hello!") 14 } 15 } 16 17 sayHi()
上述代码不同的运行效果:
能创建函数赋值给变量,当然也可以把函数作为其他函数的返回值返回
二、匿名函数
1.什么是匿名函数
一般用到匿名函数的时候都是立即执行的。通常叫做自执行匿名函数或者自调用匿名函数。常用来构建沙箱模式,作用是开辟封闭的变量作用域环境,避免全局变量的污染,在多人联合工作中,合并js代码后,不会出现相同变量互相冲突的问题
2.匿名函数的详细写法
匿名函数的详细写法有以下两种,推荐使用第一种:
1 (function(){ 2 console.log("我是匿名方式1"); 3 })(); // 我是匿名方式1 4 5 (function(){ 6 console.log("我是匿名方式2"); 7 }()); // 我是匿名方式2 8 9 console.log((function(){}).name); // name为空
注:实际上,立即执行的匿名函数并不是函数,因为已经执行过了,所以它是一个结果,这个结果是对当前这个匿名函数执行结果的一个引用(函数执行默认return undefined)。这个结果可以是一个字符串、数字或者null/false/true,也可以是对象、数组或者一个函数(对象和数组都可以包含函数),当返回的结果包含函数时,这个立即执行的匿名函数所返回的结果就是典型的闭包了
3.匿名函数的作用
- 函数表达式可以存储在变量中,并且可以赋值,可以作为其他函数的参数
- 可以通过匿名函数执行某些一次性的任务
- 避免全局变量的污染以及函数名的冲突
三、闭包
1.什么是闭包
什么是闭包:闭包指有权访问另一个函数作用域中的变量的函数,官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分
- 作为一个函数变量的一个引用,当函数返回时,其处于激活状态
- 一个闭包就是当一个函数返回时,一个没有释放资源的栈区
简单说Javascript允许使用内部函数---即函数定义和函数表达式位于另一个函数的函数体内。而且这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包
2.闭包的写法
1
2
3
4
5
6
7
8
9
10
|
function a(){ var n = 0; function inc() { n++; console.log(n); } inc(); inc(); } a(); //控制台输出1,再输出2 |
简单吧。再来看一段代码:
1
2
3
4
5
6
7
8
9
10
|
function a(){ var n = 0; this .inc = function () { n++; console.log(n); }; } var c = new a(); c.inc(); //控制台输出1 c.inc(); //控制台输出2 |
简单吧。
什么是闭包?这就是闭包!
有权访问另一个函数作用域内变量的函数都是闭包。这里 inc 函数访问了构造函数 a 里面的变量 n,所以形成了一个闭包。
再来看一段代码:
1
2
3
4
5
6
7
8
9
10
11
|
function a(){ var n = 0; function inc(){ n++; console.log(n); } return inc; } var c = a(); c(); //控制台输出1 c(); //控制台输出2 |
看看是怎么执行的:
var c = couter(),这一句 couter()返回的是函数 inc,那这句等同于 var c = inc;
c(),这一句等同于 inc(); 注意,函数名只是一个标识(指向函数的指针),而()才是执行函数。
后面三句翻译过来就是: var c = inc; inc(); inc();,跟第一段代码有区别吗? 没有
这样写的原因:
我们知道,js的每个函数都是一个个小黑屋,它可以获取外界信息,但是外界却无法直接看到里面的内容。将变量 n 放进小黑屋里,除了 inc 函数之外,没有其他办法能接触到变量 n,而且在函数 a 外定义同名的变量 n 也是互不影响的,这就是所谓的增强“封装性”。
而之所以要用 return 返回函数标识 inc,是因为在 a 函数外部无法直接调用 inc 函数,所以 return inc 与外部联系起来,代码 2 中的 this 也是将 inc 与外部联系起来而已
3.闭包的作用
- 实现匿名自执行函数
- 实现封装
- 实现类和继承
- 将变量的值一直保存在内存中
(1)闭包实现匿名自执行函数见上面匿名函数中的详细介绍
(2)闭包实现封装
JavaScript中没有私有成员的概念,所有对象属性都是公有的,不过却有一个私有变量的概念。简单说任何定义在函数中的变量都可以认为是私有变量,因为不能在函数的外部访问这些变量
私有变量包括:
- 函数的参数
- 函数的局部变量
- 在函数内部定义的其他函数
另外利用闭包原理,我们可以创建用于访问私有变量的公有方法:在函数内部创建一个闭包,闭包通过作用域链访问这些变量,从而实现外部无法直接访问内部变量只能通过某些方法来访问,这样就实现了封装
1 var person = function(){ 2 // 变量作用域为函数内部,外部无法访问 3 var name = "default"; 4 5 return { 6 getName : function(){ 7 return name; 8 }, 9 setName : function(newName){ 10 name = newName; 11 } 12 } 13 }(); 14 15 print(person.name); // 直接访问,结果为undefined 16 print(person.getName()); 17 person.setName("wyb"); 18 print(person.getName()); 19
(3)闭包实现类和继承
1 function Person(){ 2 var name = "default"; 3 4 return { 5 getName : function(){ 6 return name; 7 }, 8 setName : function(newName){ 9 name = newName; 10 } 11 } 12 }; 13 14 var p = new Person(); 15 p.setName("Tom"); 16 alert(p.getName()); 17 18 var s= function(){}; 19 // 继承自Person 20 s.prototype = new Person(); 21 // 添加私有方法 22 s.prototype.Say = function(){ 23 alert("Hello,my name is s"); 24 }; 25 var j = new s(); 26 j.setName("s"); 27 j.Say(); 28 alert(j.getName());
(4)将变量的值一直保存在内存中
1 // 闭包将变量的值一直保存在内存中 2 var f1 = function () { 3 var n = 1; 4 test = function () { 5 n += 1; 6 }; 7 var f2 = function () { 8 alert(n); 9 }; 10 return f2; 11 }; 12 13 var res = f1(); 14 alert(res()); // 1 undefined 15 test(); 16 alert(res()); // 2 undefined
闭包的应用:
1 // 上述闭包应用 - 迭代器 2 var f = function (x) { 3 var i = 0; 4 return function () { 5 return x[i++]; 6 } 7 }; 8 var next = f([11, 22, 33, 44]); 9 alert(next()); // 11 10 alert(next()); // 22 11 alert(next()); // 33 12 alert(next()); // 44
4.闭包注意事项
(1)闭包与变量
闭包只能取得包含函数中任何变量的最后一个值,闭包所保存的是整个变量对象,而不是某个特殊的变量
1 function createFunctions(){ 2 var result = new Array() 3 4 for (var i=0; i < 10; i++){ 5 result[i] = function(){ 6 return i; 7 } 8 } 9 10 return result 11 } 12 13 var foo = createFunction()
结果foo中并不是预料中的那样,而是10,这是为什么呢?仅仅声明某一个函数,引擎并不会对函数内部的任何变量进行查找或赋值操作。只会对函数内部的语法错误进行检查(如果往内部函数加上非法语句,那么不用调用也会报错),也就是说在返回result之前
result并未执行,返回之后再执行时匿名函数中的所有i引用的都是同一个变量(最后一个值10),故每个函数内部i的值为10
详细说明看这一篇文章:https://www.cnblogs.com/kindofblue/p/4907847.html
通过创建另一个匿名函数强制让闭包的行为符合预期:
1 function createFunctions(){ 2 var result = new Array() 3 4 for (var i=0; i < 10; i++){ 5 result[i] = function(num){ 6 return function(){ 7 return num 8 } 9 }(i) 10 } 11 12 return result 13 }
(2)闭包中的this对象
关于this对象:
- this对象是在运行时基于函数的执行环境绑定的
- 在全局函数中this等价于window,当函数被作为某个对象的方法调用时this等价于那个对象
- 在闭包中使用this对象可能会导致一些问题
1 var name = "The Window"; 2 var object = { 3 name: "My object", 4 getNameFunc: function() { 5 return function() { 6 return this.name; 7 }; 8 } 9 } 10 alert(object.getNameFunc()()); // "The Window"
为什么最后的结果是"The Window"而不是object里面的name"My object"呢?
首先,要理解函数作为函数调用和函数作为方法调用,我们把最后的一句拆成两个步骤执行:
var first = object.getNameFunc(); var second = first();
其中第一步,获得的first为返回的匿名函数,此时的getNameFunc()作为object的方法调用,如果在getNameFunc()中使用this,此时的this指向的是object对象。
第二步,调用first函数,可以很清楚的发现,此时调用first函数,first函数没有在对象中调用,因此是作为函数调用的,是在全局作用域下,因此first函数中的this指向的是window。
再看下面这句话:
为什么匿名函数没有取得其包含作用域(外部作用域)的this对象呢?
每个函数被调用时,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。 《Javascript高级程序设计》
那么,如何获得外部作用域中的this呢?可以把外部作用域中的this保存在闭包可以访问到的变量里。如下:
1 var name = "The Window"; 2 var object = { 3 name: "My object", 4 getNameFunc: function() { 5 var that = this; // 将getNameFunc()的this保存在that变量中 6 var age = 15; 7 return function() { 8 return that.name; 9 }; 10 } 11 } 12 alert(object.getNameFunc()()); // "My object"
(3)模仿块级作用域
在JavaScript中没有块级作用域,JavaScript从来不会告诉你是否多次同时声明同一个变量,遇到这种情况它只会对后续的声明视而不见,不过我们可以使用匿名函数和闭包来实现块级作用域
用作块级作用域的匿名函数如下:
1 (function(){ 2 // 这里是块级作用域 3 }) ();
上述匿名函数也等价于这种形式:
1 var someFunction = function(){ 2 // 这里是块级作用域 3 } 4 someFunction()
关于匿名函数及模仿块级作用域的注意事项:
- 在匿名函数中定义的任何变量都会在执行结束时被销毁
- 在私有作用域(匿名函数)中可以访问外层函数的变量是因为这个匿名函数是一个闭包,它能够访问包含作用域中的所有变量
关于以上两点,示例代码如下:
1 function func(count){ 2 (function (){ 3 for(var i=0; i < count; i++){ 4 console.log(i) 5 } 6 })(); 7 8 console.log(i) // 导致一个错误! 9 }
(4)闭包保存的是函数的作用域而不是变量本身的值
1 // 闭包保存的是函数的作用域而不是变量本身的值 2 var f = function (param) { 3 var n = function () { 4 return param; 5 }; 6 param++; 7 return n; 8 }; 9 var test = f(456); 10 console.log(test()); // 457
注:通过闭包可以实现将变量一直保存在内存中
(5)闭包会使得函数中的变量都保存在内存中,内存消耗很大,不能滥用闭包,否则会造成网页的性能问题,在IE中可能会导致内存泄露,尽量在退出函数之前,将不使用的局部变量全部删除
1 // 闭包将变量的值一直保存在内存中 2 var f1 = function () { 3 var n = 1; 4 test = function () { 5 n += 1; 6 }; 7 var f2 = function () { 8 alert(n); 9 }; 10 return f2; 11 }; 12 13 var res = f1(); 14 alert(res()); // 1 undefined 15 test(); 16 alert(res()); // 2 undefined