闭包形成环境,使用方法及传递局部作用域时到底是值拷贝还是堆引用
javascript中闭包的作用我们之前专门聊过一次,点击查看
闭包的形成环境
先从javascript的简史说起:
javascript 这门充满缺陷和坑的语言,在各大浏览器厂商的支持下,web大势所趋的形势下发展成一家独大的浏览器宿主语言,非常不容易,有兴趣的可以查阅下它的发展背景,当初创建javascript的作者Brendan Eich 在网景公司高层压力下,10天内写成的这门语言的第一个初版,他自己感觉非常不满意,用他自己的话说是这样的:
"与其说我爱Javascript,不如说我恨它。它是C语言和Self语言一夜情的产物。十八世纪英国文学家约翰逊博士说得好:‘它的优秀之处并非原创,它的原创之处并不优秀。’(the part that is good is not original, and the part that is original is not good.)”
Brendan Eich 对java并不感冒,甚至讨厌,所以javascript创作的原型绝非java ,而且可以说和java半点关系没有,他的名字叫 "Mocha" ,首先出现在网景浏览器中,到1995年改名为LiveScript ,直到网景公司和sun公司有了合作,达成一致协议,LaveScript 改名 JavaScript 让这2个八竿子打不着的2门语言貌似有了近亲关系,其实是公司层面的一种宣传,当时sun公司的java语言如日中天,火的一塌糊涂,连这门脚本语言也想去抱java 的大腿.
因为种种缺陷:
其中一个就是没有包,或者叫命名空间,所以javascript中的引用竟然是直接加载外包javascript文件.不能像java引入包,或者像c#引入命名空间,现在为了弥补这个权限,所以commjs 给出了一整套规范,其中包括javascript的引用.
因为上面的权限导致变量混乱,同名变量覆盖等等,所以javascript中尽量少用全局变量/或者轻易修改一个全局变量.
如何来实现,javascript还好有作用域的概念.如下代码
function f(){ var p_f=1; function c(){ var p_c=2; console.log(p_f,p_c); //1,2 } console.log(p_f,p_c);//1,undefined }
上面2个嵌套的函数,我们可以看做是作用域的嵌套.
f 是 c 的父作用域,在c 作用域内可以访问 f 作用域内的所有变量. 但是在 f (父作用域) 无法访问 c (子作用域)内的变量.
在C#中这些变量都应该是私有变量,顾名思义,它不对外产生访问权限.但是类似C#这样的高级语言都有属性,其实属性的本质就是方法(函数)
只不过这个方法是public 的,而且直接操作命名空间里的私有变量(get 传值, set 赋值)
javascript 的实现没有这么属性这么一说,但是它作为一门大部分借鉴Scheme语言的脚本语言,它继承了Scheme语言的精髓之一,函数是一等公民.(定义,赋值,传参,返回值)
可见javascript中的函数可以灵活的当做参数,或者返回值来使用.
通过上面罗列的javascript特性:
子函数可以访问父函数作用域内的变量
函数可以当返回值
我们现在遇到的问题是:
父作用域如何访问子作用域内的变量? 上面的2个特性正好帮了忙.我们需要在子作用域内定义一个方法,方法内访问父作用域的变量,最后返回这个方法即可. ----- 这就是闭包.
终于回到正题了,所有所有的铺垫只是想让大家了解为什么会有闭包,什么时候用闭包,闭包干什么用.
2.闭包的使用方法
简单讲就是:
闭包的使用就是,方法里面返回一个方法.
为什么使用闭包,改变函数内变量的作用域.
所以javascript中可以通过闭包轻松访问到私有作用域内的变量.
3.闭包在传递变量时到底是做了值拷贝,还是堆引用
我们看下如下循环
for(var i=0;i<10;i++) { setTimeout(function() { console.log(i); }, 300); }
猜猜打印出来会是什么结果呢?
连续输出了10个10.
setTimeout函数会在javascript的事件线程中的等待队列中一直等待,直到300毫秒后才触发执行函数.而此时10次循环早已执行完成,栈变量i也已经被赋值为10,300毫秒以后的10次输出中,i===10 .所以我们看到输出了10个10.
如何控制一下,输出0,1,2......9
我们想到了闭包,for循环看做一个父作用域, setTimeout 所在的函数看做子作用域,然后通过必报的形式,把父作用域变量i 传递出去.所以我们把代码做了如下修改:
for(var i=0;i<10;i++) { setTimeout((function(p) { return function() { console.log(p); } })(i), 300); }
父作用域没有太多变化,作用域变量i在10次循环中做自增操作.
子作用域有些变化, setTimeout 执行函数由之前的直接输出参数,变成了一个匿名自启动函数,接收一个参数p ,同时匿名自启动函数里面返回了一个匿名函数. 简单粗暴的理解为: 函数内返回函数,这一定是闭包的使用场景.
匿名自启动函数接收一个参数变量,300毫秒后自启动函数执行,i 变量传给了 p ,然后闭包把私有作用域变量传出去. 我们先看下结果:
输出完全符合我们意愿,难道这次 setTimeout 在等待队列的时候,for循环也在等待吗? 不会的,for 循环会不顾一切的执行10遍,然后哪凉快去哪呆着.他不会顾及后面的 setTimeout .
之所以每次循环的自增变量都在300毫秒后一一输出,功劳就在 闭包.
每次循环 i 变量自增1以后,都会去执行 setTimeout ,虽然执行函数会等待300毫秒,但是这时 i 变量会做一份拷贝传给匿名自启动函数,直到 300 毫秒以后,匿名自启动函数执行时,p 参数接收到的参数其实已经不是 i 了,而是 i 的一份拷贝.
那么我们回头再来看这个参数拷贝,不难想到,300毫秒后的执行,i 的值早已是 10 ,但是等待队列中的函数用各自的拷贝值运行了闭包,这传递的一定的值拷贝,也就是说每个自启动函数在接收 i 变量的时候,同时在栈上开辟一个新空间,新空间所放的值就是 i 的当前值的拷贝.
所以我们可以说: 闭包在变量传递时使用了值拷贝
下面我们再来验证下闭包对于对象传递时是不是也是值拷贝?
这次的操作的数据类型要换成对象了,我们代码做如下修改:
var obj={num:0}; for(var i=0;i<10;i++) { obj.num++; setTimeout((function(p) { return function() { console.log(p.num); } })(obj), 300); }
可以看到代码里多了一个obj 对象的定义,num属性,接收int型值,此对象可以看做全局变量,或者父作用域局部变量都可以.
然后匿名自启动函数中接收的参数直接是 obj 对象. 闭包里输出的是 p.num 属性.
我们来看下输出结果:
这个输出结果和上面使用闭包的输出结果大行径庭.......你是不是又不相信爱情了,哈哈^_^
这不是闭包的错,闭包依然是把私有作用域内的私有变量做了公开化.
而真正导致此结果的原因是参数类型,我们这里传的是对象 obj .
obj 是引用类型,栈上面存储空间保存的是它在堆上面的开辟空间的地址,而堆上面才真正保存了对象的内容.
所以闭包在每次调用时还是会尽职尽责的在栈上做一份拷贝,注意:他此时拷贝的其实是obj 的引用地址.而不论你做多少次地址的拷贝,都没有改变obj 堆上的空间.所以最后的结果就是栈上的 10 个地址同时执行了 堆上的一个引用.
所以最后输出了 10 个 10.
所以我们可以说:闭包在变量传递时使用了堆引用
喔! 和上面的结论有冲突,更准确点的说法是:
当局部变量是值类型时,闭包在变量传递时使用了值拷贝.
当局部变量是引用类型时,闭包在变量传递时使用了堆引用.
如果有疑问,你可以在最后一次实验中,把传递变量由obj 改成 obj.num 试一试 , 闭包的输出一定是 1,2,3.....10
var obj={num:0}; for(var i=0;i<10;i++) { ++obj.num; setTimeout((function(p) { return function() { console.log(p); } })(obj.num), 300); }
因为 obj.num 是一个值类型参数,所以闭包在栈中开辟的新空间保存的是 obj.num 的当前值.
End ,祝大家周末愉快,上面的演示工具是 mac 下的小工具 JS Runner ,收费工具,鼓励正版,但是你有很多办法得到它.进我的QQ群吧.(自己找入口)