不同人站在不同的角度对闭包有不同的解释,分别有以下三种比较权威的解释:
函数与其词法环境的引用共同构成了闭包。也就是说,闭包可以让你从内部函数访问外部函数作用域,在JavaScript中函数每次创建时生成闭包。——MDN
函数可以记住并访问所有的词法作用域时,就产生了闭包。即使函数是在当前作用域外执行。——《你不知道的JavaScript(上卷)》
有权访问另一个函数作用域中的变量的函数就是闭包。——《JavaScript高级程序设计(第三版本)》
在我看来,浏览器加载页面会把代码放到栈内存中执行(ECStack),函数进栈执行会产生一个私有的上下文(EC),这个上下文能保护里面的私有变量(AO)不受外界干扰,并且如果当前上下文中的某些内容被上下文以外的内容所占用,当前上下文是不会出栈释放的,这样可以保存里面的变量和变量值,所以闭包可以看成是一种保存和保护内部私有变量的机制。
下面可以通过几道题目深入的理解什么是闭包?闭包的作用?
let x = 1;
function A(y){
let x = 2;
function B(z){
console.log(x+y+z);
}
return B;
}
let C = A(2);
C(3);
在上面的代码执行中我们要注意以下几点:
1.VO和GO的区别和关联性
区别:GO是全局对象,它是一个堆内存,里面存放的是浏览器提供的内置属性和方法,浏览器会让window指向GO,所以在浏览器端window代表的就是全局对象。每一个执行环境都是有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中, 这个变量对象称之为VO(Variable Object)。它们是不同的。
关联:基于var/function在全局上下文中声明的全局变量根据映射机制也会给GO赋值一份,但就let/const等es6方式在全局上下文中创建的全局变量和GO没有任何关系。
2.函数执行
每一个函数执行都会形成一个私有的上下文,私有上下文形成后,会有一个存放私有变量对象的AO,它会进入ECStack执行。每一个执行上下文形成之后,代码执行之前,要进行变量提升 。var/function存在变量提升,let/const不存在变量提升。接下来要初始化作用域链、初始化this、初始化arguments、形参赋值、变量提升,最后才是代码执行。
3.不销毁的上下文
正常情况下,代码执行完成后,私有上下文会出栈释放,节约栈内存空间,但是如果当前私有上下文中的东西被上下文以外的事物所引用,则上下文不会在出栈释放,形成了不销毁的上下文。
let x = 5;
function fn(x) {
return function(y) {
console.log(y + (++x));
}
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);
真实项目中为了保证js的性能(堆栈内存的性能优化)应该减少闭包的使用。但是又因为闭包它可以保护私有变量不受外界的干扰,形成不销毁的栈内存,把一些值保存下来,方便后面代码调取使用又不可避免的使用闭包。
示例:选项卡
html:
var oTab = document.getElementById('tabBox'),
navBox = document.getElementById('navBox');
tabList = navBox.getElementsByTagName('li'),
divList = oTab.getElementsByTagName('div');
function changeTab(curIndex) {
for (var i = 0; i < tabList.length; i++) {
tabList[i].className = divList[i].className = '';
tabList[curIndex].className = 'active';
divList[curIndex].className = 'active';
}
}
css;
* {padding: 0; margin: 0;}
ul {list-style: none;}
#tabBox {box-sizing: border-box; 500px;margin: 100px auto;}
#navBox {display: flex;position: relative;top: 1px;}
#navBox li {box-sizing: border-box;padding: 0 10px;margin-right: 10px;line-height: 35px;border: 1px solid #999;}
#navBox li.active {border-bottom-color: #fff;}
#tabBox div {display: none;box-sizing: border-box;padding: 10px;height: 150px;border: 1px solid #999;}
#tabBox div.active {display: block;}
js:
var oTab = document.getElementById('tabBox'),
navBox = document.getElementById('navBox'),
tabList = navBox.getElementsByTagName('li'),
divList = oTab.getElementsByTagName('div');
function changeTab(curIndex) {
for (var i = 0; i < tabList.length; i++) {
tabList[i].className = divList[i].className = '';
tabList[curIndex].className = 'active';
divList[curIndex].className = 'active';
}
}
/*
for (var i = 0; i < tabList.length; i++) {
tabList[i].onclick = function () {
changeTab(i);
//解释:
//=> 事件绑定是“异步编程”,当触发点击行为,绑定的方法执行时,外层循环已经结束:方法执行产生私有作用域,用到变量i,这个i不是私有的变量,
//按“作用域链”的查找机制,找到的是全局下的i(此时全局的i已经完成,成为循环最后的结果)
}
}
*/
// 解决方法:
/*
//1.自定义属性
for (var i = 0; i < tabList.length; i++) {
tabList[i].myIndex = i;
tabList[i].onclick = function () {
changeTab(this.myIndex);
//this:给当前元素的某个事件绑定方法,当事件触发,方法执行的时候,方法中的this是当前操作的元素对象
}
}
*/
//2.闭包 利用闭包机制,把后续需要的索引存储到自己的私有作用域中“闭包有保存作用”
/*
for (var i = 0; i < tabList.length; i++) {
tabList[i].onclick = (function (n) {
//让自执行函数执行,把执行的返回值return赋值给onclick
var i = n;
return function () {
changeTab(i); //上级作用域:自执行函数形成的作用域
}
})(i);
}
*/
//3.es6 它和闭包机制类似,es6中let创建变量时会形成块级作用域,当前案例中,每一轮循环都是会形成一个自己的块级作用域,把后续需要用到的索引i存储到自己的作用域中
/*
for (let i = 0; i < tabList.length; i++) {
tabList[i].onclick = function () {
changeTab(i);
}
}
*/