闭包 closure
什么是闭包?
闭包也是一个对象。
作用: 闭包是既重用变量又保护变量不被污染的一种编程方法。使用闭包主要是为了设计私有的方法和变量。
本质: 外层函数调用后,外层函数的作用域对象,被返回的内层函数的作用域链引用着,无法释放,就形成闭包对象
使用场景: 希望给一个函数,保存一个可反复使用,又不被外界污染的专属局部变量时,就用闭包
优点: 避免全局变量的污染,且局部变量可以重复使用。
缺点: 由于闭包藏得很深几乎找不到,且常驻内存。使用不当,会增大内存使用量,造成内存泄漏!
解决内存泄漏:及时释放不用的闭包
特性:
- 函数嵌套函数
- 函数内部可以引用外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
如何释放闭包对象?
将保存内层函数对象的变量赋值为 null,导致函数名变量与内层函数对象分开。
内层函数对象没人要了,会被 js 引擎自动释放。
内层函数引用的闭包对象,也没人要了,闭包对象就被释放了。
最后,内存恢复干净的初始状态
答题技巧:
- 看程序,判断输出结果
- 找外层函数
- 找外层函数的局部变量
- 找内层函数(多个)
- 根据需求,使用闭包实现功能
为什么需要闭包?
举例:
定义一个函数帮小孩儿管理压岁钱
- 假设小孩儿共有 1000 元压岁钱,存哪儿?
- 小孩儿每花一笔钱,就从总钱数里减去一笔。
var total = 1000 function pay(money) { total -= money console.log(`花了${money}还剩${total}元`) } pay(100) // 花了100还剩900元 pay(100) // 花了100还剩800元
问题:假设别人的程序中也用到了 total 变量,并在你调用 pay() 之前修改了 total 变量值,结果会怎样?
var total = 1000
function pay(money) {
total -= money
console.log(`花了${money}还剩${total}元`)
}
pay(100) // 花了100还剩900元
total = 0 // 别人程序执行了代码
pay(100) // 花了100还剩-100元
执行完以上代码,小孩儿的钱 -100 了。这是因为 全局变量特点:可重用,但是极易被污染
解决:换局部变量
// var total=1000;// 易被污染
function pay(money) {
var total = 1000 // 改为局部变量
total -= money
console.log(`花了${money}还剩${total}元`)
}
pay(100) // 花了100还剩900元
total = 0 // 别人程序执行了代码
pay(100) // 花了100还剩900元
还是有问题,为什么 total 不变?
这是因为 局部变量特点:不会被污染,但是用完就释放!不可重用!
解决:闭包(closure)
浅显的从用法上理解:闭包是既重用变量又保护变量不被污染的一种编程方法。
今后:只要希望给一个函数,保存一个即可反复使用,又不会被外界污染的专属局部变量时,就用闭包
// 第1步: 用外层函数包裹要保护的变量和内层函数
function mother() {
var total = 1000
// 第2步: 返回内层函数对象
// 内层函数,将来迟早要被别的变量接住。所以,起不起名,结果是一样的!那干脆我们就不起函数名了!
return function (money) {
total -= money
console.log(`花了${money}还剩${total}元`)
}
}
// 第3步: 调用外层函数,用变量接住内层函数对象
var pay = mother() // 被 pay 变量接住
// pay 接住的就是 mother() 返回出来的内层函数对象
pay(100) // 花了100还剩900元
// 假设别人也有total, 且别人程序执行了代码 total = 0,现在没有影响后边的total计算
// 说明 mother 里面的 total 被保护了,只能pay函数能够使用 mother 里面的 total
total = 0
pay(100) // 花了100还剩800元;能够连续减,说明 mother 里面的 total 变量被反复使用了
闭包三部曲:
- 用外层函数包裹要保护的变量和使用变量的内层函数
外层函数的局部变量由该外层函数的多个内层函数共用
外层函数多次调用,内层函数的局部变量被多次生成,但是互不影响 - 在外层函数内部返回内层函数对象
- return 内层函数
- 将内层函数强行赋值给全局变量
- 将内层函数包裹在对象或数组中返回
- 调用外层函数用变量接住返回的内层函数对象。
使用闭包三步曲,可以为函数保存一个既可重用,又不会被污染的专属局部变量。
代码分析:
程序开始执行前:
-
词法作用域
-
创建函数对象
mother
,保存函数体 -
再创建
pay
变量,值为 undefined -
每个函数对象身上都有一个作用域链,暂时只保存全局作用域对象 window
-
程序运行:
-
pay = mother()
,调用mother
函数-
词法作用域分析
- JS 引擎临时创建
mother
函数作用域对象 mother
函数内部创建total
局部变量
- JS 引擎临时创建
-
mother
函数体语句顺序执行total = 1000
return new Function() {}
new Function() {}
,内层函数,暂时不执行函数体的内容!函数只有加()
被调用时,才执行函数体!- 返回内层函数,
pay
指向内层函数地址
-
mother
函数调用后,释放函数mother 函数调用后,释放函数,离 mother 函数最近的作用域被清空,此时作用域只有一个 window 作用域对象。mother 清掉了指向 mother 作用域对象的地址。
但是 mother 作用域对象的地址还被内层函数 pay 的作用域链引用着,无法释放(mother 作用域对象实际上还没有被释放)。可以在内层函数 pay 函数调用时看到,并且此时 mother 作用域对象只有 pay 知道存储在哪里。——专属私密
注意: mother 函数调用后,mother 外层函数就与 pay 内层函数没有关系了。
-
-
调用
pay
函数-
词法作用域分析
- JS 引擎临时创建 pay 函数作用域对象
pay
函数内部创建money
局部变量,值为 100
-
按照 mother 函数体语句顺序执行
total -= money
- 查找
money
,在pay
函数内部找到局部变量money
,且值为 100 - 查找
total
在外部函数 Closure (mother) 作用域链中找到,且值为 1000 - 执行
-=
运算,total
减去money
,将 Closure (mother) 作用域链中 total 变为 900
- 查找
console.log(`花了${money}还剩${total}元`)
- 查找
money
,在pay
函数内部找到局部变量money
,且值为 100 - 查找
total
在外部函数 Closure (mother) 作用域链中找到,且值为 900 - 执行语句,输出:花了100还剩900元
- 查找
所以,闭包结构内层函数的作用域链,比一般函数作用域链,多一级作用域
-
pay 函数调用后,释放函数
由于下面还有
pay
引用着mother
作用域,所以这里只释放pay
作用域对象,pay
作用域链引用的mother
作用域对象并没有被释放。
-
-
执行
total=0;
语句- 查找
total
,未在全局作用域内找到total
- 执行赋值语句,在全局作用域声明变量
total
,并赋值为 0
- 查找
-
再次调用
pay
函数-
词法作用域分析
- JS 引擎临时创建 pay 函数作用域对象
pay
函数内部创建money
局部变量,值为 100
-
按照 mother 函数体语句顺序执行
total -= money
- 查找
money
,在pay
函数内部找到局部变量money
,且值为 100 - 查找
total
在外部函数 Closure (mother) 作用域链中找到,且值为 900 - 执行
-=
运算,total
减去money
,将 Closure (mother) 作用域链中 total 变为 800
- 查找
console.log(`花了${money}还剩${total}元`)
- 查找
money
,在pay
函数内部找到局部变量money
,且值为 100 - 查找
total
在外部函数 Closure (mother) 作用域链中找到,且值为 800 - 执行语句,输出:花了100还剩800元
- 查找
-
pay 函数调用后,释放函数
只释放 pay 作用域对象,pay 作用域链引用的 mother 作用域对象并没有被释放。
-
小试牛刀
1、return 返回内层函数
function mother() {
var i = 0
return function () {
i++
console.log(i)
}
}
var get1 = mother()
get1() // ?
var get2 = mother()
get2() // ?
get1() // ?
get2() // ?
答:1 1 2 2
解析:
function mother() {
// 妈妈
var i = 0 // 红包
return function () {
// 孩子
i++
console.log(i)
}
}
var get1 = mother() // 妈妈生老大,给老大包一个红包
get1() // 老大的红包是1
var get2 = mother() // 妈妈又生了老二,又给老二包一个新的红包
get2() // 老二的红包是1
get1() // 老大的红包+1,变成2
get2() // 老二的红包+1,变成2
2、内层函数强行赋值给全局变量
function fun() {
var i = 999
nAdd = function () {
i++
}
return function () {
console.log(i)
}
}
var getN = fun()
getN() // ?
nAdd()
getN() // ?
答:999 1000
解析:
function fun() {
// 妈妈
var i = 999 // 红包
// 给从未声明过的变量赋值,自动在全局创建该变量
// 剖腹产
nAdd = function () {
i++
} // 孩子1
// 妈妈顺产生了一个孩子2
return function () {
// 孩子2
console.log(i)
}
}
var getN = fun()
// 妈妈包了一个红包,生了2个孩子
// 结果: 两个孩子共用同一个红包!
getN() // 红包=999
nAdd() //(红包+=1)红包=1000
getN() // 红包=1000
3、将内层函数包裹在对象或数组中返回
function fun() {
arr = []
for (var i = 0; i < 3; i++) {
arr[i] = function () {
console.log(i)
}
}
}
fun()
arr[0]() // ?
arr[1]() // ?
arr[2]() // ?
答:3 3 3
解析:
function fun() {
// 妈妈
arr = [] // 不算红包,因为内层函数中没有用到!
// 但是arr是给未声明的变量强行赋值,自动在全局创建该变量
for (var i = 0 /*红包*/; i < 3; i++) {
// 循环三次
// 强行给外部全局变量中添加新函数——破腹产
// new Function()
arr[i] = function () {
// 反复创建了3个孩子
console.log(i) // 因为只是创建函数,所以这里暂时不执行。
}
}
// for结束后,i=3
}
fun()
// 妈妈一次刨妇产生了3个孩子,包了一个共用的红包,红包里是3块钱
// 三个兄弟,输出的结果是一样的,都是3
arr[0]() // 3
arr[1]() // 3
arr[2]() // 3
4、考察 运算符优先级、toString 、 valueOf
function fn() {
const sum = 0
function fun() {
sum++
return fun
}
fun.valueOf = function () {
return sum
}
fun.toString = function () {
return sum + ''
}
return fun
}
;+fn() // ?
;+fn()() // ?
fn()() + '' // ?
答:0 1 “1”
解析:
+fn()
1. fn() 返回 fun 函数,此时sum为0
2. +fun 试图将 fun 转为数字,会默认调用 fun.valueOf 函数,得到 0
+fn()()
1. fn() 得到 fun,此时sum为0
2. fun() 得到 fun,此时sum为1
3. +fun 试图将 fun 转为数字,会默认调用 fun.valueOf 函数,得到 1
fn()() + ''
1. fn() 得到 fun,此时sum为0
2. fun() 得到 fun,此时sum为1
3. fun + '' 试图将 fun 转为字符,会默认调用fun.toString 函数,得到 字符串 1
5、var + setTimeout(fn, 50)
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 50)
}
答:5 5 5 5 5
6、let + setTimeout(fn, 50)
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 50)
}
答:0 1 2 3 4
解析:
// let 相当于自动生成匿名函数
for (let i = 0; i < 5; i++) {
;(function (i) {
setTimeout(function () {
console.log(i)
}, 50)
})(i)
}
7、var + setTimeout(fn, 0)
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
答: 5 5 5 5 5
8、fun(n, o) 闭包和作用域链
function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
},
}
}
var a = fun(0) // undefined
a.fun(1) // ?
a.fun(2) // ?
a.fun(3) // ?
var b = fun(0) // undefined
.fun(1) // ?
.fun(2) // ?
.fun(3) // ?
var c = fun(0) // undefined
.fun(1) // ?
c.fun(2) // ?
c.fun(3) // ?
答:
undefined 0 0 0
undefined 0 1 2
undefined 0 1 1
解析:
9、用原生的 call() 模拟实现 bind
答:
方法一:
Function.prototype.bind = function (_this, ...args) {
console.log('调用内部函数')
const fun = this
return function (...childArgs) {
fun.call(_this, ...args, ...childArgs)
}
}
方法二:
Function.prototype.bind = function () {
console.log('调用内部函数')
const fun = this
const _this = arguments[0]
const args = Array.prototype.slice.call(arguments, 1)
return function () {
const childArgs = Array.prototype.slice.call(arguments)
fun.apply(_this, args.concat(childArgs))
}
}
使用 bind:
function fn(parnetVal, childVal) {
console.log(parnetVal, childVal)
}
fn.call({ name: 'bar' }, 2000, 200) // 2000 200
const fun = fn.bind({ name: 'baz' }, 5000)
fun(500) // 5000 500
10、add(1)(2)(3) 函数柯里化
要求定义函数 add
,实现alert(add(1)(2)(3))
可以连续给一个函数反复传参,反复传的参数能累计到函数内
答:
var add = function (x1) {
var sum = x1
var temp = function (x2) {
sum = sum + x2
return temp
}
temp.toString = function () {
return sum + ''
}
return temp
}
alert(add(1)(2)(3)) // 6
解析 add 函数:
var add = function (x1) {
var sum = x1 // 先将第一个()中的x1,也就是 add(1) 的 1保存进局部变量
var temp = function (x2) {
// 定义内层函数,能够将之后()的局部变量 x2 累加到变量 sum 中
sum = sum + x2
return temp // 因为柯里化要求,可以连续给一个函数反复传参,反复传的参数能累计到函数内,所以,应该返回当前函数对象,才能继续调用,并传参
}
// 注意:这里是对 temp 设置 toString 方法
temp.toString = function () {
// 必须为temp函数定义个 toString() 方法。toString() 方法可以返回temp函数闭包中现在sum变量的值
return sum
}
return temp // 为了可以传入第二个参数(2),add() 也必须返回函数,才能继续调用,并传参
}
alert(add(1)(2)(3)) // 只能用 alert,会自动调用 toString 方法。console.log 不行,会输出 temp 函数
解析 alert(add(1)(2)(3))
:
add(1) // x1 = 1; sum = 1; 返回 temp
add(1)(2) // temp(2); x2 = 2; sum = 1 + 2 = 3; 返回 temp
add(1)(2)(3) // temp(3); x2 = 3; sum = 3 + 3 = 6; 返回 temp
alert(add(1)(2)(3)) // 强制调用 add(1)(2)(3) 返回的 temp 函数的 toString 方法。得到 6
11、页面上五个完全相同的按钮,点哪个按钮,让按钮弹出自己是第几个
答:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button>0</button>
<button>1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<script>
// 方法一:
var btnList = document.querySelectorAll("button");
for (var i = 0; i < btnList.length; i++) {
(function (i) {
btnList[i].onclick = function () {
alert(i);
};
})(i);
}
// 方法二:
var btnList = document.querySelectorAll("button");
for (let i = 0; i < btnList.length; i++) {
btnList[i].onclick = function () {
alert(i);
};
}
</script>
</body>
</html>
12、动态生成 4*4 表格,每个表格中有坐标(0,0) - (3,3) 点击格增加次数,且每个格互不干扰,次数通过弹窗显示
<div class="container"></div>
.container {
width: 200px;
height: 200px;
background-color: blueviolet;
}
.container p {
width: 48px;
height: 48px;
line-height: 48px;
text-align: center;
float: left;
background-color: pink;
border: 1px solid #eee;
margin: 0;
}
const box = document.querySelector(".container");
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
const p = document.createElement("p");
p.innerText = `(${i}, ${j})`;
const num = 0;
(function (num, i, j) {
p.addEventListener("click", function () {
num++;
console.log(`(${i}, ${j}), 点击第${num}次`);
});
})(num, i, j);
// 或者这种闭包写法
// p.addEventListener("click", (function (i, j) {
// var num = 0;
// return function () {
// num++;
// console.log(`(${i}, ${j}), 点击第${num}次`);
// };
// })(i, j));
box.appendChild(p);
}
}
问题: num 只能自己使用,其他人无法使用
const box = document.querySelector(".container");
// 优化
const arr = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
];
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
const p = document.createElement("p");
p.innerText = `(${i}, ${j})`;
const num = 0;
(function (i, j) {
p.addEventListener("click", function () {
arr[i][j]++;
console.log(`(${i}, ${j}), 点击第${arr[i][j]}次`);
});
})(i, j);
box.appendChild(p);
}
}
问题: arr 是一个全局变量会暴露
const box = document.querySelector(".container");
// 优化
(function () {
const arr = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
];
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
const p = document.createElement("p");
p.innerText = `(${i}, ${j})`;
const num = 0;
(function (i, j) {
p.addEventListener("click", function () {
arr[i][j]++;
console.log(`(${i}, ${j}), 点击第${arr[i][j]}次`);
});
})(i, j);
box.appendChild(p);
}
}
})();