目录
闭包
在正常情况下,函数(函数作用域)中是可以访问外部变量的,而外部是无法访问函数作用域中的变量。
<script>
function fn() {
var n = 10;
console.log(n);
}
fn();
console.log(n); // 报错
</script>
当外部去访问函数中的变量时,会报
Uncaught ReferenceError: n is not defined。
那么现在有这个需求:希望外部可以访问到函数中的变量,如何实现?
这时就需要使用闭包。
一、外部访问局部变量方法
要想从外部读取局部变量,正常情况下是不可能实现的,只有通过变通方法才能够实现
——
那就是在函数的内部,再定义一个函数。
<script>
function fn1() {
var n = 10;
function fn2() {
// console.log(n);//10
return n; //10
}
var r = fn2();
// console.log(r);//10
return r; //10
}
var rs = fn1();
console.log(rs); //10
</script>
执行流程:
首先执行(
var
rs
=
fn1
();
),进入fn1(),再执行(
var
r
=
fn2
();
),进入fn2(),返回n,n是内部,可以访问外部,得到n=10,所以fn2()返回10,10再赋值给r,fn1()返回10,10再赋值给rs,因此输出的rs等于10。
通过在函数的内部再定义一个函数来计算后把结果返回给调用来实现,这种方式就是闭包。
上面代码可以写为:
<script>
function fn1() {
var n = 10;
return function fn2() {
return n;
}
}
var rs = fn1();
console.log(rs());
</script>
在上面的代码中,函数
fn2
就被包括在函数
fn1
内部,这时
fn1
内部的所有局部变量,对
fn2
都是可见的。但是反过来就不行,fn2
内部的局部变量,对
fn1
就是不可见的。这就是
Javascript
语言特有的"
链式作用域
"
结构(
chain scope
),子对象会一级一级地向上寻找所有父对象的变量。 所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然
fn2
可以读取
fn1
中的局部变量,那么只要把
fn2
作为返回值,我们就可以在
fn1
外部读取它的内部变量了!
二、闭包的概念
上面代码中的
fn1
函数,就是闭包。
各种专业文献上的
"
闭包
"
(
closure
)定义非常抽象,很难看懂。简单理解就是:
闭包就是能够读
取其他函数内部变量的函数
。
由于在
Javascript
语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 "
定义在一个函数内部的函数
"
。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包是
JavaScript
中最强大的特性之一。
JavaScript
允许函数嵌套,并且内部函数可以访问定义在外部函数中的所有变量和函数,以及外部函数能访问的所有变量和函数。
《
Javascript
高级程序设计》
:闭包其实是一种语言特性,它是指的是程序设计语言中,允许将
函数
看作对象,然后能像在对象中的操作般在函数中定义实例(局部)变量,而这些变量能在函数中保存到函数的实例对象销毁为止,其它代码块能通过某种方式获取这些实例(局部)变量的值并进行应用扩展。
三、闭包的作用
闭包可以用在许多地方。它的最大用处有两个:一个是前面提到的可以读取函数内部的变量,另一个是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下面的代码。
<script>
function f1() {
var n = 9;
add = function() {
n += 1;
};
//因为没有定义这个add变量,他就是全局变量
function f2() {
console.log(n);
}
return f2;
}
var r = f1();
//console.log(r);
r(); //9 r()x相当于f1的立即执行函数
add();
r(); //10
r(); //10
r(); //10
add();
r();//11
r();//11
</script>
以上代码执行过程如下:
首先执行(r();),进入f1()函数,再执行(var n = 9;),执行(return f2; ),进入f2()函数,内部可以访问外部,因而得到n=9,再输入结果9,接下来执行(add();),执行后n=10,再执行(r();),进入f1()函数,再执行(var n = 9;),执行(return f2; ),进入f2()函数,因为之前通过add()函数,n的值为10了,所以这次返回的结果就是10。
在这段代码中,
r
实际上就是闭包
f2
函数。它一共运行了两次,第一次的值是
9
,第二次的值10。这证明了,函数
f1
中的局部变量
n
一直保存在内存中,并没有在
f1
调用后被自动清除。
为什么会这样呢?原因就在于
f1
是
f2
的父函数,而
f2
被赋给了一个全局变量,这导致
f2
始终在内存中,而 f2
的存在依赖于
f1
,因此
f1
也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection
)回收。
这段代码中另一个值得注意的地方,就是
"add=function(){n+=1}"
这一行,首先在
add
前面没有使用 var
关键字,因此
add
是一个全局变量,而不是局部变量。其次,
add
的值是一个匿名函数(anonymous function
),而这个匿名函数本身也是一个闭包,所以
add
相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
四、用闭包做一个计数器
通过闭包的数据共享的特性来做一个计数器。
<script>
function fn() {
var start = 0;
return function() {
return ++start;
};
}
var inc = fn();
//console.log(inc);
console.log(inc()); // 1
console.log(inc()); // 2
console.log(inc()); // 3
console.log(inc()); // 4
inc = null; // 释放内存
</script>
五、闭包封装对象的私有属性和方法
闭包还可以用来封装对象的私有属性和方法。如下例:
<script>
function Person() {
// 私有属性
var age;
// 私有方法
function getAge() {
return age;
}
function setAge(n) {
age = n;
}
return {
setAge: setAge,
getAge: getAge
}
}
var p = new Person();
//console.log(p.age);
p.setAge(5);
console.log(p.getAge()); //5
p = null;
</script>
六、闭包使用注意事项
(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
闭包需要三个条件:
(1)函数嵌套
(2)访问所在的作用域
(3)在所在作用域外被调用
七、闭包应用场景
7.1、返回值
这是最常见的一种方式。
<script>
function fn() {
var name = '天子';
return function() {
return name;
};
}
//var r = fn()();
var f = fn();
var r = f();
console.log(r);
</script>
7.2、函数赋值
<script>
var f;
function fn() {
var name = '天子';
var fun = function() {
return name;
};
f = fun;
}
console.log(fn()); //undefined 未调用fun
console.log(f()); //天子
</script>
7.3、函数参数
把一个函数作为参数传递给另一个函数,这种方式叫高阶函数。
<script>
function fn(f) {
console.log(f());
}
function fn1() { // fn1 也是一个闭包
var name = '天子';
var fun = function() {
return name;
};
fn(fun);
}
fn1();
</script>
7.4、自执行函数
<script>
function fn(f) {
console.log(f());
}
(function fn1() { // fn1 也是闭包
var name = '天子';
var fun = function() {
return name;
};
fn(fun)
})();
</script>
7.5、循环赋值
<script>
function fn() {
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = (function(n) {
return function() {
return n + 5;
};
})(i);
}
return arr;
}
var f = fn();
console.log(f[0]());
console.log(f[1]());
</script>