首先需要了解什么是作用域和作用域链?
作用域(scope)
1、什么是作用域?
一个变量的使用范围叫做作用域
2、为什么存在作用域?
为了避免函数内外的变量间互相干扰
3、作用域分为两种
1、全局作用域:保存着所有全局变量/函数
2、函数作用域:保存函数内的局部变量
4、变量的使用顺序
先使用局部变量,当找不到局部变量时,再去全局寻找
5、函数的生命周期**
1、程序执行前
创建一个数组(执行环境栈ESC):用于记录正在执行的函数
浏览器本身也是一个程序,会创建全局变量作用域对象(window),保存所有浏览器内置的对象和方法
2、函数定义时
在全局创建函数名变量
在window之外创建函数对象保存函数定义
函数名变量通过地址引用函数对象
函数对象使用scope属性指回自己来自的作用域
3、函数调用时
现在ECS中添加本次函数调用的记录
为本次函数调用创建专门的函数作用域的对象(AO)
在函数作用域对象中保存本次函数调用所需的所有局部变量
函数作用域对象的parent属性指向函数来自的父级作用域对象
变量的使用顺序:就近原则->先使用局部,如果局部没有再去全局window中找
4、函数调用后
将本次函数调用的记录从ECS中出栈
导致函数作用域对象释放
导致局部变量一同释放
故:局部变量不可重用!
作用域链
由多级作用域主机引用而形成的链式结构
作用:
-1、保存了所有变量
-2、控制了变量的使用顺序:先局部,后全局
1、什么是闭包?
即重用变量,又保护变量不被篡改的一种机制
2、为什么要使用闭包?
全局变量-优点:可以重用。 缺点:随处可用,容易被污染
局部变量-优点:仅函数内可用,不会被污染。缺点:不可以重用
当我们既想重用变量又想保护变量不被污染的情况下,就可以使用闭包。
3、判断闭包的三个特点***
1、用外层函数包裹受保护的变量和内层函数
2、外层函数将内层函数对象返回到外部(共3种途径)
-(1)使用return
-(2)直接给全局变量赋值
-(3)将内层函数包裹在数组/对象中返回
3、使用者调用外层函数,获得内层函数的对象
4、闭包是怎么形成的?
外层函数的函数作用域对象(AO)无法释放
5、闭包的笔试题答题技巧
可以用两步来判断是否是闭包:***
1、找受保护的变量:外层函数的局部变量
2、找外层函数共返回哪些内层函数:一次外层函数的调用,返回的多个内层函数,公用同一个闭包中的受保护的变量。
6、闭包会导致的问题
内层函数比普通函数占用更多的内存空间(外层函数的AO)
如何解决?
一旦闭包不再使用时,应立即释放闭包结构
例:函数 = null
闭包的一些例子
1、使用闭包实现点赞功能
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<button id="btn_zan" οnclick="zan()">赞(+0)</button>
<script>
var zan=(function(){ //使用匿名函数
var i=0; //要保护变量i
return function(){ //使用内部函数返回,可以实现闭包
i++; //让i每点击一次递增
btn_zan.innerHTML="赞(+"+i+")";//输出赞的次数
}
})();
</script>
</body>
</html>
原理:如果单在函数内定义i,例如:
<script>
var zan = function(){
var i =0;
i++;
btn_zan.innerHTML="赞("+i+")";
}
</script>
因为这里的i是局部变量,不可重用,所以i只会被递增一次,故i一直为1
而如果将i定义为全局变量,例如:
<script>
var i =0;
var zan = function(){
i++;
btn_zan.innerHTML="赞("+i+")";
}
</script>
这样的话确实可以实现i的点击累加,但是由于i在这里是全局变量,故容易被污染,如果在函数任意位置重新定义了i,则函数无法
执行,所以这个时候需要使用闭包来实现既能重用又不会被污染。
2、经典笔试题
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
function fun(){
//1. 找受保护的变量: 外层函数的局部变量
var n=999;
//2. 外层函数共抛出哪些内层函数
nAdd=function(){n++};
return function(){ console.log(n) };
}
var get=fun();//fun的AO(n=999)
//var nAdd: function(){n++}
//get: function(){ console.log(n) };
get();//999
nAdd();//AO(n=1000)
get();//1000
</script>
</body>
</html>
这个例子参照判断闭包的三个特点中:外层函数将内层函数对象返回到外部的三种途径中的第二个途径-直接给全局变量赋值
因为nAdd没有被定义过,强行赋值会使JS将nAdd定义为全局变量,所以他也会被返回到外部,故也将执行
所以当先调用一次get()的时候函数作用域对象中的n为999
再调用nAdd()时,由于返回到外部,所以也会对函数作用域对象造成影响,所以这个时候n变成了1000
当再次调用get()时,函数作用域对象中的n变成了1000,所以会输出1000
3、经典笔试题2:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
function fun(){
//1. 找受保护的变量: i=0
for(var i=0,arr=[];i<3;i++){
arr[i]=function(){ console.log(i); }
}//*************i=3******************
//2. 外层函数向外抛出了哪些内层函数: 3个
return arr;
}
var funs=fun();
//funs:arr:[
// function(){ console.log(i); },
// function(){ console.log(i); },
// function(){ console.log(i); }
//]
funs[0]();//3
funs[1]();//3
funs[2]();//3
</script>
</body>
</html>
这个例子中,数组容易迷惑人
这个例子参照判断闭包的三个特点中:外层函数将内层函数对象返回到外部的三种途径中的第二个途径-将内层函数包裹在数组/对象中返回。
所以即便是数组也返回了外部
第二个迷惑点是
arr[i]=function(){ console.log(i); }
这里虽然看起来i都受到for循环的影响,其实不然,因为function()的意思是构造一个函数
在后面function(){console.log(i);}中,只是构造了函数,并没有调用函数,所以function中的i并不受for的影响
所以只有arr[i]中的i会跟着for进行改变,并将function(){console.log(i);}添加到数组中,所以最后数组arr中
会包含三个function(){console.log(i);}
arr[
function(){console.log(i);}, -对应下标0 因为for循环从0开始
function(){console.log(i);}, -对应下标1 到这里i变成1所以下标为1
function(){console.log(i);} -对应下标2 到这里i变成2所以下标为2
]
另外:
for(var i=0,arr[];i<3;i++)
i在循环结束时并不等于2,因为等于二的情况下还是小于3的,只有再进入一次循环,执行一次i++时才算循环结束,
因为当i真正等于3的时候,才真正的不满足循环条件,循环才算真正结束,所以这个时候函数作用域对象中的i=3
未完待续。。。