1. 闭包的概念
- MDN:
- 函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
- 闭包是由函数以及声明该函数的词法环境组合而成的。
- 阮一峰老师:
- 闭包就是能够读取其他函数内部变量的函数。可以把闭包简单理解成***"定义在一个函数内部的函数"***
总结下来就是闭包是定义在一个函数内部的函数,可以从内部函数访问外部函数作用域,它是由函数以及声明该函数的词法环境组合而成的,包含被引用变量 or 函数的对象
2. 变量的作用域
首先明确JavaScript中变量的作用域。
分为局部变量和全局变量
//Javascript的函数,在内部可以直接读取全局变量。
var n=999;
function f1(){
alert(n);
}
f1(); // 999
//另一方面,在函数外部自然无法读取函数内的局部变量。
function f1(){
var n=999;
}
alert(n); // error
//这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
function f1(){
n=999;
}
f1();
alert(n); // 999
/*
于是,为了在外部读取局部变量,我们在函数中再返回一个函数
result 是执行 f1 时创建的 f2 函数实例的引用。f2 的实例维持了一个对它的词法环境(变量 n 存在于其中)的引用。
通过调用这个实例,实现了对函数内部变量的访问
*/
function f1(){
var n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
作用域链
作用域链:内部函数访问外部函数的变量,采用的是链式查找的方式来决定取哪个值,这种结构称之为作用域链。查找时,采用的是就近原则。
var num = 10;
function fn() {
// 外部函数
var num = 20;
function fun() {
// 内部函数
console.log(num);
}
fun();
}
fn();
//结果为20
3. this
this指的是,调用函数的那个对象。this永远指向函数运行时所在的对象。
-
以函数的形式调用时,this永远都是window。比如fun();相当于window.fun();
-
以方法的形式调用时,this是调用方法的那个对象
-
以构造函数的形式调用时,this是新创建的那个对象
-
使用call和apply调用时,this是指定的那个对象
一般的定义函数是运行的时候决定this的指向。箭头函数中的this是在定义函数的时候绑定,而不是在执行函数的时候绑定。箭头函数没有自己的this,箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。当对箭头函数使用call()和apply()方法时对函数内的this没有影响。箭头函数会从自己的作用域链的上一层继承this
4. 闭包的用法
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。
/*
可以利用闭包,将具有不同参数的同一功能分别用一个全局变量引用
add5和add10其实就是闭包function(y)
原因就在于makeAdder是function(y)的父函数,而function(y)被赋给了一个全局变量,这导致function(y)始终在内存中,而function(y)的存在依赖于makeAdder,因此makeAdder也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
*/
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
/*
编程语言中,比如 Java,是支持将方法声明为共有或者私有(public、private)的,即它们只能被同一个类中的其它方法所调用。
而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。这种方式可称为模块模式
*/
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
//或者不声明为自调用函数
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 */
应用举例
(1)myModule.js:(定义一个模块,向外暴露多个函数,供外界调用)
function myModule() {
//私有数据
var msg = 'Smyhvae Haha'
//操作私有数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase()); //字符串大写
}
function doOtherthing() {
console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写
}
//通过【对象字面量】的形式进行包裹,向外暴露多个函数
return {
doSomething1: doSomething,
doOtherthing2: doOtherthing
}
}
上方代码中,外界可以通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到。
(2)index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>05_闭包的应用_自定义JS模块</title>
</head>
<body>
<!--
闭包的应用 : 定义JS模块
* 具有特定功能的js文件
* 将所有的数据和功能都封装在一个函数内部(私有的)
* 【重要】只向外暴露一个包含n个方法的对象或函数
* 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule();
module.doSomething1();
module.doOtherthing2();
</script>
</body>
</html>
方式二
同样是实现方式一种的功能,这里我们采取另外一种方式。
(1)myModule2.js:(是一个立即执行的匿名函数)
(function () {
//私有数据
var msg = 'Smyhvae Haha'
//操作私有数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase())
}
function doOtherthing() {
console.log('doOtherthing() ' + msg.toLowerCase())
}
//外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象
window.myModule = {
doSomething1: doSomething,
doOtherthing2: doOtherthing
}
})()
(2)index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>05_闭包的应用_自定义JS模块2</title>
</head>
<body>
<!--
闭包的应用2 : 定义JS模块
* 具有特定功能的js文件
* 将所有的数据和功能都封装在一个函数内部(私有的)
* 只向外暴露一个包信n个方法的对象或函数
* 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->
<!--引入myModule文件-->
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
myModule.doSomething1()
myModule.doOtherthing2()
</script>
</body>
</html>
5. 闭包的作用
由上可见,闭包的作用主要有两个:
-
作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
-
作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
隐藏局部变量,暴露操作函数
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2;
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
const createAdd = ()=>{
let n = 0
return ()=>{
n += 1
console.log(n)
}
}
const add = createAdd()
add() // 1
add() // 2
6. 闭包的注意点
-
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
-
闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。