浅谈 JavaScript 中的闭包
JS 的闭包概念几乎是任何面试官都会问的问题,最近把闭包这块的概念梳理了一下,记录成以下文章。
什么是闭包?
我先列出一些官方及经典书籍等书中给出的概念,这些概念虽然表达的不一样,但是都在对闭包做了最正确的定义和翻译,也帮助大家更好的理解闭包,这阅读起来可能比较模糊,大家往后看,本文通过对多个经典书籍中的例子讲解,相信会让大家能很好的理解js中的闭包。文章开始,我会先铺垫一下闭包的概念和为什么要引入闭包的概念,然后结合例子来说明讲解,并讲解如何使用闭包。
百度百科中的定义
闭包包含自由(未绑定到特定对象)变量;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域) -- 百度百科
JavaScript 权威指南 中的概念
函数对象可以通过作用域链互相关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学中成为闭包
JavaScript 高级教程 中概念
闭包是指有权访问另一个函数作用域中的变量的函数。
个人总结的闭包概念
- 闭包就是子函数可以有权访问父函数的变量、父函数的父函数的变量、一直到全局变量。归根结底,就是利用js得词法(静态)作用域,即作用域链在函数创建的时候就确定了。
- 子函数如果不被销毁,整条作用域链上的变量仍然保存在内存中。
为什么引入闭包的概念
我引入《深入理解JavaScript系列:闭包(Closures)》文章中的例子来说明,也可以直接去看那篇文章,我结合其他书籍反复读了很多遍此文章才理解清楚。如下:
function testFn() {
var localVar = 10; // 自由变量
function innerFn(innerParam) {
alert(innerParam + localVar);
}
return innerFn;
}
var someFn = testFn();
someFn(20); // 30
一般来说,在函数执行完毕之后,局部变量对象即被销毁,所以 innerFn 是不可能以返回值形式返回的,innerFn 函数作为局部变量应该被销毁才对。
这是当函数以返回值时的问题,另外再看一个当函数以参数形式使用时的问题,还是直接引用《深入理解 JavaScript 系列》中的例子,也方便大家有兴趣可以直接去阅读那篇文章
var z = 10;
function foo() {
alert(z);
}
foo(); // 10 – 使用静态和动态作用域的时候
(function () {
var z = 20;
foo(); // 10 – 使用静态作用域, 20 – 使用动态作用域
})();
// 将foo作为参数的时候是一样的
(function (funArg) {
var z = 30;
funArg(); // 10 – 静态作用域, 30 – 动态作用域
})(foo);
当函数 foo 在不同的函数中调用,z 该取哪个上下文中的值呢?这就又是一个问题,所以就引入了闭包的概念,也可以理解为定义了一种规则。
理解闭包
函数以返回值返回
看一个《JavsScript 权威指南》中的一个例子,我稍微做一下修改如下:
var scope = 'global scope';
function checkScope() {
var scope = 'local scope';
return function() {
console.log(scope);
}
}
var result = checkScope();
result(); // local scope checkScope变量对象中的scope,非全局变量scope
分析:
即使匿名函数是在 checkScope 函数外调用,也没有使用全局变量 scope,即是利用了 js 的静态作用域,当匿名函数初始化时,就创建了自己的作用域链,其实当把作用域链理解好了之后,闭包也就理解了,此匿名函数的作用域链包括 checkScope 的活动对象和全局变量对象,当 checkScope 函数执行完毕后,checkScope 的活动对象并不会被销毁,因为匿名函数的作用域链还在引用 checkScope 的活动对象,也就是 checkScope 的执行环境被销毁,但是其活动对象没有被销毁,留存在堆内存中,直到匿名函数销毁后,checkScope 的活动对象才会销毁,解除对匿名函数的引用将其设置为 null 即可,垃圾回收将会将其清除,另外当外部对 checkScope 的自由变量存在引用的时候,其活动对象也不会被销毁。
result = null; //解除对匿名函数的引用
注释:
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
函数以参数形式使用
当函数以参数形式使用时一般用于利用闭包特性解决实际问题,比如浏览器中内置的方法等,下面我直接引用《深入理解 JavaScript 系列:闭包(Closures)》中关于闭包实战部分的例子如下:
sort
在 sort 的内置方法中,函数以参数形式传入回调函数,在 sort 的实现中调用:
[1, 2, 3].sort(function (a, b) {
... // 排序条件
});
map
和 sort 的实现一样
[1, 2, 3].map(function (element) {
return element * 2;
}); // [2, 4, 6]
另外利用自执行匿名函数创建的闭包
var foo = {};
// 初始化
(function (object) {
var x = 10;
object.getX = function() {
return x;
};
})(foo);
alert(foo.getX()); // 获得闭包 "x" – 10
利用闭包实现私有属性的存取
先来看一个例子
var fnBox = [];
function foo() {
for(var i = 0; i < 3; i++) {
fnBox[i] = function() {
return i;
}
}
}
foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
var fn2 = fnBox[2];
console.log(fn0()); // 3
console.log(fn1()); // 3
console.log(fn2()); // 3
用伪代码来说明如下:
fn0.[[scope]]= {
// 其他变量对象,一直到全局变量对象
父级上下文中的活动对象AO: [data: [...], i: 3]
}
fn1.[[scope]]= {
// 其他变量对象,一直到全局变量对象
父级上下文中的活动对象AO: [data: [...], i: 3]
}
fn2.[[scope]]= {
// 其他变量对象,一直到全局变量对象
父级上下文中的活动对象AO: [data: [...], i: 3],
}
分析:
这是因为 fn0、fn1、fn2 的作用域链共享 foo 的活动对象,而且 js 没有块级作用域,当函数 foo 执行完毕的时候 foo 的活动对象中 i 的值已经变为 3,当 fn0、fn1、fn2 执行的时候,其最顶层的作用域没有i变量,就沿着作用域链查找 foo 的活动对象中的 i,所以 i 都为3。
但是这种结果往往不是我们想要的,这时就可以利用认为创建一个闭包来解决这个问题,如下:
var fnBox = [];
function foo() {
for(var i = 0; i < 3; i++) {
fnBox[i] = (function(num) {
return function() {
return num;
}
})(i);
}
}
foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
var fn2 = fnBox[2];
console.log(fn0()); // 0
console.log(fn1()); // 1
console.log(fn2()); // 2
用伪代码来说明如下:
fn0.[[scope]]= {
// 其他变量对象,一直到全局变量对象
父级上下文中的活动对象AO: [data: [...], k: 3],
fn0本身的活动对象AO: {num: 0}
}
fn1.[[scope]]= {
// 其他变量对象,一直到全局变量对象
父级上下文中的活动对象AO: [data: [...], k: 3],
fn1本身的活动对象AO: {num: 1}
}
fn2.[[scope]]= {
// 其他变量对象,一直到全局变量对象
父级上下文中的活动对象AO: [data: [...], k: 3],
fn2本身的活动对象AO: {num: 2}
}
分析:
当使用自执行匿名函数创建闭包,传入i的值赋值给 num,由于作用域链是在函数初始化时创建的,所以当每次循环时,函数 fn10、fn1、fn2 的作用域链中保存了当次循环是 num 的值,当 fn10、fn1、fn2 调用时,是按照本身的作用域链进行查找。
总结
从理论的角度将,由于 js 作用域链的特性,js 中所有函数都是闭包,但是从应用的角度来说,只有当函数以返回值返回、或者当函数以参数形式使用、或者当函数中自由变量在函数外被引用时,才能成为明确意义上的闭包。
最后,我想表达的是本篇大量引用和罗列了经典的犀牛书《JavaScript 权威指南》、红宝书《JavaScript 高级教程》、以及《深入理解 JavaScript 系列:闭包(Closures)》系列文章中的概念和例子,不为能形成自己的独特见解,只为了能把闭包清晰的讲解出来。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论