稳扎稳打JavaScript(四)——闭包
什么是闭包
定义
闭包是一个能够访问其他函数作用域的函数。
这句话看似拗口,如果读过先前的几篇博客,那理解起来应该不难。下面来解析一下这句话:
- 首先,闭包是一个函数;
- 其次,这个函数不仅能访问自己的作用域,更为关键的是它还能访问其他函数的作用域。
换句话说,如果一个函数能访问其他函数作用域中的变量,那么这个函数就叫做“闭包”。
如何创建闭包?
只要在一个函数中再定义一个函数,这个内部函数就是一个闭包。
注意:只要满足一个函数在另一个函数的内部的条件,这个内部函数就是闭包,不管这个内部函数是以怎样的形式存在于外层函数中的。下面举几个例子:
- 形式一:
function father () {
function son () {
}
}
- 形式二:
function father () {
var object = new Object();
object.son = function () {
}
}
- 形式三:
function father () {
var object = {
son : function(){}
};
}
以上三种形式定义的内部函数都是闭包。
- 形式一:在一个函数中又定义了个函数,这完全符合闭包的定义,不用多说。
- 形式二:在一个函数中先创建了一个对象,然后在对象内部定义了函数,这也是闭包。
- 形式三:本质上和形式二一样,也是先在函数内部定义了一个对象,再在对象内部定义了个闭包。只不过定义对象的方式和形式二有所区别。
闭包有何用?
前面闭包的定义中说“闭包能访问其他函数作用域中的变量”。“其他函数”指的是闭包所在的外层函数。 也就是说,闭包能访问它所在外层函数全部变量,即使外层函数已经执行结束。 闭包的这种特性有助于我们在JavaScript这门非面向对象的语言中实现面向对象的一些特性。这个后面会详细介绍。
闭包的原理
闭包的原理涉及到作用域链的内存模型,这里带大家回顾下作用域链,更为详细的内容请看:稳扎稳打JavaScript(一)——作用域链
以下列代码为例:
function father () {
var name = "爸爸";
var company = "Google";
function son () {
var name = "儿子";
var school = "启东中学";
alert(name);
alert(school);
}
return son;
}
这段代码在window作用域中定义了函数father; father中有两个局部变量:name、company; father中定义了一个闭包; 闭包中定义了两个局部变量:name、school。
- 当JS初始化完成后:
内存中出现与全局环境相关的四个东西:
- 全局环境的变量对象(用于存储全局环境的变量,比如:father函数)
- 全局环境的作用域 和 作用域链(作用域链是一个链表,由多个作用域构成,全局环境的作用域链中只有一个全局环境的作用域,它指向全局环境的变量对象)
- 全局环境的执行环境 和 执行环境栈(执行环境栈里面存放着一个个执行环境,栈顶的那个表示正在执行的环境。执行环境中有一个指针指向它的作用域链)
- father函数的作用域链(此时father函数的作用域链中只包含全局环境的作用域,并没有father函数自己的作用域) 注意:每个函数自己的作用域是在函数执行时才创建的,而函数的作用域链则是在函数所在的环境被执行是创建的。当函数被执行时,它自己的作用域才会被添加到已经创建的作用域链的头部。
- 当执行father函数时:
JS引擎也会为它创建上述四样东西:
- 一个属于它的变量对象(里面存着name、company、son)
- 一个属于它的作用域链 此时,这个作用域链中包含两个作用域,分别是:全局作用域 和 father函数的作用域,他们分别指向各自的变量对象;
- 一个属于他的执行环境,并将执行环境压入执行环境栈的栈顶;
- son函数的作用域链(其中包含father和全局环境的作用域) 此时,内存图如下:
- 当father函数执行结束: 当函数执行结束后,它的执行环境 和 作用域链 将会被销毁,而它的变量对象就取决于是否有引用指向它。 由于son函数的作用域链指向了father的变量对象,因此它仍然驻留在内存中。
- 当执行son函数时: 同样的,JS引擎也会为son函数依次创建这些东西,并将新建的son函数作用域压入作用域链的头部,此时内存图如下:
- 当执行alert(company)时: JS引擎会沿着son函数的作用域链依次查找变量对象中的值。 查找首先从son变量对象中开始,若该变量对象中没有,则沿着作用域链继续查找father变量对象,一旦找到,就停止。 因此,若查找name,首先就在son变量对象中找到,查找停止,因为无法访问到father中的name,除非在son函数中是否delete name将son中的name删除。
闭包的原理总结
综上所述,闭包之所以能访问其外层函数作用域中的变量,是因为闭包的作用域链中存在外层函数的变量对象。即使外层函数之行结束,但由于其变量对象仍然被内层函数的作用域引用,因此不会被内存回收,直到闭包执行结束后,外层函数的变量对象才会被回收。
闭包的特点
闭包访问外层函数变量的特点
若闭包在外层函数执行结束后执行,那么它只能获取到外层函数中所有变量的最终状态。
- 例1:
function father(){
var array = [];
for (var i=0; i<10; i++) {
array[i] = function(){
return i;
}
}
return array;
}
father函数执行后会返回一个包含闭包的数组,每个闭包都会返回i。 由于这里的闭包调用时,外层函数早就执行结束了,外层函数变量对象中i值已经变成了9,此时不管执行array中的哪个闭包,返回的结果都是9。 但是,如果在闭包外层函数执行过程中立即执行闭包,那么结果就不一样了,请看例2.
- 例2:
function father(){
var array = [];
for (var i=0; i<10; i++) {
array[i] = (function(){
return i;
})();
}
return array;
}
此时的闭包在外层函数执行时就立即执行,在那个时刻,闭包中i的值就是外层函数当前i的值,因此返回的array中存储的将是0-9. 但是,例子1中array存放的明明是闭包,而这里的array存放的却是闭包执行的结果,那么若仍想让array存储闭包,我们还需要稍稍改造,请看例3。
- 例3:
function father(){
var array = [];
for (var i=0; i<10; i++) {
array[i] = (function(){
var curI = i;
return function(){
return curI;
}
})();
}
return array;
}
我们让立即执行的闭包再返回一个闭包,并且将for循环中的i值赋给立即执行函数的局部变量,此时array中存储的将是闭包,并且每个闭包都拥有正确的值。
闭包中的this指向window
闭包的内存问题
闭包占用内存比普通函数大
因为如果一个函数内部有闭包存在,那么函数之行结束后不会释放自己的变量对象,只有当闭包执行结束后才会释放,因此闭包将会占用更大的内存空间,用于存储外层函数的变量兑现。 因此,能避免的情况下就不要使用闭包。
闭包中涉及DOM对象可能会出现内存泄漏
我们知道,JavaScript由ECMAScript、BOM、DOM组成,在某些浏览器中他们使用不同的语言实现,因此他们具有不同的垃圾回收机制。 ECMAScript对象采用标记清除算法回收内存,而某些浏览器的DOM对象采用引用计数算法回收内存。引用计数有个致命的缺点——无法回收循环引用的对象。 举个例子:如果一个DOM对象A中的属性a指向另一个DOM对象B,而B中有属性b指向对象A,那么这两个对象存在循环引用,垃圾回收机制就无法回收他们,这就造成了内存泄漏。 退一步将,只要循环引用的两个对象中存在一个DOM对象,就会导致内存泄漏,请看下面的例子:
function func () {
var dom = document.getElementById("xx");
dom.onclick = function(){
alert(dom.id);
}
}
这段代码获取了一个DOM对象,并且让这个对象的onclick属性指向了一个JS函数对象;而这个函数对象又指向了DOM对象的id属性,从而出现了循环引用,即使func函数执行完成,这个函数中定义的。由于这两个对象中存在一个DOM对象,因此就会出现内存泄漏。
如何避免?
要解决内存泄漏,我们只要破坏两个对象的相互引用即可。 上述代码要为dom添加一个点击事件,因此dom.onclick属性必须要指向一个JS函数对象,因此这个引用不能切断。 而第二个引用是由JS函数对象指向DOM对象的,目的是为了获取dom的id,我们可以通过如下代码切断这个引用:
function func () {
var dom = document.getElementById("xx");
var id = dom.id;
dom.onclick = function(){
alert(id);
}
}
- 柯洁宣布复出,再次迎战AI!但对手不再是阿尔法狗……
- 【Scikit-Learn 中文文档】协方差估计 / 经验协方差 / 收敛协方差 / 稀疏逆协方差 / Robust 协方差估计 - 无监督学习 - 用户指南 | ApacheCN
- 先搞懂这八大基础概念,再谈机器学习入门!
- 人工智能化的传感器技术
- 带有CSS3的动画3D条形图
- 有故事的微信小游戏“跳一跳”
- 机器学习敲门砖:任何人都能看懂的TensorFlow介绍
- 跟小编来体验一下微信小程序
- DeepVO:基于深度循环卷积神经网络的端到端视觉里程计
- 是人工智障还是蹭热度?我一定是抱走了假的二次元老婆
- 重庆日报:丹妮拉·鲁斯认为人工智能是人类的助手
- 深度学习系列(2):前向传播和后向传播算法
- 未来5年,什么样的人最抢手
- 腾讯AI Lab俞栋:AI的发展需要合作伙伴一起AI in ALL
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- PHP将整数数字转换为罗马数字实例分享
- PHP如何通过表单直接提交大文件详解
- PHP基于openssl实现的非对称加密操作示例
- python图片验证码识别最新模块muggle_ocr的示例代码
- virtualenv介绍及简明教程
- Keras 数据增强ImageDataGenerator多输入多输出实例
- TensorFlow中如何确定张量的形状实例
- 使用Dajngo 通过代码添加xadmin用户和权限(组)
- python和js交互调用的方法
- Python中flatten( ),matrix.A用法说明
- python中id函数运行方式
- CentOS 7如何实现定时执行python脚本
- PHP自动生成缩略图函数的源码示例
- 解决tensorflow 释放图,删除变量问题
- php生成word并下载代码实例