JS闭包、作用域链、垃圾回收、内存泄露相关知识小结
补充:
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
闭包的三个特性:
1.函数嵌套函数
2.函数内部可以引用外部的参数和变量
3.参数和变量不会被垃圾回收机制回收
闭包的定义及其优缺点:
闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量
闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
闭包是javascript语言的一大特点,主要应用闭包场合主要是为了:设计私有的方法和变量。
一般函数执行完毕后,局部活动对象就被销毁,内存中仅仅保存全局作用域。但闭包的情况不同!
围着主题展开话题
1. 闭包的定义?
来看一些关于闭包的定义:
1.闭包是指有权访问另一个函数作用域中变量的函数
2.函数对象可以通过作用域链相关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称为 ‘闭包' 。
3.内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments)。
来个定义总结
1.可以访问外部函数作用域中变量的函数
2.被内部函数访问的外部函数的变量可以保存在外部函数作用域内而不被回收---这是核心,后面我们遇到闭包都要想到,我们要重点关注被闭包引用的这个变量。
来创建个简单的闭包
var sayName = function () {
var name = 'jozo';
return function () {
alert(name);
}
};
var say = sayName();
say();
来解读后面两个语句:
•var say = sayName() :返回了一个匿名的内部函数保存在变量say中,并且引用了外部函数的变量name,由于垃圾回收机制,sayName函数执行完毕后,变量name并没有被销毁。
•say() :执行返回的内部函数,依然能访问变量name,输出 'jozo' .
2. 闭包中的作用域链
理解作用域链对理解闭包也很有帮助。
变量在作用域中的查找方式应该都很熟悉了,其实这就是顺着作用域链往上查找的。
当函数被调用时:
1.先创建一个执行环境(execution context),及相应的作用域链;
2.将arguments和其他命名参数的值添加到函数的活动对象(activation object)
作用域链:当前函数的活动对象优先级最高,外部函数的活动对象次之,外部函数的外部函数的活动对象依次递减,直至作用域链的末端--全局作用域。优先级就是变量查找的先后顺序;
先来看个普通的作用域链:
function sayName(name) {
return name;
}
var say = sayName('jozo');
这段代码包含两个作用域:a.全局作用域;b.sayName函数的作用域,也就是只有两个变量对象,当执行到对应的执行环境时,该变量对象会成为活动对象,并被推入到执行环境作用域链的前端,也就是成为优先级最高的那个。 看图说话:
这图在JS高级程序设计书上也有,我重新绘了遍。
在创建sayName()函数时,会创建一个预先包含变量对象的作用域链,也就是图中索引为1的作用域链,并且被保存到内部的[[Scope]]属性中,当调用sayName()函数的时候,会创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起作用域链,此后,又有一个活动对象(图中索引为0)被创建,并被推入执行环境作用域链的前端。
一般来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是,闭包的情况又有所不同 :
再来看看看闭包的作用域链:
function sayName(name) {
return function () {
return name;
}
}
var say = sayName('jozo');
这个闭包实例比上一个例子多了一个匿名函数的作用域:
在匿名函数从sayName()函数中被返回后,它的作用域链被初始化为包含sayName()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在sayName()中定义的所有变量和参数,更为重要的是,sayName()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链依然在引用这个活动对象,换句话说,sayName()函数执行完后,其执行环境的作用域链会被销毁,但他的活动对象会留在内存中,直到匿名函数会销毁。这个也是后面要讲到的内存泄露的问题。
作用域链问题不写那么多了,写书上的东西也很累 o(╯□╰)o
3. 闭包的实例
实例1:实现累加
// 方式1
var a = 0;
var add = function () {
a++;
console.log(a)
}
add();
add(); //方式2 :闭包
var add = (function () {
var a = 0;
return function () {
a++;
console.log(a);
}
})();
console.log(a); //undefined
add();
add();
相比之下方式2更加优雅,也减少全局变量,将变量私有化
实例2 :给每个li添加点击事件
var oli = document.getElementsByTagName('li');
var i;
for (i = 0; i < 5; i++) {
oli[i].onclick = function () {
alert(i);
}
}
console.log(i); // 5
//执行匿名函数
(function () {
alert(i); //5
}());
上面是一个经典的例子,我们都知道执行结果是都弹出5,也知道可以用闭包解决这个问题,但是我刚开始始终不能明白为什么每次弹出都是5,为什么闭包可以解决这问题。后来捋一捋还是把它弄清晰了:
a. 先来分析没用闭包前的情况:for循环中,我们给每个li点击事件绑定了一个匿名函数,匿名函数中返回了变量i的值,当循环结束后,变量i的值变为5,此时我们再去点击每个li,也就是执行相应的匿名函数(看上面的代码),这是变量i已经是5了,所以每个点击弹出5. 因为这里返回的每个匿名函数都是引用了同一个变量i,如果我们新建一个变量保存循环执行时当前的i的值,然后再让匿名函数应用这个变量,最后再返回这个匿名函数,这样就可以达到我们的目的了,这就是运用闭包来实现的!
b. 再来分析下运用闭包时的情况:
var oli = document.getElementsByTagName('li');
var i;
for (i = 0; i < 5; i++) {
oli[i].onclick = (function (num) {
var a = num; // 为了说明问题
return function () {
alert(a);
}
})(i)
}
console.log(i); // 5
这里for循环执行时,给点击事件绑定的匿名函数传递i后立即执行返回一个内部的匿名函数,因为参数是按值传递的,所以此时形参num保存的就是当前i的值,然后赋值给局部变量 a,然后这个内部的匿名函数一直保存着a的引用,也就是一直保存着当前i的值。 所以循环执行完毕后点击每个li,返回的匿名函数执行弹出各自保存的 a 的引用的值。
4. 闭包的运用
我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。
1. 匿名自执行函数
我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:
//将全部li字体变为红色
(function () {
var els = document.getElementsByTagName('li');
for (var i = 0, lng = els.length; i < lng; i++) {
els[i].style.color = 'red';
}
})();
我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此els,i,lng这些局部变量在执行完后很快就会被释放,节省内存!
关键是这种机制不会污染全局对象。
2. 实现封装/模块化代码
var person = function () {
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName: function () {
return name;
},
setName: function (newName) {
name = newName;
}
}
}();
console.log(person.name); //直接访问,结果为undefined
console.log(person.getName()); //default
person.setName("jozo");
console.log(person.getName()); //jozo
3. 实现面向对象中的对象
这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,我们可以模拟出这样的机制。还是以上边的例子来讲:
function Person () {
var name = "default";
return {
getName: function () {
return name;
},
setName: function (newName) {
name = newName;
}
}
};
var person1 = Person();
person1.getName();
person1.setName("person1");
person1.getName(); // person1 var person2 = Person();
person2.getName();
person2.setName("person2");
person2.getName(); //person2
Person的两个实例person1 和 person2 互不干扰!因为这两个实例对name这个成员的访问是独立的 。
5. 内存泄露及解决方案
垃圾回收机制
说到内存管理,自然离不开JS中的垃圾回收机制,有两种策略来实现垃圾回收:标记清除 和 引用计数;
标记清除:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 2008年为止,IE,Firefox,opera,chrome,Safari的javascript都用使用了该方式;
引用计数:跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,如果这个值再被赋值给另一个变量,则引用次数加1。相反,如果一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收。
这个方式存在一个比较大的问题就是循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0 。早期的IE版本里(ie4-ie6)采用是计数的垃圾回收机制,闭包导致内存泄露的一个原因就是这个算法的一个缺陷。
我们知道,IE中有一部分对象并不是原生额javascript对象,例如,BOM和DOM中的对象就是以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数。因此,虽然IE的javascript引擎采用的是标记清除策略,但是访问COM对象依然是基于引用计数的,因此只要在IE中设计COM对象就会存在循环引用的问题!
举个栗子:
window.onload = function () {
var el = document.getElementById("id");
el.onclick = function () {
alert(el.id);
}
}
这段代码为什么会造成内存泄露?
el.onclick = function () {
alert(el.id);
};
执行这段代码的时候,将匿名函数对象赋值给el的onclick属性;然后匿名函数内部又引用了el对象,存在循环引用,所以不能被回收;
解决方法:
window.onload = function () {
var el = document.getElementById("id");
var id = el.id; //解除循环引用
el.onclick = function () {
alert(id);
}
el = null; // 将闭包引用的外部函数中活动对象清除
}
JS闭包、作用域链、垃圾回收、内存泄露相关知识小结的更多相关文章
- 【转载】Java垃圾回收内存清理相关(虚拟机书第三章),GC日志的理解,CPU时间、墙钟时间的介绍
主要看<深入理解Java虚拟机> 第三张 P84 开始是垃圾收集相关. 1. 1960年诞生于MIT的Lisp是第一门采用垃圾回收的语言. 2. 程序计数器.虚拟机栈.本地方法栈3个区域随 ...
- JS 之作用域链和闭包
1.JS无块级作用域 <script> function Main(){ if (1==1){ var name = "alex"; } console.log(nam ...
- js中作用域链的问题
为什么没有var声明的变量是全局的? 是因为,在js中,如果某个变量没有var声明,会自动到上一层作用域中去找这个变量的声明语句,如果找到,就使用,如果没有找到,继续向上查找,一直查找到全局作用域为止 ...
- 浅谈JS的作用域链(二)
上一篇文章中介绍了Execution Context中的三个重要部分:VO/AO,scope chain和this,并详细的介绍了VO/AO在JavaScript代码执行中的表现. 本文就看看Exec ...
- JS的作用域链与this指向
JS的作用域链是在函数创建时创建的.而this对象是在函数运行期间绑定的. 下面看几个例子,说明JS的作用域链和this是两套分离的链. 1) var name = 'window下的name< ...
- JS高程中的垃圾回收机制与常见内存泄露的解决方法
起因是因为想了解闭包的内存泄露机制,然后想起<js高级程序设计>中有关于垃圾回收机制的解析,之前没有很懂,过一年回头再看就懂了,写篇博客与大家分享一下. #内存的生命周期: 分配你所需要的 ...
- js之作用域链到闭包
一.作用域 全局作用域和函数作用域(局部作用域). 一个变量的作用域就是源代码中定义这个变量的区域. 二.作用域链和闭包 全局变量只有一个(window,globel),全局环境下每一个函数都会形成一 ...
- 深入理解JS函数作用域链与闭包问题
function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } ); a.fun(); a.f ...
- js原型链闭包作用域链-Tom
1.原型相当于Java.C++里面的父类,由封装公有属性及方法而产生,子类可以继承. 原型继承实现(函数的原型属性指向原型函数一个实例对象,函数的原型的构造函数指向函数本身) 1)eg:原型链 fun ...
随机推荐
- 数数(高维DP)
T1 数数 [问题描述] fadbec 很善于数数,⽐如他会数将 a 个红球,b 个黄球,c 个蓝球,d 个绿球排成⼀列,任意相邻不同⾊的数⽬. 现在 R 君不知道 fadbec 数的对不对,想让你也 ...
- 936. Stamping The Sequence
You want to form a target string of lowercase letters. At the beginning, your sequence is target.len ...
- kali linux之msf信息收集
nmap扫描 Auxiliary 扫描模块 目前有557个扫描方式
- linux安装报错之:ifconfig command not found解决
问题描述: 用虚拟机VMware安装linux系统(镜像文件是从官网下载的CentOS-7.0-1406-x86_64-DVD.iso), 在安装完成之后,输入ifconfig命令报错:ifconfi ...
- JAVA 多线程 杂谈
一:java创建线程的三种方式: 1.继承Thread类: 2.实现Runnable接口: 3.实现Callable接口:Callable接口重写的是 call() 方法.1-允许有返回值,2-允许抛 ...
- 【手记】如果Idx/Sub字幕导不进MKVToolNix,看看是否这个原因
用记事本之类的文本编辑器打开idx文件,看看时间轴部分是不是存在不规范的条目,比如: timestamp: :::, filepos: 注意,上述条目中,filepos:后面缺了一个空格,就这么一处问 ...
- python高级(四)—— 文本和字节序列(编码问题)
本文主要内容 字符 字节 结构体和内存视图 字符和字节之间的转换——编解码器 BOM鬼符 标准化Unicode字符串 Unicode文本排序 python高级——目录 文中代码均放在github上: ...
- idea使用时,部分jdk的jar包(tool.jar com.sun.javadoc) 无法引入-gradle处理方案
gradle 增加配置 def jdkHome = System.getenv("JAVA_HOME") dependencies { compile files("$j ...
- jq扩展获取表单值、设置值
不多说,直接上代码 //jq添加插件 (function (window, $) { $.fn.serializeJson = function () { var serializeObj = {}; ...
- UML-6.2-用例-用例模型/用例/场景关系
参与者:具有某些行为的人或事物.如上一章中的收银员. |_主要参与者:收银员. |_协助参与者:程序(自动付费.帮收银员验证输入要素) |_幕后参与者:政府等(电子签章取证找公证机构) 用例:一组相关 ...