作用域、作用域链和闭包那些事
之所以写这篇文章,是跟我经历有关,前两天面试碰到一个很无理的面试官,年纪不大,电话面试,说话傲气,自称是写react和ts的。问道了闭包的概念,然后我回答闭包是前端比较容易混淆的概念,而且他的概念很多,阮一峰的定义是:。。。然后就让我跳过去了。。。
额。。。闭包概念多不多,下面我将进行逐次举例:
一、概念:先从带我入门的阮一峰大神开始吧:
1,阮一峰:我的理解是,闭包就是能够读取其他函数内部变量的函数。
2,廖雪峰:
image廖大神没有给明定义而是举了一个例子,简单概括来说:
即在外部函数内定义内部函数,当把内部函数返回时相关的参数和变量都会被保存在返回函数中。
3,MDN:闭包是函数和声明该函数的词法环境的组合。
4,红宝书(JS高级程序设计):有权访问另一个函数作用域中变量的函数。
5,某简书作者(思路非常有意思):
闭包是一种特殊的对象。
它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。
当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。
在大多数理解中,包括许多著名的书籍,文章里都以函数B的名字代指这里生成的闭包。而在chrome中,则以执行上下文A的函数名代指闭包。
综上所述:概念真的很多。。。
但是概念多并不妨碍我们去理解闭包,先从作用域、作用域链开始解释吧:
二、作用域(scope):
变量作用域分为两种:全局变量和局部变量(函数作用域)
这两个概念前端都知道我不做多余解释。
三、作用域链(chain scope):
这里面涉及的概念有点多了:
1>基础数据类型与引用数据类型
2>内存空间
3>垃圾回收机制
4>执行上下文
5>变量对象与活动对象
但是可以通过一张图轻松看明白:
image作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
简单来说就是:
当前执行的代码所在环境的变量对象(如果该环境是函数,则将其活动对象作为变量对象),下一个变量对象来自包含环境(包含当前执行环境的环境),下一个变量对象来自包含环境的环境,依次往上,直到全局执行环境的变量对象。全局执行环境的变量对象始终是作用域链中的最后一个对象。
标识符解析是沿着作用域一级一级的向上搜索标识符的过程。搜索过程始终是从作用域的前端逐地向后回溯,直到找到标识符(找不到,就会导致错误发生)。
作用域链决定了全局变量和局部变量(函数变量)的有限访问权。
看图说话:
image这里还要引出另外两个概念:
四、提升:
1,变量提升:
看一段代码:
var name = "haha";
function changeName(){
console.log(name)
var name = "xixi";
}
changeName();
console.log(name);
输出结果是:undefined 和 haha。
为什么不是: haha或者xixi啊?
这个现象就是变量提升,就是把变量提升到函数的顶部,需要注意的是,变量提升只是提升变量的声明,不会吧变量的值也提升上来!
上述代码相当于:
var name="haha";
function changeName(){
var name;
console.log(name);
name="xixi";
}
changeName();
console.log(name);
2,函数提升:
在JavaScript中函数的创建方式有三种:函数声明(静态的,像函数example()的形式)、函数表达式(函数字面量)、函数构造法(动态的,匿名的)。
注意:只有函数声明才存在提升。
**//函数声明
function myTest1(){
func();
function func(){
console.log("我可以被提升");
}
}
myTest1();//函数表达式function myTest2(){
func();
var func =function(){
console.log("我不能被提升");
}
}
myTest2();
打印结果:
image3,函数提升和变量提升的优先级:
函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。
示例:
console.log(a);// f a() {console.log(10)}
console.log(a());// undefined
var a =3;
function a(){
console.log(10)//10
}
console.log(a)//3
a =6;
console.log(a());//a is not a function;
五、下面回到闭包的用途:
1,读取函数内部变量(函数作为返回值):
用法:通过函数返回值:
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
2,模拟一个块级作用域(函数作为参数被传递):
for(var i=1; i<=5; i++) {
(function(i){
setTimeout(function timer(){
console.log(i);
}, i*1000);
})(i)
}
3,模拟私有方法(模块模式):
(function(){
var a =10;
var b =20;
function add(num1, num2){
var num1 = !!num1 ? num1 : a;
var num2 = !!num2 ? num2 : b;
return num1 + num2;
}
window.add = add;
})();
add(10,20);
六、闭包的危害:
关于闭包的危害,倒是没什么可争论的:
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。(MDN)
阮一峰:
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
----------------一个朋友跟我说父函数看不懂,可以理解成函数外部的。
PS:
作为一个老司机,还是忍不住BB两句,那个自称会写TS的前端小朋友,谦虚点没什么坏处,前端的技术更新很快,但是对经典的学习,是每个开发者必须要走完的路!!!
前端水很深,且行且珍惜,保持一颗学习心,一颗愿意交流的心,比什么都重要!
Be Gentle! && Stay Hungry Stay Foolish!
PPS:
简书附上代码还是第一次,如果引起不适,请多担待,听朋友的话,把富文本改成了Markdown。一时还不太习惯,以后慢慢改进好了!
最后附上学习链接:
廖雪峰官网:
阮一峰:
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
MDN:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
某简书作者:
https://www.jianshu.com/p/21a16d44f150
其他:
https://blog.csdn.net/whd526/article/details/70990994
https://www.cnblogs.com/wangfupeng1988/p/3994065.html