JavaScript 闭包全方位解析
Posted Atlantis
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 闭包全方位解析相关的知识,希望对你有一定的参考价值。
总结
概念: 有权访问另一个函数作用域中的变量的函数
优点: 内存驻留、避免全局变量污染
缺点: 内存泄漏(?)、无法预知变量被更改
相关知识点: 作用域、内存驻留、内存泄露、JS执行机制、内存机制、垃圾回收机制
一、什么是闭包
1、作用域链
要理解什么是闭包、首先我们要对JS中的作用域
和作用域链
有一定的理解:
作用域: 简单来讲就是一个变量能够被访问的范围,JS有三种作用域,分别是:全局作用域、函数作用域、块级作用域
作用域链: 在JS中作用域是一层一层嵌套的, 子作用域中可以访问父作用域中的变量, 我们把这种可以一层一层向上访问的链式结构叫做作用域链.
~~ 当JS创建一个函数时, 首先会创建一个预先包含全局变量对象的作用域链
, 保存在内部的Scope
属性之中
当JS调用这个函数时, 会为此函数创建一个执行环境
, 也就是函数上下文
然后复制函数的Scope
的属性中的对象构建执行环境的作用域链
~~
作用域与作用域链就好比是数学中的集合, 最大的便是全局作用域, 子集便是函数作用域, 子集中又可以有子集,所有的自己都可以向外访问,但所有的父级不可以向子集访问,相同的子集之间也不可以互相访问。
2、闭包的概念
有权访问另一个函数作用域中的变量的函数 《javascript高级程序设计》
我们来理解一下这句话
- 首先: 闭包是...函数
- 然后: 有权访问另一个函数作用域中的变量
那么怎么才有权访问另一个函数作用域中的变量呢?
根据上文中子作用域中可以访问父作用域中的变量
的特性,答案是: 成为另一个函数的子函数
补充一点:
在《JavaScript权威指南》, 强调了函数体内部变量可以保存在函数作用域 函数对象可以通过作用域链相互关联起来,函数体内部变量可以保存在函数作用域内,这就是闭包。
3、闭包的结构
从严格的角度来讲, 闭包需要满足三个必要的条件:
- 函数嵌套
- 访问父函数作用域中的变量
- 在函数声明作用域外被调用( 有异议,欢迎讨论 )
因此我们猜想一个闭包的样子, 大概应该是这样的:
// 全局变量-全局作用域
var global = "global scope";
function partner() {
// 局部变量-函数作用域
var variable = \'Function scope\';
function children() {
console.log(variable);
}
}
// 此时子函数 children 访问了父函数 partner, 我们就称子函数 children 为闭包.
二、闭包存在的意义
意义: 内存驻留
当我们要实现一个计数器时, 首先用常规的方法来写:
// 计数器
var count = 0;
function counter() {
console.log(count++);
}
counter(); // 1
counter(); // 2
上面的代码已经实现了我们所需的功能, 但是它并不完美, 一方面全局的count
变量可能造成变量污染, 另一方面代码中的任何一个位置都可以轻松的修改这个count的值, 这是我们所不能接受的!
因此, 我们需要一个变量可以在counter
函数中访问, 但它并不在全局作用域中, 且可以长时间的停留在内存当中不被浏览器的垃圾回收机制
清除, 于是我们就想到了闭包, 接下来我们用闭包再实现一下计数器:
// 计数器
var counter = (function() {
var count = 0;
return function() {
console.log(count++);
}
})()
counter(); // 1
counter(); // 2
闭包仿佛结合了全局作用域与局部作用域的优点与一身,对于其他函数作用域而言,父函数作用域中的变量就像是父函数和闭包的一个 “私有变量” , 而对于父函数和闭包而言, 父函数作用域中的变量又好像身处 “全局作用域” 中.
三、闭包造成的影响
缺陷: 影响性能、变量修改
影响性能:
闭包的存在会导致函数中得变量一直存在于内存中,不能被垃圾回收机制清理, 导致内存消耗增加, 影响系统运行的性能,所以不能滥用闭包.
如果要使用闭包, 应该在使用结束时手动的清除闭包!
变量修改:
在闭包存在的时候,将父函数比做一个类,父函数中得局部变量就是类的私有属性,而闭包访问的变量就是类的公共属性,在父函数作用域和闭包函数中都可以对 variable 变量进行修改, 这是件令人头疼的事情, 因为你并不知道有多少闭包会在什么时候对你的变量进行修改, 这将造成你的程序极不稳定甚至执行异常.
四、闭包的经典案例
[操作内部变量]返回函数内部变量
function parent() {
let name = \'sf\'
return {
get() { return name },
set(val) { name = val }
}
}
const nameProxy = parent()
console.log(nameProxy.get()) // \'sf\'
// 子函数children也可以定义为null在全局,然后在parent中赋值
[锁定变量状态]for循环中的定时器
// 因为setTimeout是异步的, 代码执行先同步后异步, 所以当它执行的时候for循环已经结束了
for(var i=0,len=10; i<len; i++) {
setTimeout(() => console.log(i), 1);
}
// 10个10
// 闭包写法-IIFE
for(var i=0,len=10; i<len; i++) {
((i) => {
setTimeout(() => console.log(i), 1);
})(i)
}
[制造安全环境]完全封闭的功能模块-IIFE
// 内部的变量不会污染全局变量, 可以放心的对多个模块进行合并
(function(window) {
let a = 1
let b = \'2\'
function add() {
a++
}
})(window)
五、闭包的底层原理
执行父函数时, JS线程会对内部的子函数进行预编译, 看一看子函数中是否用到了父函数的内部变量
如果用到了, 为了保证在未来调用子函数时不出错, JS线程会在父函数执行完毕之后, 清空函数执行栈中的上下文之前, 将父函数中被用到的变量 copy 一份放在堆中, 供之后子函数引用.
六、其他相关的扩展
内存泄露
内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。
在闭包造成的影响中,我们经常会听到一句话, 那便是:在IE中闭包的使用可能会导致内存泄漏。但是,在我学习V8引擎的过程中发现,引发内存泄漏的原因似乎是循环引用,这让我对闭包和内存泄漏的关系产生了疑惑.
后续我将围绕内存泄漏
重新整理一篇文章,这里先引用一篇文章中的一句话和一个例子:
在IE浏览器中,由于BOM和DOM中的对象是使用C++以COM对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。
作者:前端小学生\\_f675
链接:https://www.jianshu.com/p/66881ba3c8ba
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 内存泄漏
var ele = document.getElementById("someElement");
ele.click = function() {
console.log(ele.id);
}
// 解决方案:使用结束后释放内存
var ele = document.getElementById("someElement");
var eleID = ele.id;
ele.click = function() {
console.log(eleID);
}
ele = null;
垃圾回收
文章地址: 待更新
七、闭包的相关文章
以上是关于JavaScript 闭包全方位解析的主要内容,如果未能解决你的问题,请参考以下文章