闭包 closure

闭包 closure

什么是闭包?
闭包也是一个对象。

作用: 闭包是既重用变量又保护变量不被污染的一种编程方法。使用闭包主要是为了设计私有的方法和变量。

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

使用场景: 希望给一个函数,保存一个可反复使用,又不被外界污染的专属局部变量时,就用闭包

优点: 避免全局变量的污染,且局部变量可以重复使用。

缺点: 由于闭包藏得很深几乎找不到,且常驻内存。使用不当,会增大内存使用量,造成内存泄漏!

解决内存泄漏:及时释放不用的闭包

特性:

  1. 函数嵌套函数
  2. 函数内部可以引用外部的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

如何释放闭包对象?
将保存内层函数对象的变量赋值为 null,导致函数名变量与内层函数对象分开。

内层函数对象没人要了,会被 js 引擎自动释放。

内层函数引用的闭包对象,也没人要了,闭包对象就被释放了。

最后,内存恢复干净的初始状态

答题技巧:

  1. 看程序,判断输出结果
    • 找外层函数
    • 找外层函数的局部变量
    • 找内层函数(多个)
  2. 根据需求,使用闭包实现功能

为什么需要闭包?

举例:

定义一个函数帮小孩儿管理压岁钱

  1. 假设小孩儿共有 1000 元压岁钱,存哪儿?
  2. 小孩儿每花一笔钱,就从总钱数里减去一笔。
    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 变量被反复使用了
闭包三部曲:
  1. 用外层函数包裹要保护的变量和使用变量的内层函数

    外层函数的局部变量由该外层函数的多个内层函数共用
    外层函数多次调用,内层函数的局部变量被多次生成,但是互不影响

  2. 在外层函数内部返回内层函数对象
    • return 内层函数
    • 将内层函数强行赋值给全局变量
    • 将内层函数包裹在对象或数组中返回
  3. 调用外层函数用变量接住返回的内层函数对象。

使用闭包三步曲,可以为函数保存一个既可重用,又不会被污染的专属局部变量。

代码分析:

程序开始执行前:

  • 词法作用域

    • 创建函数对象 mother,保存函数体

    • 再创建 pay 变量,值为 undefined

    • 每个函数对象身上都有一个作用域链,暂时只保存全局作用域对象 window
      在这里插入图片描述

程序运行:

  • pay = mother() ,调用 mother 函数

    • 词法作用域分析

      • JS 引擎临时创建 mother 函数作用域对象
      • mother 函数内部创建 total 局部变量
        在这里插入图片描述
    • 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);
    }
  }
})();
  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值