JavaScript之深入函数(二)
上一篇我们主要讲解了函数的执行过程和原理,本篇我们将介绍函数的另外两个特殊表现:闭包和立即执行函数。
一 闭包
1, 闭包的形成
之前我们提到,函数执行完毕,马上就会销毁自己的ao对象。但是如果遇到下面这种情况:有子函数的定义,并将子函数返回。它真的就完全销毁了自己的ao对象吗?
1 function fn(){ 2 var a = 1; 3 function son(){ 4 console.log(a); 5 } 6 return son; 7 } 8 var test = fn(); 9 test();//error ? 1
这将打印什么呢?表面上看,test是son的另一个引用,son内并没有变量的声明,consol.log()访问a应该抛出错误。
但事实上,test()将打印1,这是为什么呢?回忆上一篇文章函数的作用域链,不难发现:
当fn执行时:fn.[[scope]] --- {0:ao(fn),1:go};son被声明:son.[[scope]] --- {0:ao(fn),1:go};
renturn son也将保留该属性,这时fn已经执行完毕:
fn.[[scope]] --- {0:go}(ao(fn)被销毁?);
直到test()执行时,test.[[csope]] --- {0:ao(son),1:ao(fn),2:go}( test是son的另一个引用,实际上他们是同一个函数)。
这时test想要访问变量a,那么他将先在自己的ao内查找,没有,那么他将到fn的ao里去查找,刚好有,所以最终打印的是1。
这里被看似已经被fn销毁的ao(fn),实际上还被son引用着,所以它并没有真正的被完全销毁,只是对于fn来说,已经丢弃了对这个对象的引用,看起来像被销毁了。这个还被son保留着的ao对象我们即称之为闭包。闭包能帮助一个函数读取另一个函数内部的变量,它起到了连接两个函数的桥梁作用。
总结一下,在javascripe中形成闭包需要三个要素:
1, 父函数内定义了子函数。
2, 子函数内访问了父函数的变量。
3, 子函数被返回。
2,闭包的应用
a) 变量私有化,但可以实现全局变量的效果
1 function add(){ 2 var count = 0; 3 return function (){ 4 count ++; 5 //some code 6 console.log(count); 7 } 8 } 9 var myadd = add(); 10 myadd();//1 11 myadd();//2 12 myadd();//3
b) 用作(类似)缓存
1 function person(){ 2 var money = 0; 3 var obj = { 4 pay:function (){ 5 if(money > 0){ 6 console.log("i spent one yuan."); 7 money --; 8 }else{ 9 console.log("i run out of my money."); 10 } 11 }, 12 make:function (){ 13 console.log("i made one yuan."); 14 money ++; 15 } 16 }; 17 return obj; 18 } 19 var person1 = person(); 20 person1.pay();//"i run out of my money." 21 person1.make();//"i made one yuan." 22 person1.pay();//"i spent one yuan."
c) 模块化开发,防止变量污染
1 var a = "global"; 2 function p0(){ 3 console.log(a); 4 } 5 function p1(){ 6 var a = "p1"; 7 return function(){ 8 console.log(a); 9 }; 10 } 11 function p2(){ 12 var a = "p2"; 13 return function(){ 14 console.log(a); 15 }; 16 } 17 var myp1 = p1(); 18 var myp2 = p2(); 19 20 p0();//"global" 21 myp1();//"p1" 22 myp2();//"p2"
大型项目一般都是多人协同开发,每个人负责不同的模块,不可避免的,大家可能使用了相同的变量名,这将造成全局变量污染。使用闭包,即可解决这个问题题。了解了下一节的立即执行函数,这段代码还可以加以优化。
二 立即执行函数
在认识立即执行函数之前,让我们先来了解执行符()的两个特点。
1)只有表达式才能被()执行。
1 function test(){ 2 console.log(1); 3 }();//error 这是函数声明 4 var test = function (){ 5 console.log(1); 6 }();//1 这是函数表达式
2)能被()执行的表达式会被系统忽略函数名称。
1 var test = function (){ 2 console.log(1); 3 }();//1 4 console.log(test);//undefined 5 //这是一个有趣的现象:我们声明了变量test,并把一个函数赋值给它,紧接着使用()执行了这个表达式,随即打印出了1。
按理说,这时test的值应该是一个匿名函数的函数体才对,但实际上它是undefined,变量刚被声明的状态,即系统放弃了变量test对函数的引用。
这很好的印证了()执行符的第二个特点。
1,立即执行函数的形式
我们知道"()"括号实际上也是一种数学运算符,表示运算优先级的。那么我们当然可以把函数声明用括号包起来,使它成为一个表达式。这样我们就可以使用()执行符马上执行它并得到函数执行的结果了。
1 (function test(){ 2 console.log(1); 3 }());//1 4 //集合()执行符的第二个特点,我们还可以将它优化 5 (function (){ 6 console.log(1); 7 });//1
以上就是立即执行函数的最终形式。另外,把()执行符放在函数声明的括号外面其实也是可以的。
1 (function (){ 2 console.log(1); 3 })();
2,立即执行函数的特点
知道了()执行符的特点,其实我们不难发现:
1)立即执行函数被声明后会马上执行函数体内的代码。
2)执行完毕后立即销毁,不会被一直保存在内存中。
3)只能被执行一次,不能起到代码块复用的功能。
除了上述特点外,立即执行函数和普通函数的功能完全一样。
3,立即执行函数结合闭包的经典应用
1 function fn(){ 2 var arr = []; 3 for(var i = 0; i < 10; i++){ 4 arr[i] = function () { 5 console.log(i); 6 }; 7 } 8 return arr; 9 } 10 var myarr = fn(); 11 myarr.map(function (item){ 12 item(); 13 });//10 10 10 10 10 10 10 10 10 10
我们是想依次输出1--9啊!为什么跑出来10个10呢?
仔细想一想,不难发现,这是因为所有子函数和fn形成的是同一个闭包,所以最后都打印了10,那么要怎样才能实现我们想要的功能呢?
1 function fn(){ 2 var arr = []; 3 for(var i = 0; i < 10; i++){ 4 (function (j){ 5 arr[j] = function () { 6 console.log(j); 7 } 8 }(i)); 9 } 10 return arr; 11 } 12 var myarr = fn(); 13 myarr.map(function (item){ 14 item(); 15 });//0 1 2 3 4 5 6 7 8 9
通过利用立即执行函数定义完即被执行的特点,使每个子函数都和fn形成单独的闭包,再把每次循环的i的值当做它的实参传递进去,那么最终子函数在执行时访问到的其实都是各自闭包里的i的值了。这样就得到了我们想要的结果了。
虽然闭包能在很多地方发挥很大作用,但闭包也有它自身的缺陷:闭包将一直占用内存空间,严重时将导致内存泄漏,甚至系统崩溃。所以我们应该尽量避免使用闭包,如果别无他法,也应该在使用完后手动的解除它对内存的占用,比如把引用返回函数的变量赋值为null。
推荐阅读
-
JavaScript学习笔记(二)——函数和数组
-
深入理解PHP内核(二)之SAPI探究,深入理解sapi_PHP教程
-
你必须知道的Javascript知识点之"深入理解作用域链"的介绍_javascript技巧
-
深入解析JavaScript中的立即执行函数
-
javascript之DOM技术(二) JavaScriptIECSS搜索引擎HTML
-
JavaScript深入浅出第1课:箭头函数中的this究竟是什么鬼?
-
前端笔记知识点整合之JavaScript(二)
-
Javascript学习笔记二 之 变量_基础知识
-
JavaScript实用库:Lodash源码数组函数解析(二)
-
深入浅出Python——Python高级语法之函数