闭包是函数和声明该函数的词法环境的组合
一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因为这些变量也是该表达式的一部分。闭包的特点:
- 作为一个函数变量的一个引用,当函数返回时,其处于激活状态;
- 一个闭包就是当函数返回时,一个没有释放资源的栈区。
简单的说,JavaScript允许使用内部函数,即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问他们所在的外部函数汇总声明的所有变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包裹它们的外部函数之外被调用时,就会形成闭包。
说到这里,下面就先说说词法作用域。
词法作用域
运行下面的代码:
var age = 28; // age 是一个全局变量
function init() {
var name = "Guohh"; // name 被局部变量init函数创建
function displayName() { // displayName()是一个内部函数,一个闭包
alert (name); // displayName() 调用了父函数中的变量
alert (age); // displayName() 调用了全局中的变量
}
displayName();
}
init();
运行代码,可以发现函数displayName()
内部调用了父函数中的变量和全局变量。这个例子介绍了浏览器引擎是如何解析函数嵌套中的变量。词法作用域中使用的域,是变量在代码声明的位置所决定的。嵌套函数可以访问器外部声明的变量。
什么是闭包
在运行下面这一段代码:
function init() {
var name = "Guohh";
function displayName() {
alert (name);
}
return displayName;
}
var ini = init();
ini();
这段代码和词法作用域的结果一致,不同的就是init()
函数将displayName()
返回。在词法作用域中我们会认为name
变量在init()
函数执行完毕后将不能被访问,但是因为变量提升,我们将获得到undefined
,但真实的结果却不是这样的。那么为什么呢?
JavaScript中的函数会形成闭包。闭包是有函数以及创建函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。
既然知道什么是闭包,那么下面就来说一下它的几种用法。
闭包的用途
立即执行函数IIFE
在我们翻读jQuery
源码时,发现它开篇用的就是立即执行函数。立即执行函数常用于第三方库,好处在于隔离作用域,任何一个第三方库都会存在大量的变量和函数,为了避免变量污染(命名冲突)。
写法
-
在函数外边后加括号,并被括号包裹
(function(){ // 执行 }())
-
在函数外边包裹括号,并在后边添加括号
(function(){ // 执行 })()
-
函数前面加运算符,创建的是
!
和void
!function(){ // 执行 }() void function(){ // 执行 }()
参数
立即执行函数式可以访问到外部变量,很多情况下并不需要传递参数。当我们看jQuery
源码时,就会看到如下代码块:
(function(window) {})()
其实在jQuery
中也可以传,内部依然能使用,但这里传递的原因就是在函数内部对window的操作不影响全局的window。
返回值
立即执行函数和其他函数一样,它也能返回值并且赋值给其他变量。
好处
-
立即执行函数模式被广泛使用了,它可以帮你封装大量的工作而不会在背后遗留任何全局变量。
-
定义的所有变量都会成为立即执行函数的局部变量,避免这些临时变量污染全局空间。
-
这种模式经常被使用在书签工具中,因为书签工具在任何页面运行并且保持全局命名空间干净是非常必要的。
-
这种模式也可以让你将独立的功能封装在自包含模块中。
-
可以将这些代码封装在一个立即执行函数中,并且确保页面在没有它的情况下正常运行。
-
可以添加更多的加强模块,移除他们,单独测试他们,允许用户去禁用它们等。
注意:当将立即执行函数左右单独模块使用是在函数前面添加分号,这样可以有效地与前面的代码出现隔离。
模块化
在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。比如Java,是支持方法声明为私有的,即它们只能被同一个类中的其他方法所调用。
在Web中,我们大部分所写的JavaScript代码都是基于事件的某种行为,然后将其添加到用户触发的事件之上。我们的代码通常作为回调:为相应事件而执行的函数。而JavaScript中,却没有原生的方法来支持像面向对象中的私有方法的声明,但是我们可以通过闭包来实现模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码公共接口部分。这个方式也叫做模块化。
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
上面的代码我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量counter
。我们可以把这个函数储存在另外一个变量makeCounter
中,并用他来创建多个计数器。每个闭包(计时器)都是引用自己词法作用域内的变量 privateCounter
。每次调用其中一个计数器是,改变变量的值,只会改变这个闭包的词法环境,而不会影响另外一个闭包内中的变量。
以这种方式使用闭包,提供了许多与面向对象编程相关的好处:数据的隐藏和封装。
实现类和继承
function Person(){
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
};
var p = new Person();
p.setName("Tom");
alert(p.getName());
var Jack = function(){};
//继承自Person
Jack.prototype = new Person();
//添加私有方法
Jack.prototype.Say = function(){
alert("Hello,my name is Jack");
};
var j = new Jack();
j.setName("Jack");
j.Say();
alert(j.getName());
闭包中常见的问题
-
for
循环会形成闭包,解决方案是:函数工厂、匿名闭包、用let
代替var
进行变量的声明。 - 引用的外部变量可能会发生改变。
- this执行问题
- 内存泄露
注意
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能会大致内存泄露解决方法是:在退出函数之前,将不适用的比变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以如果你把父函数当做对象使用,把闭包当做他的公用方法,把内部变量当做他的私有属性,这是一定注意不要随便改变函数内部变量的值。
网友评论