本文结合个人学习及实践,对闭包及相关知识点进行总结记录,欢迎读者提出任何不足之处
一、js变量
二、作用域(scope)
三、[[scope]] 和 scope chain
四、作用域(scope)和关键字(this)
五、闭包实例理解 及 垃圾回收
一、js变量
在 ECMAScript 中,变量可以存在两种类型的值,即原始值和引用值。
原始值:存储在栈区(stack)的简单数据段,变量所在的位置直接存储变量的值。
原始值包括:Undefined、Null、Boolean、Number 和 String 型
引用值:存储在堆区(heap)中的对象,变量所在的位置存的是地址指针(pointer),指向存储对象在内存中的地址
二、作用域(scope)
1.作用域
作用域指代码当前的上下文环境,即代码可以访问和可以被访问的区域。
2.全局作用域(一个页面一般只有一个全局作用域)
<script>
//这里是全局作用域
var myname1="yaoming";
</script>
3.本地作用域/局部作用域
<script>
//这里是全局作用域
var myname1="yaoming";
var myfun=function(){
console.log(myname1); //yaoming
var myname2="liuxiang";
//这里是本地作用域
}
console.log(myname2);
//myname2 is not defined
</script>
myname1打印出yaoming,是因为局部域可以访问全局域中的变量(反之则不行)
myname2未定义,是因为你全局域不能访问局部域内的变量
4.函数域
所有域都只能由函数域所创建
<script>
// 全局作用域
var myFunction = function () {
// 局部作用域1
var myOtherFunction = function () {
//
局部作用域2
};
};
注:for循环、while等循环都不能创建局部作用域
//这里是全局作用域
for(var i=0;i<10;i++){
//code
}
console.log(i); //打印出10
</script>
i 值为10,i 依然属于全局域,是全局变量,所以循环不能创建局部作用域
5.作用域链(scope chain,Scope Chain是一个链表,js中的闭包就是通过作用域链实现的)
<script>
// 全局作用域
var myFunction1 = function () {
// 局部作用域1 (scope1)
var myOtherFunction1 = function () {
//
局部作用域2(scope2)
};
};
var myFunction2 = function () {
// 局部作用域3
(scope3)
var myOtherFunction2 = function () {
//
局部作用域4
(scope4)
};
};
</script>
图解:
上图栈区中的var a 和 var myFun1 在全局区域
fun对应myFunction1
,在scope1中
fun2对应myOtherFunction1
,在scope2中
myOtherFunction1
中形成的作用域链:scope1--》scope2--》全局
作用域
说明:a.scope1
scope1
scope1 中可以访问scope2
和全局作用域中的任何变量
b.如果 scope2中使用了 i 变量但是没有定义 i 变量,那么它会往其上级作用域寻找 i 变量直到找到为止,找不到为null。
上图:fun2 中使用了变量 i
第一步:在scope2 中寻找 i 变量,找到则停止,否则进入下一步
第二步:在上一级域 scope1中继续寻找 i 变量,同理找到停止,否则下一步
第三步:在scope1的上级作用域 全局作用域中 寻找 i 变量 ,找到停止,否则变量未定义
myOtherFunction2
中形成的作用域链:
--》scopescope4
--》3
全局作用域 (此处同上)
6.闭包(closure)/词法作用域/静态作用域
当B函数嵌套在A函数内,B中引用了A作用域中的变量,
并且B在A的外部调用了B函数,B函数是闭包函数。
<script>
//scope global
function a(){
var myname="liuxiang";
return function b(){
console.log(myname);
//此作用域访问上级作用域的 myname变量
}
}
var c=a(); //c就是函数b ,在b的外层函数a之外被使用,那么此时就形成了闭包
c(); //此处打印出 liuxiang
console.log(this);
</script>
闭包函数会拥有许多变量和绑定了这些变量的环境的表达式
console.log(this);打印出window对象,从全局对象window中找到 变量c并展开如下:
由上图可以看到,全局变量 c 指向了内部函数 b
函数b的作用域scope=A0+[[scopes]],其中[[scopes]]为函数b的属性,此属性包含了一个与其形成闭包的函数a的Closure(a)作用域,和一个Global全局作用域
Closure(a)中包含所有 b函数中使用到的变量
Gloal 包含所有的全局变量
三、 VO/AO, scope chain , [[scope]] 和 scope
JS 代码的执行
在js代码执行之前,js引擎会在全局域创建一个 VO (变量对象) ,在每一个函数中的局部域创建一个 AO (活动对象)
VO 指向属于全局域的所有对象,进入js代码块的时候即被创建
AO 活动对象是进入函数上下文时被创建的,指向局部作用域的所有对象
作用域链
作用域链正是内部上下文 所有变量对象 和 所有父变量对象 的列表。
仍然以 二、作用域 中的第 5 点 中的代码为例
myOtherFunction1
上下文的作用域链 为 AO(myOtherFunction1
) AO(myFunction1
) 和 VO(global)
[[scope]]
[[scope]]属性是在当前函数被定义时确定,[[scope]]属性是所有父变量的层级链,位于当前的 AO 上下文 之上。
函数之所有能访问 上级作用域中的对象,就是[[scope]]属性来实现的。
scope的定义:
scope=AO+[[scope]]; 当前的 AO 是作用域 数组的第一个对象,即从当前活动对象 往上级查找 ,则局部变量 比 父级变量 有更高的优先级。
以 二、作用域 中的第 5 点 中的代码为例
myOtherFunction1
Context.Scope= myOtherFunction1
Context.AO + myOtherFunction1
.[[Scope]]
= myOtherFunction1
Context.AO + myFunction1
Context.AO + myFunction1
.[[Scope]]
= myOtherFunction1
Context.AO + myFunction1
Context.AO + globalContext.VO
myOtherFunction1
的scope数组是
.Scope= [ myOtherFunction1
ContextmyOtherFunction1
Context.AO, myFunction1
Context.AO, globalContext.VO ];
四、作用域(scope)和关键字(this)
五、闭包实例理解 及 垃圾回收
现有数组b,b中包含三个人的姓名和年龄信息。现在通过调用sayHello 方法,分别为每一个对象添加一个说出自己名字的方法。
代码实现如下:
<script>
var b=[
{"name":"yaoming",age:40},
{"name":"liuxiang",age:38},
{"name":"lining",age:50}
];
function addSayHello(){
for(var j=0;j<b.length;j++){
b[j].sayHello=function(){
return "hello,i am "+b[j].name;
};
}
--j;
}
addSayHello();
console.log(b[0].name+"说:"+b[0].sayHello());
console.log(b[1].name+"说:"+b[1].sayHello());
console.log(b[2].name+"说:"+b[2].sayHello());
</script>
控制台输入如下:
发现 每个人的sayHello 都会打印出 我是 lining,这并不是我们想要的结果。
分析:
addSayHello 执行完毕之后,全局作用域 里 b数组中的每个成员都有了自己的sayHello方法,
该方法的表达式:function(){return "hello,i am "+b[j].name;};
当调用b[0].sayHello()时,sayHello.AO中无 j 变量的定义,那么下一步,会到 addSayHello.AO 中寻找 j 变量,此时找到了 j 变量,此时 j 变量的值为2
因此 b[0].sayHello()会返回 "hello,i am "+b[2].name;.同理所有人调用sayHello方法都会返回"hello,i am "+b[2].name;
正确的方法:
<script>
var b=[
{"name":"yaoming",age:40},
{"name":"liuxiang",age:38},
{"name":"lining",age:50}
];
function addSayHello(){
for(var j=0;j<b.length;j++){
b[j].sayHello=(function(index){
return function(){
return "hello,i am "+b[index].name;
}
})(j);
}
--j;
}
addSayHello();
console.log(b[0].name+"说:"+b[0].sayHello());
console.log(b[1].name+"说:"+b[1].sayHello());
console.log(b[2].name+"说:"+b[2].sayHello());
</script>
正确的控制台运行结果:
思路:错误的方法中,sayHello 方法中找不到变量j,上级addSayHello.AO 中只保存其作用域中的变量 j(保存为循环执行后j的最终值),造成所有方法访问同一变量。
如果我们能让每个 sayHello 方法在 sayHello.AO 中保存自己所需的变量,就解决了刚刚的问题。在上面正确的方法中,每次循环都把当前 j 对应的变量以参数的形式传给内层函数,在内层函数的作用域中保存当前的值即可。
总结:
在错误的方法中,内层sayhello() 函数和 外层函数 addSayHello 形成了闭包,内层变量 j 永远都指向 外层函数 addSayHello 中的 j变量。
如下图,每一个对象的函数作用域中 都指向 j=2
改进的方法中,sayhello() 函数 为一个自执行函数返回的一个匿名函数。 sayHello对应的匿名函数 和 其外层的自执行函数 形成闭包,这里 sayHello对应的匿名函数中的 index变量 会指向 其自身的 外层自执行函数,其中每个自执行函数的AO里都分别保存了运行时 j 变量的副本 index。
如下图,每一个对象的函数作用域中 都指向 其自身对应的下标:index:index
JS的垃圾回收机制:
找到那些不被使用的变量,然后释放其所占用的内存
?问:为什么闭包中用到的变量会保存在内存中?
因为 全局变量 保存了对内部函数的引用。所以, 内部函数,及其所绑定的上下文环境均被使用,因此变量不会被释放,而是保存在内存中。