• 首页
  • javascript
  • 闭包形成环境,使用方法及传递局部作用域时到底是值拷贝还是堆引用

闭包形成环境,使用方法及传递局部作用域时到底是值拷贝还是堆引用


javascript中闭包的作用我们之前专门聊过一次,点击查看


  1. 闭包的形成环境


先从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特性:

  1. 子函数可以访问父函数作用域内的变量

  2. 函数可以当返回值


我们现在遇到的问题是:

父作用域如何访问子作用域内的变量?  上面的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群吧.(自己找入口)


回到顶部