js 闭包

128 阅读8分钟

场景

1.开发一个消费程序

支持根据参数修改金额

方法1,全局变量

var money = 500
function sub(m) {
    money = money - m
    console.log(`消费金额{$m},剩余${money}`)
}
sub(100)
sub(200)

缺点
其他代码容易修改到全局变量

var money = 500
function sub(m) {
    money = money - m
    console.log(`消费金额{$m},剩余${money}`)
}
sub(100)//消费金额100,剩余400
sub(200)//消费金额200,剩余200

var money = 50 //其他开发不清楚有全局的变量,自己修改了

方法2 局部变量

优点
不影响全局
缺点\

  • 每次都重新初始化
  • 调用完就会被销毁,不能重复使用
function sub(m) {
    var money = 500 //如果放在方法里,则每次都会创建+销毁
    money = money - m
    console.log(`消费金额{$m},剩余${money}`)
}
//每次结果都是一样
sub(100) //消费金额100,剩余400
sub(100) //消费金额100,剩余400
sub(100) //消费金额100,剩余400

使用闭包

优点

  • 可以做到函数的局部变量重复使用一个变量的效果
  • 同时不污染外部变量

实现方式:

  1. 在原有函数外部多包裹一层方法 (包裹要保护的变量与使用变量的内层函数)
  2. 原函数通过return返回
  3. 通过函数柯里化方式调用
function mainSub() { //1.多包一层方法
    var money = 500
    return function sub(m) {//2.原函数return返回,这里可以不起名,直接用匿名函数
        money = money - m
        console.log(`消费金额${m},剩余${money}`)
    }
}
//1.需要先保存引用,再调用
let mainSubFun = mainSub(); //注意这里的内层函数sub,只是定义了,未执行
//2.通过函数柯里化方式调用
mainSubFun(100)
mainSubFun(200)

如果单独分开调用,则每次mainSub每次都是独立的一个对象,里面的money也是互相独立。

mainSub()(100)//消费金额100,剩余400
mainSub()(200)//消费金额200,剩余300 //money并没有共享

注意点

  • 内层函数sub只是定义,并未调用过。
  • function sub() 其实等价于 new Function,函数声明定义其实就是new一个对象,得到一个引用地址
  • new Function都会同步在对象创建作用域链,当前VO暂时为空,等待执行才赋值并加入到作用域链上。 执行流程
  1. 由于都每次都要先调用 mainSub(),所以每次都会mainSub的内部money作用域变量。
  2. mainSub方法返回的是一个sub对象,这个sub对象里面保留了对mainSub函数的money变量的引用。
  3. 由于mainSub执行结束,mainSub的作用域对象都会释放,但是由于sub的作用域链中保留了mainSub作用域对象的引用,所以mainSub作用域下的money变量被保留下来。

所有函数调用完,只会清除离自己的作用域。

//1.首先创建全局作用域对象window,同时包含mainSub函数定义和变量mainSubFun定义
//2.function mainSub(){...} 执行了new Function(..) 多了个全局对象mainSub,同时建立对应的作用域链
function mainSub() { //外层函数 3.创建作用域对象与局部变量
    var money = 500     //4.执行方法体内容
    return function sub(m) {//6.内层函数 创建局部的作用域对象sub,同时建立对应的作用域链
        money = money - m   //7.sub执行代码
        console.log(`消费金额${m},剩余${money}`)  
        //8.清除sub函数作用域
    }
    // 5.释放函数作用域对象VO,返回值
}
let mainSubFun = mainSub(); //3.执行外部函数 (1)创建作用域vo(2)执行逻辑并 返回的sub对象(3)函数销毁清空当前的作用域,由于被mainSubFun 引用着,mainSub()的作用域对象无法销毁。
mainSubFun(100)// 6.执行内部函数 (1)创建作用域vo,(2)执行逻辑(3)函数销毁清空当前的作用域
mainSubFun(200)// 7.执行内部函数 (1)创建作用域vo,(2)执行逻辑(3)函数销毁清空当前的作用域

mainSubFun = null;//8.切除对内部函数的引用。闭包和其他引用对象都会被释放

  1. 程序刚启动-创建全局作用域对象window和全局作用域链
  2. 全局函数定义mainSub与变量声明mainSubFun
  3. 外层函数执行-mainSub创建作用域对象与局部变量
  4. 外层函数执行-mainSub执行方法体内容
  5. 外层函数执行-mainSub清除自己函数作用域对象,返回值
  6. 内层函数执行-sub创建作用域与变量
  7. 内层函数执行-sub执行代码
  8. 内层函数执行-sub清除自己函数作用域
  9. 内层函数执行-执行结束,sub恢复原来效果
  10. 释放闭包的所有引用mainSubFun=null

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

定义

  • 闭包也是一个对象
  • 闭包就是每次调用外层函数时,临时创建的函数作用域对象。
  • 外层函数作用域对象能留下来,是因为被内层函数对象的作用域链引用着, 无法释放。

原理

外层函数调用后,外层函数的作用域对象,被返回的内层函数的作用域链引用着,无法释放,就形成了闭包对象。

缺点

闭包逻辑很隐晦,容易找不到,所以容易造成内层泄露

解决方法

将保存内层函数对象的变量赋值为null

let mainSubFun = mainSub();
mainSubFun(100)
mainSubFun(200)
//调用结束后,把外层函数指向空 
mainSubFun = null

分析闭包的方法

1.确定3个元素

  1. 外层函数
  2. 外层函数的局部变量
  3. 内层函数

2.外层函数返回内层函数3种方法

  1. return function
//匿名函数
function outerFun() {
    var para = 0
    return function () { //
        para++
    }
}

//表达式返回
function outerFun() {
    var para = 0
    var fun = function innerFun(){para++}
    return fun
}

//表达式返回-科里化
function outerFun() {
    var para = 0
    var fun = function innerFun(){
        para++
        return fun//这是科里化的方式,返回后可以继续一直调用 
    }
    return fun
}
let ofun = outerFun()
ofun()()()()
  1. 强行赋值为全局变量 , 通过不定义,直接赋值的方式
function outerFun() {
    var para = 0
    fun = function innerFun(){//这里不申请,让他提前到全局定义
        para++
    }
    return fun
}
//等价于
var fun;
function outerFun() {
    var para = 0
    fun = function innerFun(){//这里不申请,让他提前到全局定义
        para++
    }
    return fun
}
  1. 将函数包裹在对象或数组中返回
//对象方式
function outerFun() {
    var para = 0
    var obj = {
        innerFun:function(){
           para++
        }
    } 
    return obj
} 
let fun = outerFun()['innerFun']
fun()
fun()

//数组方式
function outerFun() {
    var para = 0
    var fun = function innerFun(){para++}
    var array = [fun] 
    return array
} 
let fun = outerFun()[0]
fun()
fun()

3.外部函数返回多个内部方法

  1. 内部方法共用外部方法的变量
  2. 内部方法不共用外部方法的变量,互不干扰

栗子

1 强行把函数赋值给全局

function fun() { //外部函数
    var i = 999; //fun的作用域变量, 
    //第一个内部函数
    add = function () {
        i++
    }; 
    //第二个内部函数
    return function () {
        console.log(i)
    }
}
var fun1 = fun();
fun1(); //999
add(); //999+1
fun1(); //1000

2 多次调用方法有多个独立作用域

function outerFunc() { //外部函数
    var i = 0; //作用域变量
    return function () { //内部函数
        i++;
        console.log(i);
    }
}
var outer1 = outerFunc();
var outer2 = outerFunc(); 
outer1(); //1
outer2(); //1
outer1(); //2
outer2(); //2

3 for 中定义的var

function fun() { 
    arr = []; //这里没有加 var声明,会提升到全局
    for (var i = 0 ; i < 3; i++) { //循环结束时候,i = 3,i定义在fun 内部的变量
        arr[i] = function () { //这里没有加 var声明,会提升到全局
            console.log(i);
        }
    }
}
fun();
arr[0](); //3
arr[1](); //3
arr[2](); //3

4 settimeout

for (var i = 0; i < 5; i++) { 
    setTimeout(function () {
        console.log(i)
    }, 200);
}

会默认输出 5个5, 原因\

  • var i 是定义在全局,所以会被全局污染
  • setTimeout的回调方法需要等 for全局执行完,所以在回调前,i已经都是5

方法1 使用自执行函数IFFE

for (var i = 0; i < 5; i++) { 
    (function (i) {
        setTimeout(function () { 
         console.log(i)
        }, 200);
    })(i)  
}

方法2 var 改成 let

for (let i = 0; i < 5; i++) { 
    setTimeout(function () {
        console.log(i)
    }, 200);
}

js没有实际的块作用域概念,底层实现还是上面的IFFE实现。只是IFFE会自动把 for的i参数当实际参数传入到函数里。

5 科里化

要求定义函数add,实现:alert (add (1)(2)(3))
函数柯里化:可以连续给一个函数,反复传的参数,还能累计到函数內。

var add = function (x1) {
    var sum = x1;
    var tmp = function (x2) {
        sum = sum + x2;
        return tmp;
    }
    tmp.toString = function () {
        return sum;
    }
    return tmp;
}
alert(add(1)(2)(3));

6 onclick事件

五个完全一样的按钮,点击那个弹出自己是第几个

image.png 方法1

var container = document.getElementsByTagName("body")[0] 
for (let i = 0; i < 5; i++) {
    var box = document.createElement("button")
    box.innerHTML = `${i+1}`
    let btnClickFunc = function (i) {
        return function () {
            alert(`当前点击第${i+1}`)
        }
    }(i)
    box.onclick = btnClickFunc
    container.append(box)
}

方法2 把自执行函数放在外部先执行

var container = document.getElementsByTagName("body")[0] 
for (let i = 0; i < 5; i++) {
    var box = document.createElement("button")
    box.innerHTML = `${i+1}` 
    ;(function (i) { 
        box.onclick = function () {
            alert(`当前点击第${i+1}`)
        }
    })(i)
    container.append(box)
}

7 动态生成4*4表格

动态生成4*4表格,每个表格中有坐标() - (3,3) 点击格增加次数,且 每个格互补干扰,次数通过弹窗提示

image.png

image.png

<!DOCTYPE html>
<html>
<head>
    <style>
        #container{
            display: flex;
            flex-wrap: wrap;
        }
        #container > div{
            width: 20vw;
            height: 100px;
            background-color: burlywood;
            margin: 5px;
        }
    </style>
</head>
<body>
    <div id="container">
    </div>
    <script src="test3.js"></script>
</body>
</html>

方案1 一维数组+ onclick + 闭包

通过flexbox布局+ document.createElement动态创建div + 闭包btnClickFunc方法实现点击每个格子的数据隔离。

var container = document.getElementById("container")
for (let i = 0; i < 4; i++) { 
    for (let j = 0; j < 4; j++) {  
        var box = document.createElement("div")
        let btnClickFunc = function(i,j) {
            var sum = 0 
            return function () {
                sum++
                console.log(`第${i}---${j}格格,点击${sum}下`)
            }
        }(i,j) 
        box.onclick = btnClickFunc
        container.append(box)
    } 
}

方案2 二维数组+ onclick + 闭包

var container = document.getElementById("container") 
var array = [
    [0,0,0,0],
    [0,0,0,0],
    [0,0,0,0],
    [0,0,0,0]
]
for (let i = 0; i < array.length; i++) { 
    for (let j = 0; j < array[i].length; j++) {  
        var box = document.createElement("div") 
        box.innerHTML= `${i}--${j}`
        let btnClickFunc = function(i,j) {
            return function () {
                array[i][j]++
                console.log(`第${i}---${j}格格,点击${array[i][j]}下`)
            }
        }(i,j) 
        box.onclick = btnClickFunc
        container.append(box)
    } 
}

方案2 优化

把全局变量array变为私有变量,通过IFFE自执行函数实现

由于array 被有内部函数btnClickFunc 引用着,所以不会被销毁

var container = document.getElementById("container")

;(function () {
    var array = [
        [0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0]
    ]
    for (let i = 0; i < array.length; i++) {
        for (let j = 0; j < array[i].length; j++) {
            var box = document.createElement("div")
            box.innerHTML = `${i}--${j}`
            let btnClickFunc = function (i, j) {
                return function () {
                    array[i][j]++
                    console.log(`第${i}---${j}格格,点击${array[i][j]}下`)
                }
            }(i, j)
            box.onclick = btnClickFunc
            container.append(box)
        }
    }
})()

8 对象中的闭包

var name="window";
var p = {
    name:"Perter",
    getName:function(){ //这里相当于外层函数
        var self = this; //外层函数的上下文被保留下来
        return function(){//这里相当于内层函数
            return self.name;
        }
    }

}
var getName = p.getName()
var _name=getName()
console.log(_name)//输出 Perter

形成闭包

image.png

9 对象中的必包2

function fun(n,o) {
    console.log(o)
    return {
        fun:function(m) {
            return fun(m,n)
        }
    }
}

var a = fun(0)//undefined
a.fun(1)//0
a.fun(2)//0
a.fun(3) //0

var b = fun(0)//undefined
        .fun(1)//0
        .fun(2)//1
        .fun(3)//2  
        
var c = fun(0).fun(1);//undefined //0
c.fun(2) //1
c.fun(3) //1

10 对象中的闭包3

var a = 2
var obj = {
    a:4,
    fn1:(function () {
        // console.log("this.a",this.a)
        this.a*=2 //匿名函数自调用 this 指向window
        var a = 3 //这里的a 是闭包
        return function(){ 
            // console.log("--this.a",this.a)
            this.a*=2 //这里根据实际调用的对象,如果是 fun() 则是全局,obj.fun() 则是调用的对象
            a*=3 //这里访问的是闭包的a
            console.log(a)
        }
    })()
}
var fn2 = obj.fn1;
console.log(a)
fn2() //this 指向window ,执行的是闭包返回的函数
obj.fn1() //由于是obj.调用,所以this 指向obj ,执行的是闭包返回的函数
console.log(a)
console.log(obj.a)

// 输出结果
// 4
// 9
// 27
// 8
// 8

最终的结果图 image.png