JavaScript 闭包全方位解析

Posted Atlantis

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 闭包全方位解析相关的知识,希望对你有一定的参考价值。

总结

概念: 有权访问另一个函数作用域中的变量的函数

优点: 内存驻留、避免全局变量污染

缺点: 内存泄漏(?)、无法预知变量被更改

相关知识点: 作用域、内存驻留、内存泄露、JS执行机制、内存机制、垃圾回收机制

一、什么是闭包

1、作用域链

要理解什么是闭包、首先我们要对JS中的作用域作用域链有一定的理解:

作用域: 简单来讲就是一个变量能够被访问的范围,JS有三种作用域,分别是:全局作用域、函数作用域、块级作用域

作用域链: 在JS中作用域是一层一层嵌套的, 子作用域中可以访问父作用域中的变量, 我们把这种可以一层一层向上访问的链式结构叫做作用域链.

~~ 当JS创建一个函数时, 首先会创建一个预先包含全局变量对象的作用域链, 保存在内部的Scope属性之中
当JS调用这个函数时, 会为此函数创建一个执行环境, 也就是函数上下文
然后复制函数的Scope的属性中的对象构建执行环境的作用域链~~

作用域与作用域链就好比是数学中的集合, 最大的便是全局作用域, 子集便是函数作用域, 子集中又可以有子集,所有的自己都可以向外访问,但所有的父级不可以向子集访问,相同的子集之间也不可以互相访问。

2、闭包的概念

有权访问另一个函数作用域中的变量的函数 《javascript高级程序设计》

我们来理解一下这句话

  • 首先: 闭包是...函数
  • 然后: 有权访问另一个函数作用域中的变量

那么怎么才有权访问另一个函数作用域中的变量呢?
根据上文中子作用域中可以访问父作用域中的变量的特性,答案是: 成为另一个函数的子函数

补充一点:

在《JavaScript权威指南》, 强调了函数体内部变量可以保存在函数作用域 函数对象可以通过作用域链相互关联起来,函数体内部变量可以保存在函数作用域内,这就是闭包。

3、闭包的结构

从严格的角度来讲, 闭包需要满足三个必要的条件:

  1. 函数嵌套
  2. 访问父函数作用域中的变量
  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 闭包全方位解析的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 作用域(链)预解析闭包函数

关于 JavaScript 函数式编程的全方位解析

JS---闭包

Javascript学习笔记:闭包题解

深入解析Javascript闭包及实现方法

javascript 闭包解析