2023/1/15 JS-闭包问题研究
1 举个栗子分析执行上下文
1: let a = 3
2: function addTwo(x) {
3: let ret = x + 2
4: return ret
5: }
6: let b = addTwo(a)
7: console.log(b)
为了理解 JavaScript 引擎是如何工作的,让我们详细分析一下:
- 在第 1 行,我们在全局执行上下文中声明了一个新变量 a,并将赋值为 3。
- 接下来就变得棘手了,第 2 行到第 5 行实际上是在一起的。这里发生了什么?
我们在全局执行上下文中声明了一个名为addTwo的新变量,我们给它分配了什么? -->一个函数定义。
两个括号{}之间的任何内容都被分配给addTwo,函数内部的代码没有被求值,没有被执行,只是存储在一个变量中以备将来使用。
- 现在我们在第 6 行。
6: let b = addTwo(a)
首先,我们在全局执行上下文中声明一个新变量,并将其标记为b,变量一经声明,其值即为 undefined。
接下来,仍然在第 6
行,我们看到一个赋值操作符。我们准备给变量b赋一个新值,接下来我们看到一个函数被调用。当看到一个变量后面跟着一个圆括号(…)时,这就是调用函数的信号
,接着,函数都返回一些东西(值、对象或
undefined),无论从函数返回什么,都将赋值给变量b。
- 但是首先我们需要调用标记为addTwo的函数。JavaScript
将在其全局执行上下文内存中查找名为addTwo的变量。它是在[步骤 2(或第 2 - 5
行)中定义]的。
注意:变量[a]作为参数传递给函数。
JavaScript 在全局执行上下文内存中搜索变量a,找到它,发现它的值是 3,并将数字 3 作为参数传递给函数,准备好执行函数。
现在执行上下文将切换【全局执行上下文 -> 函数执行上下文】,创建了一个函数执行上下文,我们将其命名为“addTwo执行上下文”,函数执行上下文被推送到调用栈上。在 addTwo 执行上下文中,我们要做的第一件事是什么?
2: function addTwo(x) {
3: let ret = x + 2
4: return ret
5: }
你可能会说,“在 addTwo 执行上下文中声明了一个新的变量 ret”,这是不对的。
正确的答案是:我们需要先看函数的参数。在 addTwo 执行上下文中声明一个新的变量[x],因为值 3 是作为参数传递的,所以变量 x被赋值为 3。
下一步才是在 addTwo 执行上下文中声明一个新的变量ret。它的值被设置为 undefined(第三行)。
- 仍然是第 3 行,需要执行一个相加操作。
首先我们需要x的值,JavaScript 会寻找一个变量x,它会首先在addTwo执行上下文中寻找,找到了一个值为 3。第二个操作数是数字2。两个相加结果为 5 就被分配给变量ret。
- 第 4 行,我们返回变量ret的内容,在 addTwo 执行上下文中查找,找到值为 5,返回,函数结束。
- 第 4 - 5 行,函数结束。
addTwo 执行上下文被销毁,变量x和ret被消去了,它们已经不存在了。addTwo执行上下文从调用堆栈中弹出,返回值返回给调用【全局】上下文,在这种情况下,调用上下文是全局执行上下文,因为函数addTwo是从全局执行上下文调用的。
- 现在我们继续第 4 步的内容,返回值 5 被分配给变量b,此时实际上程序仍然在第 6 行
- 在第 7 行,b的值 5 被打印到控制台了。
2 举个栗子分析作用域
我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript 的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined)。
1: let val1 = 2
2: function multiplyThis(n) {
3: let ret = n * val1
4: return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)
- 在全局执行上下文中声明一个新的变量val1,并将其赋值为 2。
- 行 2 - 5,声明一个新的变量 multiplyThis,并给它分配一个函数定义。
- 第6行,声明一个在全局执行上下文 multiplied 新变量。
- 从全局执行上下文内存中查找变量multiplyThis,并将其作为函数执行,传递数字 6 作为参数。
新函数调用(创建函数执行上下文),创建一个新的 multiplyThis 函数执行上下文。
在 multiplyThis 执行上下文中,声明一个变量 n 并将其赋值为 6 -->声明后才会进入函数体内部执行
- 执行函数回到第 3 行。
在multiplyThis执行上下文中,声明一个变量ret。
继续第 3 行。对两个操作数 n 和 val1 进行乘法运算。在multiplyThis执行上下文中查找变量 n。 我们在步骤 6中声明了它,它的内容是数字 6。在multiplyThis执行上下文中查找变量val1。 multiplyThis执行上下文没有一个标记为val1 的变量。我们向调用上下文查找,调用上下文是全局执行上下文,在全局执行上下文中寻找 [val1]。它在步骤 1 中定义,数值是 2。
继续第 3 行。将两个操作数相乘并将其赋值给ret变量,6 * 2 = 12,ret 现在值为 12。
- 返回ret变量,销毁multiplyThis执行上下文及其变量 ret 和 n 。变量 val1 没有被销毁,因为它是全局执行上下文的一部分。
- 回到第 6 行。在调用上下文中,数字 12 赋值给 multiplied 的变量。
- 最后在第 7 行,我们在控制台中打印 multiplied 变量的值
3 返回函数的函数[高阶函数]
在第一个例子中,函数addTwo返回一个数字。请记住,函数可以返回任何东西。让我们看一个返回函数的函数示例,因为这对于下方理解闭包非常重要。看栗子:
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
高阶函数是什么?
所谓高阶函数,就是一个函数就可以接收另一个函数作为参数,或者是返回一个函数–>常见的高阶函数有map、reduce、filter、sort等
<script>
var ADD = function add(a) {
return function (b) {
return a + b
}
}
console.log(ADD(100)(100)); // 200
</script>
map
<script>
// map接受一个函数作为参数,不改变原来的数组,只是返回一个全新的数组
var arr = [1, 2, 3, 4, 5]
arr.map(item => item * 2)
console.log(arr);
</script>
<script>
// map接受一个函数作为参数,不改变原来的数组,只是返回一个全新的数组
var arr = [1, 2, 3, 4, 5]
var arrNew = arr.map(item => item * 2);
console.log(arrNew);
</script>
reduce
<script>
// reduce也是返回一个全新的数组-
// reduce接受一个函数作为参数:
// 这个函数要有两个形参,代表数组中的前两项
// reduce会将这个函数的结果与数组中的第三项再次组成这个函数的两个形参以此类推进行累积操作
var arr = [1, 2, 3, 4, 5]
var arr2 = arr.reduce((a, b) => a + b)
console.log(arr2) // 15
</script>
filter
<script>
// filter返回过滤后的数组-
// filter也接收一个函数作为参数:
// 这个函数将作用于数组中的每个元素,根据该函数每次执行后返回的布尔值来保留结果:
// 如果是true就保留,如果是false就过滤掉
var arr = [1, 2, 3, 4, 5]
var arr3 = arr.filter(item => item % 2 == 0)
console.log(arr3)// [2,4]
</script>
4 闭包
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起,这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
1 引出闭包概念
错误场景 - 需求: 点击某个按钮, 提示"点击的是第n个按钮"
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<!--
需求: 点击某个按钮, 提示"点击的是第n个按钮"
-->
<script>
var btns = document.getElementsByTagName('button')
for (var i = 0, length = btns.length; i < length; i++) {
var btn = btns[i]
btn.onclick = function () { //遍历加监听
alert('第' + (i + 1) + '个') //结果 全是[4]
}
}
</script>
</body>
此处错误是:直接修改并使用全局变量[i],导致for循环结束后 【对于以上案例 i = 3】,所有点击按钮绑定的弹窗值都是[i+1 = 4 ]
将变量挂载到自身来解决:
<script>
var btns = document.getElementsByTagName('button')
for (var i = 0, length = btns.length; i < length; i++) {
var btn = btns[i]
btn.index = i //存到自身
btn.onclick = function () { //遍历加监听
alert('第' + (this.index + 1) + '个') // 结果正确
}
}
</script>
利用闭包:
<script>
var btns = document.getElementsByTagName('button')
// 利用闭包
for (var i = 0, length = btns.length; i < length; i++) {
// 此处的j是局部的,它将传入的[i]存入局部的[j]中,这样就能实现效果
(function (j) {
var btn = btns[j]
btn.onclick = function () {
alert('第' + (j + 1) + '个')
}
})(i)
}
</script>
2 举个闭包栗子分析理解
按照正常逻辑理解:
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
错误的流程理解,故意按照正常的逻辑流程走,做印证:
<script>
function createCounter() {
let counter = 0
const myFunction = function () {
counter = counter + 1
return counter
}
return myFunction
}
const increment = createCounter()
const c1 = increment()
const c2 = increment()
const c3 = increment()
console.log('example increment', c1, c2, c3)
</script>
正确的理解:
increment函数记住了那个cunter的值。这是怎么回事?
counter是全局执行上下文的一部分吗?
尝试 console.log(counter),得到undefined的结果,显然不是这样的。
也许,当你调用increment时,它会以某种方式返回它创建的函数(createCounter)?
这怎么可能呢?变量increment包含函数定义,而不是函数的来源,显然也不是这样的。
所以一定有另一种机制。闭包:
它是这样工作的,无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含
在函数创建时作用域中
的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变化
所以我们上面的解释都是错的,让我们再试一次,但是这次是正确的:
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
** 你此时可能会问,是否有任何函数具有闭包,甚至是在全局范围内创建的函数? **
答案是肯定的。在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。
但当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。
5 常见的闭包
1 将函数作为另一个函数的返回值
<script>
// 1. 将函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
</script>
2 将函数作为实参传递给另一个函数调用
<script>
// 2. 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
for (let i = 0; i < 3; i++) {
setTimeout(function () {
msg ++
alert(msg)
}, time)
}
}
showDelay(1, 2000) // 2 3 4
</script>
6 高阶函数与柯里化
1 从 ES6 高阶箭头函数理解函数柯里化(运用到闭包)
<script>
function add(a) {
return function (b) {
return a + b
}
}
var add3 = add(3) // add3表示一个指向函数的变量 可以当成函数调用名来用
console.log(add3(4) === 3 + 4); // true
</script>
再简化一下,可以写成如下形式:
<script>
let add = function (a) {
var param = a;
var innerFun = function (b) {
return param + b;
}
return innerFun;
}
</script>
虽然好像没什么意义,但是很显然上述使用了[闭包],而且该函数的返回值是一个函数。其实,这就是高阶函数的定义:以函数为参数或者返回值是函数的函数。
<script>
let add = function (a) { // a = 100 100 100
var param = a; // param = 100
console.log(' var param = a;')
var innerFun = function (b) { // b = 200 200 200
param = param + param // 200 400 800
return param + b; // 400 600 1000
}
return innerFun;
}
var ADD = add(100)
console.log(ADD(200)); // 400
console.log(ADD(200)); // 600
console.log(ADD(200)); // 1000
</script>
2 柯里化
关键就是理解柯里化,其实可以把它理解成,柯里化后,将第一个参数变量存在函数里面了(闭包),然后本来需要n个参数的函数可以变成只需要剩下的(n - 1个)参数就可以调用,比如:
let add = x => y => x + y
let add2 = add(2)
-*----------------------------------
本来完成 add 这个操作,应该是这样调用
let add = (x, y) => x + y
add(2,3)
----------------------------------
1. 而现在 add2 函数完成同样操作只需要一个参数,这在函数式编程中广泛应用。
let add = x => y => x + y
let add2 = add(2)
2.详细解释一下,就是 add2 函数 等价于 有了 x 这个闭包变量的 y => x + y 函数,并且此时 x = 2,所以此时调用
add2(3) === 2 + 3
3 总结
如果是a => b => c => {xxx}
这种多次柯里化的,如何理解?
理解:前n - 1次调用,其实是提前将参数传递进去,并没有调用最内层函数体,最后一次调用才会调用最内层函数体,并返回最内层函数体的返回值
结合上文可知,这里的多个连续箭头(无论俩个箭头函数三个及以上)函数连在一起 就是在柯里化。所以连续箭头函数就是多次柯里化函数的 es6 写法。
调用特点:let test = a => b => c => {xxx}
比如对于上面的 test 函数,它有 3 个箭头, 这个函数要被调用 3 次 test(a)(b)(c),前两次调用只是在传递参数,只有最后依次调用才会返回 {xxx} 代码段的返回值,并且在 {xxx} 代码段中可以调用 a,b,c
7 闭包的作用
- 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
函数执行完后, 函数内部声明的局部变量是否还存在?
一般是不存在, 存在于闭中的变量才可能存在
在函数外部能直接访问函数内部的局部变量吗?
不能, 但我们可以通过闭包让外部操作它
8 闭包的生命周期
产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
死亡: 在嵌套的内部函数成为垃圾对象时,即没有人指向它时死亡,通常置为[null],当然指向其他也行,但不安全(容易污染变量)
<script>
//闭包的生命周期
function fn1() {
//此时闭包就已经产生了(函数提升,实际上[fn2]提升到了第一行, 内部函数对象已经创建了)
var a = 2
function fn2() { //如果时[let fn2=function(){}],那么在这行才会产生闭包
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)
</script>
9闭包的应用
闭包的应用 : 定义JS模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包信n个方法的对象或函数
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
模块定义
模块的调用
10 闭包的缺点及解决
缺点:
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 容易造成内存泄露
解决: - 能不用闭包就不用
- 及时释放
function fn1() {
var arr = new Array(100000)
function fn2() {
console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
f = null //让内部函数成为垃圾对象-->回收闭包