JS闭包、闭包优缺点、this指向、内存泄漏
前奏
昨晚和百度高级前端工程师聊地好hi,我被“虐”的好惨,完全不懂他给我的上下文,我的回答也许都不在作用域内,比如下面系列问题:
1.img100-1000性能解决问题—瀑布流在不知道图片尺寸的情况下怎么做到每列高度相当:这是我项目里做的啊,当时误以为瀑布流下仍然存在的那点高度差怎么解决,以为要设置齐齐的,其实问的是我怎么做到的高度相当,我项目就是这样做的~列数定即宽度定,高度自适应,相对父容器绝对定位,每放一张图片都计算一下此前哪一列的高度小就置于哪一列下。
2.怎么判断画布中的一个圆被鼠标点击了:其实这个问题何尝不是我项目里用到的,当时听成”页面有个圆,怎么判断鼠标点击了它”,我幻想它是什么呢?是a标签的block表示的呢就active?是一个普通的div取完border-radius得到的圆形就mouseover?还是button?其实强调了画布不就很简单了嘛。点击画布绑定一个事件判断点击的坐标,与圆心坐标比较,就是项目里的落子事件实现反过来啊。
3.JS继承,闭包,闭包的优缺点,this指向情况产生什么问题怎么解决,内存泄漏与内存溢出————这就是我本文要细致讲讲的
4.盒子模型—IE下什么不同—ie的其他兼容性问题—css3新增的—html5新增的
5.node做了些什么—-模板引擎的原理是什么,应该问的就是JS引擎原理
6.http状态码301与302区别
7.跨域解决方案—单向还是双向
回头想想被问的就是自己项目里的实现方法,一些系统的基础知识。都没处于上下文环境中,状态没把握好啊。
好,回归正题,毕竟被系列问了继承、闭包、this、内存泄漏。
闭包概念
- 闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数的内部创建另一个函数。《JS高级程序设计第三版》P178
- 函数对象可以通过作用域链相关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性就叫闭包《JS权威指南》P183
- 内部函数可以访问定义他们的外部函数的参数和变量(除了this和arguments)《JS语言精髓》P36
总结
内部函数可以访问定义它的外部函数的参数和变量,当外部函数返回后,引用仍在未被销毁,闭包引用包含函数的整个活动对象,活动对象置空,解除引用即可,不然就会导致后面的内存泄漏问题。
闭包的例子
- 闭包只能取得包含函数中任何变量的最后一个值
1
2
3
4
5
6
7
8
9function setArr(){
var result=new Array();
for(var i=0;i<5;i++){
result[i]=function(){
return i;
}
}
return result;
}
红皮书这个例子放在引擎中执行返回是一组function的数组,想看result数组结果要这样1
2
3
4
5
6
7
8
9
10
11
12function setArr(){
var result=[];
for(var i=0;i<5;i++){
result[i]=function(){
return i;
};
}
for(var j=0;j<5;j++){
result[j]();
}
return result;
}
可见返回的结果是最后的I的值重复。原因就在于开始说的那句“闭包是对包含函数内任何变量的引用,只能取得最后值”。解决这一问题,其实有三种方法,包括红皮书上那种加匿名传参函数再匿名返回(做了下修改,否则匿名函数无法自执行返回一组function)如下:1
2
3
4
5
6
7
8
9
10
11function setArr(){
var result=[];
for(var i=0;i<5;i++){
result[i]=function(num){
return function(){
return num;
}();//匿名函数自执行
}(i);
}
return result;
}
红皮书采用按值传递参数的形式解决了闭包只取包含函数任何变量最后值所产生的问题,其实在我看来问题原没那么复杂,现列出以下几种解决方案:1
2
3
4
5
6
7function setArr(){
var result=[];
for(var i=0;i<5;i++){
result[i]=i;//去掉闭包
}
return result;
}
如果还是想用闭包,可以啊,每次赋值都立即执行就可以解决问题了1
2
3
4
5
6
7
8
9function setArr(){
var result=[];
for(var i=0;i<5;i++){
result[i]=function(){
return i;
}();//每次都立即执行,不采用最后执行当然数组随着i变也变咯
}
return result;
}
所以不得不来说说匿名自执行函数
匿名函数自执行
匿名自执行函数可以解决上面闭包所带来的问题,匿名函数想立即执行直接加()即可解决,而类似函数表达式的匿名函数,在赋值完再执行,看下面这个问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function setArr(){
var result=[];
for(var i=0;i<5;i++){
result[i]=function(){
return i;
};
}
for(var i=0;i<5;i++){
result[i]();
}
return result;
}//要调用一下setArr()执行
//2
(function(){
var result=[];
for(var i=0;i<5;i++){
result[i]=function(){
return i;
}();//每次都立即执行,不采用最后执行当然数组随着i变也变咯
}
return result;
})();//外层函数也立即执行,不调用了
这样的结果也是正确的,不过代码多么的冗长。其实这段代码更加反应了闭包返回包含函数变量最后值,因为在执行的时候又把包含函数的变量I改变了,而且立即执行,当然结果一样。算了不纠结这里了,总之记住“立即执行”和“闭包获取的只是包含函数变量的最后值”.
再欣赏一下闭包带来的封装性/模块化代码,减少全局变量污染1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21var add=function(n){
var a=0;
return function(){
for(var i=a+1;i<n;i++){
a+=i;
}
console.log(a);
}(n);
};
add(5);//10
//2
(function(n){
var a = 0;
return function(){
for(var i=a+1;i<n;i++){
a+=i;
}
return a;
}(n);
})(5);//10...终于体会了匿名函数自执行的用法了
//同时解释下为什么函数被()包裹一下:JS将function关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。而函数表达式后面可以跟圆括号,将函数声明转换为函数表达式方式有二---外加()、将其赋值给一个变量由变量名执行《JS高级程序设计》P185
再来看看闭包如何实现面向对象中的对象
1 | function Person(){ |
看到这个结果有点懵了,第一个实例改了name属性了,那么其他实例应该访问到的是改了之后的属性啊,那是继承啊,想想第六章的创建对象的几种模式、继承的几种方式。好了,开始解释:
- 关键的关键就在于采用了构造函数模式自定义特权方法,没有出现原型的
- 私有变量,任何在函数中定义的变量都不可以在函数外部被访问,可以认为是私有变量
- 内部创建一个闭包,执行流进入闭包,闭包的环境就被推入执行环境栈中并为其生成作用域链(本身的作用域,包含函数的作用域,全局的作用域),执行环境中的变量可以通过其作用域链访问到外部的私有变量,利用这一点就可以创建用于访问私有变量的公有方法—特权方法《JS高程》P187
- 私有变量在构造函数的每一个实例中都不相同,因为每次调用构造函数new新对象—将构造函数的作用域赋给新对象(this指向新对象)—执行构造函数中的代码(为新对象添加属性、方法函数等)—返回新对象。
- 因此person1、person2分别保存着Person的一个不同的实例
那么看看静态私有变量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20(function(){
var name="default";
Person=function(name){
this.name=name;
};
Person.prototype.getName=function(){
console.log(name);
};
Person.prototype.setName=function(newName){
name=newName;
};
})();//没有对匿名函数的引用,函数返回后即退出执行环境,销毁作用域链
var person1=new Person();
var person2=new Person();
person1.getName();//default Person
person1.setName("person1");
person1.getName();//person1
person2.getName();//person1
console.log(person1 instanceof Person);//true
console.log(person1 instanceof Object);//true
使用原型模式定义特权方法,从而实现所有实例共享私有变量。
闭包的优缺点
优点
- 封装性,变量私有化,减少全局变量污染
- 函数返回后,活动对象依然可以被访问,用得多了就内存泄漏了
缺点 - 作用域链那么长,占用大量内存
- 对一个变量的引用会常驻内存,引起内存泄漏
- 多查找作用域链中的一个层次就会在一定程度上影响查找速度,这也是使用闭包的不足
- this指向发生变化
接下来就讲讲this指向问题this指向
- this对象是在运行时基于函数的执行环境绑定的
- 全局函数中的this即window
- 函数作为某个对象的方法调用时,this等于那个对象
- 匿名函数的执行环境具有全局性,this等于window
- 在通过call(),apply(),bind()改变函数执行环境的情况下,this就会指向那个对象。其中bind()返回的是函数要调用执行一下
1
2
3
4
5
6//全局函数
var name="ping";
(function(){
var name="like";
console.log(this.name);//ping
})();
1 | //调用对象的函数,this指向对象 |
1 | //匿名函数中的this指向window |
1 | //function的call(),apply(),bind()改变函数执行环境 |
内存泄漏
《JS高级程序设计》P184:闭包引用包含函数的整个活动对象。1
2
3
4
5
6
7
8function assignHandler(){
var element=document.getElementById("someelement");
var id=element.id;//避免闭包对element的循环引用
element.click=function(){
console.log(id);
};
element=null;//解除引用,解除对dom的引用
}
解除引用,可以确保正常回收其占用的内存。垃圾收集会自动处理这一事项
自动垃圾收集机制
JS有自动垃圾收集机制,对不再使用的变量清除,释放其占用的内存。浏览器标示无用变量的策略有两种
标记清除
最常用的垃圾收集方式。垃圾收集器在运行的时候会存储在内存中的所有变量添加标记;然后会去掉环境中的变量以及被环境中变量引用的变量的标记;在此之后,再被加上标记的变量将被视为准备清除的变量,最后完成清除工作,释放内存空间。
引用计数
不太常用的,IE下COM对象的垃圾收集机制采用的是引用计数,要避免循环引用,因为引用计数永远显示它不能被清除。当引用次数变为0时垃圾收集下次运行时才清除并释放内存。