作用域链
我们知道在js中作用域分为全局作用域与局部作用域,作用域链的访问规则为从内到外,也就是说如果当前的作用域中没有该变量或者方法,则会在包含该作用域的外层作用域中,一层一层的向上找,直到window作用域,并且外层不能访问内层的作用域,不同的局部作用域之间也是不能互相访问的,如下样例:
var a=1
function fn(){
a=2
console.log(a)
}
fn() //2
以上的代码为什么会输出2,就是跟作用域链的访问规则相关
什么是闭包?
上面说到由于作用域链的访问规则,我们是无法访问到一个函数内部的变量的,为了实现这样的访问,可以利用闭包,闭包是指有权访问另一个函数作用域中的变量,变量可以反复使用,也不会污染全局
闭包创建的三个步骤:
- 在一个函数里面创建另一个函数
- 内部的函数可以引用外部的变量
- 内层函数作为外层函数的返回值
function A() {
var x = 1
function B() {
return ++x
}
return B
}
var m=A()
console.log(m()) //2
console.log(m()) //3
为什么上面的B()在调用的时候,值会递增,这就是闭包的特点之一,参数和变量不会被垃圾回收机制回收,也就是说外层的活动对象不能被垃圾回收机制回收,因为内层函数引用外层函数的活动对象。
闭包的原理
下面以图例说明代码片段的整个执行过程:
函数在创建的时候会创建两个对象,一个是函数本身,一个是作用域链对象,该作用域链对象为一个栈结构,栈的底端放的是window对象,栈的顶端放的是函数调用时产生的活动对象,如果函数调用的过程中需要某一个变量或者方法,会从栈的顶端向下找,直到window对象。
一般函数执行完毕之后,产生的活动对象会被垃圾回收机制回收,也就是说A函数在执行完毕之后产生的活动对象会被销毁,但是这里A产生的活动对象与函数B还存在引用关系,因此不会被销毁。
闭包的优点
我们知道全局变量,可以在任意一个函数中去修改,变量可能被污染;局部变量只能在函数体内部使用,不能全局使用。
综上原理,我们知道如果想让一个变量长期驻扎在内存中重复使用,并且不希望被全局变量污染,使用闭包就再合适不过了。
闭包的优点总结为以下几点:
-
变量可以重复使用
-
避免全局变量的污染
-
拥有私有成员
闭包的缺点
function A() {
var x = 1;
return function () {
console.log(++x);
}
}
var B= A()
B()//2
B()//3
var C = A()
C()//2
C()//3
如上面这段代码,第一次执行A函数时,x从1开始,第二次执行A函数时,x依旧从1开始,因为两次执行分别产生两个不同的地址B和C,两者之间是相互独立的,B和C在调用时,产生的活动对象是不一样的。
因此如果滥用闭包,就会造成一些性能的问题,也可能导致内存泄漏
什么是内存泄漏?
js自身都有一个内存回收机制,当分配出去的内存不使用,便会回收,内存泄漏的根本原因就是,代码中分配了一些顽固的内存,无法回收,如果这样的顽固内存还在一直不停的分配,就会导致内存不足,造成泄漏,因此,尽量少使用闭包。
闭包中的难点
function A(){
var B=[]
for(var i=0;i<10;i++){
B[i]=function(){
return i
}
}
return B
}
for(var i=0;i<10;i++){
var C=A()
console.log(C[i]()) //10
}
根据代码的我们猜测输出的结构为0-9,但是实际上输出全都是10,A函数在执行的时候,创建了活动变量B和 i ,并且给B数组中的每一项赋值为一个地址,当执行C函数时,才会产生他的活动变量,但是此时变量 i 已经循环完成,变为10,因此要知道活动变量产生的时机,只有在函数被调用时产生。
闭包的使用
设计模式中的单例模式
**单例模式的思想在于 单例对象的类必须保证只有一个实例存在 。**也就说如果第一次new,则产生一个,如果第二次new,不会产生新的实例,而是返回第一次new的实例,因此我们就需要将产生的实例存储在变量中,在以后每次new一个实例的时候,都需要去判断这个变量是否存在,但是又不能将这个变量放在全局,并且需要它长期驻扎在内存中,因此我们使用闭包实现。
function A() {
function ClassName(name) {
this.name=name
}
var a = null //存储className类的实例
return function (name) {
if (!a) {
a = new ClassName(name)
return a
}
return a
}
}
var B = A()//第一次调用A函数
console.log(B("1")) //name :"1"
console.log(B("2")) //name :"1"
var C = A()//第二次调用A函数
console.log(C("2")) //name :"2"
console.log(B===C) //false
我们可以通过以上这样的闭包来实现单例模式,但是如果我们多次调用A函数,就产生多个闭包,可以产生多个实例,因此这里希望A函数不能多次被调用,只能被调用一次,我们就可以把外层函数放在一个自调用函数中。
var B = (function A() {
function ClassName(name) {
this.name = name
}
var a = null //存储className类的实例
return function (name) {
if (!a) {
a = new ClassName(name)
return a
}
return a
}
})()
console.log(B("1")) //name :"1"
console.log(B("2")) //name :"1"
这样就能严格的实现单例模式了
函数防抖
在开发的过程中,用户频繁的触发向后台发送数据,会对服务器造成压力,可以使用函数防抖来解决这个问题
原理:在一件事情结束或者给定的时间内没有执行,再去执行回调函数
例如注册时,用户名唯一的情况下,用户在输入用户名时,我们需要实时的向后端发送请求,判断是否存在该用户名,但是如果用户每输入一个字符都去执行这个过程的话,一是浪费资源,二是给服务器造成压力,解决方案就是如果用户在给定的时间范围内没有输入,则去发送请求。
$(".userName").keyup(debounce(inputChange,1000))) //监听用户名输入框
function debounce(inputChange,time){
var timer=null
return function(){
if(timer){
clearTimeout(timer)
timer=null
}
timer=setTimeout(function(){
inputChange()
},time)
}
}
function inputChange(){ //事件处理函数
//请求后台的逻辑
}
每输入一个字符,都会调用debounce()函数,传入的参数为事件处理函数与间隔的时间,debounce()为一个闭包,内层函数使用外层函数的timer变量,如果闭包被频繁的调用,每一次都会在内层函数中先将该定时器清除,再赋值为延迟定时器,如果在1s内没有继续输入,timer就不会被清除,1s之后就会调用inputChange()函数,向后台发送请求。
函数防抖也就是利用了闭包的内层函数可以使用外层函数中的变量,并且外层变量不会被释放的原理