闭包,是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
好啦说人话:
“我的理解是,闭包就是能够读取其他函数内部变量的函数。”--------阮一峰
相较官方文档而言阮一峰老师直接的多。不过由于js语言的特殊性使得不像其他面向对象语言一样拥有明确的类与对象的关系以及特殊的作用域,闭包的作用也就尤为重要了
不过在此之前我们需要做一些铺垫。++不想看的老铁可以直接跳到正文哦++
JavaScript内存机制
底层语言中用户一般都可以控制自己的内存(比如C中的malloc和free)。而高级语言则有一套自己的垃圾回收机制。
JavaScript同样也有一套属于自己的内存管理机制:内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时“自动”释放。后者被称为垃圾回收。
内存模型
JS内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)。 其中栈存放变量,堆存放复杂对象,池存放常量。
基础数据类型与栈内存
JS中的基础数据类型,这些值都有固定的大小,往往都保存在栈内存中(闭包除外),由系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问
数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。
基础数据类型: Number String Null Undefined Boolean
引用数据类型与堆内存
JS的引用数据类型,比如数组Array,它们值的大小是不固定的。
引用数据类型的值是保存在堆内存中的对象。JS不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。
堆存取数据的方式,则与书架与书非常相似。
书虽然也有序的存放在书架上,但是我们只要知道书的名字,我们就可以很方便的取出我们想要的书。好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。
我们来举个例子
// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?
在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。
所以给b重新赋值之后b和a的值互不影响。
// demo02.js
var m = { a: 10, b: 20 };
var n = m;
n.a = 15;
// 这时m.a的值是多少
在demo02中,我们通过var n = m执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在栈内存中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在堆内存中访问到的具体对象实际上是同一个。
|栈内存空间||
|变量名|具体值|
内存的生命周期
这一段对闭包的影响最大!!!!!
JS环境中分配的内存一般有如下生命周期:
- 内存分配:当我们申明变量、函数、对象的时候,系统会自动为他 们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
为了便于理解,我们使用一个简单的例子来解释这个周期。
var a = 20; // 在内存中给数值变量分配空间
alert(a + 100); // 使用内存
var a = null; // 使用完毕之后,释放内存空间
第一步和第二步我们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。
- 从内存来看 null 和 undefined 本质的区别是什么?
- 为什么typeof(null) //object typeof(undefined) //undefined?
- 现在再想想,构造函数和立即执行函数的声明周期是什么?
- 对了,ES6语法中的 const 声明一个只读的常量。一旦声明,常量的值就不能改变。但是下面的代码可以改变 const 的值,这是为什么?
const foo = {};
foo.prop = 123;
foo.prop // 123
foo = {}; // TypeError: "foo" is read-only
内存回收
JavaScript有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?其实很简单,就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。
在JavaScript中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此 a = null 其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。
-
在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。
-
以Google的V8引擎为例,在V8引擎中所有的JAVASCRIPT对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。
-
另外,V8引擎对堆内存中的JAVASCRIPT对象进行分代管理。新生代:新生代即存活周期较短的JAVASCRIPT对象,如临时变量、字符串等;
老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。
function fun1() {
var obj = {name: 'csa', age: 24};
}
function fun2() {
var obj = {name: 'coder', age: 2}
return obj;
}
var f1 = fun1();
var f2 = fun2();
在上述代码中,当执行var f1 = fun1();的时候,执行环境会创建一个{name:‘csa’, age:24}这个对象,当执行var f2 = fun2();的时候,执行环境会创建一个{name:‘coder’, age=2}这个对象,然后在下一次垃圾回收来临的时候,会释放{name:‘csa’, age:24}这个对象的内存,但并不会释放{name:‘coder’, age:2}这个对象的内存。这就是因为在fun2()函数中将{name:‘coder, age:2’}这个对象返回,并且将其引用赋值给了f2变量,又由于f2这个对象属于全局变量,所以在页面没有卸载的情况下,f2所指向的对象{name:‘coder’, age:2}是不会被回收的。
由于JavaScript语言的特殊性(闭包…),导致如何判断一个对象是否会被回收的问题上变的异常艰难。
咳咳有点跑题了。
正文!!!
我们先用一个例子来开个头
var count=10;//全局作用域 标记为flag1
function add(){
var count=0;//函数全局作用域 标记为flag2
return function(){
count+=1;//函数的内部作用域
alert(count);
}
}
var s=add()
s();//输出1
s();//输出2
- add()的返回值是一个函数,首先第一次调用s()的时候,是执行add()的返回的函数
- 也就是将count+1,在输出,那count是从哪儿来的的呢,根据作用域链的规则,底层作用域没有声明的变量,会向上一级找,找到就返回,没找到就一直找,直到window的变量,没有就返回undefined。这里明显count 是函数内部的flag2 的那个count
- 不过诸位有没有发现:为什么第二次输出的是2呢?原因在于第二次调用add时count的值还是第一次执行add时留下的count变量。
如果一个变量的引用不为0,那么他不会被垃圾回收机制回收,引用,就是被调用
由于再次执行s()的时候,再次引用了第一次add()产生的变量count ,所以count没有被释放,第一次s(),count 的值为1,第二次执行s(),count的值再加1,自然就是2了。
//如果我们将count变量的作用域做一下更改,那么结果就完全不同了
function add(){
var count=0;//函数全局作用域
return function(){
count+=1;//函数的内部作用域
alert(count);
}
}
add()();//输出1
add()();//输出1
使用闭包的注意点
-
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
-
闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
闭包的用途
Ⅰ.函数防抖&&函数节流
DOM中的debounce其实是从机械开关和继电器的“去弹跳”衍生而来的,基本思路是将多个信号合并为一个信号
而js中debounce就是用来强制一个函数在某个连续时间段内只执行一次,哪怕它会被多次调用
- eg:通过用户的输入实时向服务器发送Ajax请求获取数据
function debounce(fn, delay){
var timer; //定时器,用来setTimeout
return function(){
//保存函数调用时的上下文和参数
var context = this;
var args = arguments;
//每次返回的函数被调用就清空计时器,以保证不执行fn
clearTimeout(timer);
//当返回函数的最后一次调用后(即用户停止了某连续操作)
timer = setTimeout(function(){
fn.apply(context, args);
}, delay);
}
}
//当用户停止输入时(0.3s)向服务器发送数据
$(selector).on('keyup', debounce(function(e){
//发送ajax请求
}, 300);
- eg2:每隔30秒向后台发送ajax请求新数据以更新页面信息
function throttle(fn, threshhold){
//记录上次执行时间
var last;
//定时器
var timer;
//默认间隔为30 000毫秒
threshhold || (threshhold = 30000)
//返回的函数每过threshhold ms执行一次
return function(){
var context = this;
var args = arguments;
var now = new Date();
//若距上次执行fn函数的时间小于threshhold则放弃
if(last && now<last+threshhold){
clearTimeout(timer);
//保证在当前时间结束后再执行一次fn
timer = setTimeout(function(){
last = now;
fn.apply(context, args);
}, threshhold);
}else{
last = now;
fn.apply(context, args);
}
}
}
什么你告诉我你不知道apply?那你看一下另一篇博客好啦
Ⅱ.设置私有变量
Java里可以用private,但是js只能咱们自己搞啦
- 首先是ES6的方法
let _width = Symbol();
class Private {
constructor(s) {
this[_width] = s
}
foo() {
console.log(this[_width])
}
}
var p = new Private("50");
p.foo();
console.log(p[_width]);//可以拿到
- 然后是传统闭包
//赋值到闭包里
let sque = (function () {
let _width = Symbol();
class Squery {
constructor(s) {
this[_width] = s
}
foo() {
console.log(this[_width])
}
}
return Squery
})();
let ss = new sque(20);
ss.foo();
console.log(ss[_width])
Ⅲ.最后是小红书上的经典问题:拿到正确的值
for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i)//10个10
},1000)
}
只要用到闭包一切就好办多啦
for(var i=0;i<10;i++){
((j)=>{
setTimeout(function(){
console.log(j)//1-10
},1000)})(i)
}
不会用箭头函数?那咱们换种传统的方法
for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i)//10个10
}(i),1000)
}
最后特别感谢几位大佬–qiudaoermu、梁音、唯情–的分享,大家可以去博客园,掘金上找到他们的身影