JavaScript的闭包原理及使用场景

介绍闭包前需要了解的

刚开始学习闭包,最经常听到描述闭包的一句话就是:“一个函数返回另一个函数,让你可以在一个内层函数中访问到其外层函数的作用域。”这句话没毛病的,甚至很直观地指出了闭包的形式。闭包就是一个为了重用对象,又保护对象不被篡改的机制。

在JavaScript中,如果在函数外直接访问函数内部定义的变量,是会报错的。如下面的代码的错误提示就是:Cannot access ‘num’ before initialization.

function aaa() {
let num = 0;
num++;
   console.log(num); //√
}
console.log(num);//×

V8引擎解析代码的工作原理

上面的代码,可以用V8引擎解析代码的工作原理讲一讲。首先,引擎会在内存中先创建一个执行环境栈,然后把window对象放入栈中,全局函数aaa作为window对象的方法被创建,同时这个window对象也放进了作用域链中。当我们调用这个全局函数的时候,就在执行环境栈里调用了这个方法,并会为这个函数临时生成一个活动对象AO(activation object)(AO包含了函数所有的局部变量、参数以及this),执行完函数后又立即销毁掉AO,函数也出栈。在未关掉浏览器前,全局变量还是在的。但是函数出栈了,活动对象被销毁了,函数里的变量的不再被引用,就被垃圾回收掉了。我们就在函数外访问不到num。这就是我们在函数里定义的局部变量不可重用的原因,这也是闭包需要出现的原因。(应该是这样?)

作用域和作用域链

说到这里也不得不提作用域和作用域链。每个函数都有自己的执行环境,当代码在环境中执行时,会创建变量对象的一个作用域。而作用域链,就是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。作用域的前端,始终是当前执行的代码所在的环境的变量对象。作用域链中的下一个变量对象来自外部环境,直到最后一个全局window对象作为作用域链的结束。搜索过程从作用域前端开始,一级一级网上找,找不到就停止和返回undefined。在上面的例子中,函数里的num一下就被访问到了。

闭包是什么?

我可以改下一下上面的例子,写成一个闭包的形式,然后重点看看输出什么。

function a() {
let num = 0;
num++;
return function getNum(){  //返回一个函数
  return num;  //在这个函数里返回这个变量
}
}
let number = a();
console.dir(number);//显示number对象的所有属性和方法

[[scopes]]就是我们的作用域,他显示着Closure里存着一个num:1的变量。也就是我们通过返回一个函数的闭包形式,顺利地把函数外部的变量保存了起来。

还是先说说怎么回答闭包是什么吧?

闭包是指有权访问另一个函数作用域中的变量的函数。父函数将子函数作为返回值,再将子函数赋值给一个变量,所以子函数就会存在于内存当中,而子函数依赖于父函数存在,所以父函数也存在内存当中,也就不会被垃圾回收机制回收。他让变量成为不可释放的引用,做到了延长变量的生命周期

再说一个超级超级常见的例子,就是for循环打印。

for (var i = 0; i < 4; i++) {  //当然,改写let就不一样了
 setTimeout(function() {
   console.log(i);
}, 300);
}

结果都是4个4,不是0,1,2,3.首先,这个i引用的都是同一个变量i。其次,setTimeout的执行涉及到事件循环机制,他会等待宏任务队列和微任务队列清空后才执行。然鹅,这个for循环执行的非常快,肯定在0.3秒内执行完了。以至于setTimeout到点执行的时候,打印的i引用的值都是for循环执行后的4了。

这时候我们可以创建闭包,让闭包保存一个自己i变量副本。

for (var i = 0; i < 4; i++) {
setTimeout(
(function (i) {
return function () {//每次形成的闭包互不影响
console.log(i);  //保存了对应的值
};
})(i),//传参自执行
300
);
}
// 或者
for (var i = 0; i < 4; i++) {
setTimeout(
(function () {
var temp = i;
return function () {
console.log(temp);
};
})(),
300
);
}

尽管setTimeout在等待着被执行,但是由于闭包的作用,他们携带着的是变量值都是for循环里对应的i的值了。

闭包的使用

事件防抖

这个例子体现了事件防抖,就是输入框停止输入后的一秒,才会执行函数(发起请求等等),这样避免了大量的无为请求。

//第一个简单应用:事件防抖
function antiShake(fn, wait) {
let timeOut = null; //当我输入的时候没有超过一秒,就不会触发
return args => {   //运用到一个闭包
if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(fn, wait)
}

}
function demo2() {
console.log("请求数据");
}
let telInput = document.querySelector("input");
telInput.addEventListener('input', antiShake(demo2, 1000));   //每隔一秒执行一次

用闭包模拟私有方法或私有属性

在面向对象的语言中,几乎都有类,类就要私有属性和方法。只能被同一个类中的其他方法所调用,不会被外部引用,不被污染的就是私有属性或者方法。虽然JavaScript没有类,但是用闭包可以模拟这个特性。

let makeCounter = function () {
let privateCounter = 0;
function changeBy(val) {//私有方法
privateCounter += val;
}
return {
increment: () => {
changeBy(1);
},
decrement: () => {
changeBy(-1);
},
getValue: () => {
return privateCounter;
}
}
}
let counter1 = makeCounter();//创建两个计数器,各自保持独立,每个闭包引用的都是自己词法环境的变量。
//一个闭包中的变量修改,不会影响到其他闭包中的变量,这就像面向对象当中的数据的隐藏和封装
let counter2 = makeCounter();
console.log(counter1.getValue());
counter1.increment();
counter1.increment();
counter2.increment();
console.log(counter1.getValue());
console.log(counter2.getValue());

总结

除了以上的优点,闭包在处理速度和内存消耗方面对脚本性能具有负面影响,因为过多使用闭包肯定还是要占内存的。

和闭包有关系的,我暂时还能想到,ES6中的尾调用优化的前提之一是函数不能是个闭包,不能有访问当前栈帧的变量。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇