来一个前端闭包知识点

在这里插入图片描述

什么是闭包

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。

自由变量:是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

闭包共有两部分组成:闭包 = 函数 + 函数能够访问的自由变量。

闭包是指有权访问另外一个函数作用域中的变量的函数。闭包是一种特殊的对象,它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数 (代号 B),当 B 执行时,如果访问了 A 中变量对象的值,那么闭包就会产生,且在 Chrome 中使用这个执行上下文 A 的函数名代指闭包。

function makeFunc() {
  var name = '奶油桃子'
  function displayName() {
    console.log(name)
  }
  return displayName
}

var myFunc = makeFunc()
myFunc()

简单来说,闭包就是函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

如果说闭包的是函数嵌套了函数,然后返回一个函数。其实这个解释是不完整的。

function A() {
  let a = 1;
  window.B = function () {
    console.log(a)
  }
}
A();
B();//1

在 JS 中,闭包存在的意义就是可以间接访问函数内部的变量

闭包的本质

当前环境中存在指向父级作用域的引用

闭包的优点

  • 可以重复使用变量,并且不会造成变量污染(全局变量可以重复使用,但是容易造成变量污染。局部变量仅在局部作用域内有效,不可以重复使用,不会造成变量污染。闭包结合了全局变量和局部变量的优点。)

  • 可以用来定义私有属性和私有方法。

闭包的缺点

  • 比普通函数更占用内存,会导致网页性能变差,容易造成内存泄露。
闭包变量的存储位置

由闭包的特点可以看出,闭包中的变量没有保存在栈中,而是保存到了堆中。如果变量存储在栈中,栈中数据在函数执行完成后就会被自动销毁。

闭包的作用

  • 封装私有变量
  • 柯里化 bind
  • 模块化
  • 函数防抖和节流

在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包,IIFE(立即执行函数表达式)创建闭包。

var num = 10;
var obj = {
  num: 20
}
obj.fn = (function(num) {
  this.num = num * 3;
  num++;
  return function(n) {
    this.num += n
    num++
    console.log('num', num)
  }
})(obj.num)

var fn = obj.fn
fn(5)
obj.fn(10)

console.log('num1', num)
console.log('obj.num', obj.num)
// 22 23 65 30 => 浏览器环境下
// 22 23 10 30 => node 环境下
  1. 首先会声明一个全局作用域(window)。
  2. 第一步:全局作用域下的变量提升(注意:函数表达式、箭头函数、立即执行函数不会进行变量提升)。
  3. 代码自上而下执行。
  4. 正常的(数字/字符串)变量或常量直接赋值。
    • 如果遇到对象或者函数赋值就会在堆内存开辟一个内存空间,将字符串存进去。
      • 存储属性名
      • 将地址赋值给变量
      • 代码继续自上而下执行
    • 如果遇到自执行函数,就将自执行函数的返回结果赋值给变量。
      • 函数执行,声明一个私有的作用域(变量都是私有的)。
      • 函形参赋值。
      • 变量提升。
      • 自执行函数代码自上而下继续执行。
      • 如果自执行函数没有主体,则 this 指向的 window;注意:看改变的值改变的是私有变量还 是其他变量。 如果自执行函数返回一个函数(return)。
        • 开辟一个堆内存空间(将字符串存进去);
        • 将该堆内存的地址返回给 return
      • 所以自执行函数执行返回的变量地址指向 return 的地址(也就是执行开辟的堆内存空 间)。
      • 此时函数内部的函数被外部的变量引用着,所以这个私有的作用域不会销毁。
    • 遇到函数执行。
      • 函数执行,形成一个私有的作用域(变量是私有的)。
      • 形参赋值。
      • 变量提升。
      • 函数自上而下执行。
      • this 的指向遵循三个原则,以及注意私有作用域变量的作用范围
      • 函数执行完毕,进行作用域销毁
(() => {
 let x, y;
 try {
  throw new Error();
 } catch (x) {
  (x = 1), (y = 2);
  console.log(x);
 }
 console.log(x);
 console.log(y);
})();
// 1 undefined 2

解析: catch块接收参数x。当我们传递参数时,这与变量的x不同。这个变量x是属于catch作用域的。之后,我们将这个块级作用域的变量设置为1,并设置变量y的值。 现在,我们打印块级作用域的变量x,它等于1。 在catch块之外,x仍然是undefined,而y是2。 当我们想在catch块之外的console.log(x)时,它返回undefined,而y返回2。

let res = new Array()
for (var i = 0; i < 10; i++) {
  res.push(function () {
    return console.log(i)
  })
}
res[0]()
res[1]()
res[2]()
// 10 10 10

期望输出的是0,1,2,实际上却不会。原因就是涉及「作用域」,怎么解决呢?

使用let代替var,形成块级作用域
使用bind函数。

res.push(console.log.bind(null, i))

解法还有其他的,比如使用IIFE,形成私有作用域等等做法。

function fun(n, o) {
  console.log(o)
  return {
    fun: function (m) {
      return fun(m, n);
    }
  };
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
// undefined 0 0 0
var b = fun(0).fun(1).fun(2).fun(3);
// undefined 0 1 2
var c = fun(0).fun(1); c.fun(2); c.fun(3);
// undefined 0 1 1

闭包面试题

{
  for (var i = 1; i <= 5; i++) {
    setTimeout(function timer() {
      console.log(i);
    }, i * 1000);
  }
}

在这里插入图片描述
循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是 6。因此,输出显示的是循环结束时 i 的最终值。
延迟函数的回调会在循环结束时才执行。当定时器运行时即使每个迭代中执行的是 setTimeout(…, 0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 6 出来。

解决思路:
=> 在循环的过程中每个迭代都需要一个闭包作用域
=> IIFE 会通过声明并立即执行一个函数来创建作用域。每个延迟函数都会将 IIFE 在每次迭代中创建的作用域封闭起来。

{
  for (var i = 1; i <= 5; i++) {
    (function () {
      var j = i;
      setTimeout(function timer() {
        console.log(j);
      }, j * 1000);
    })();
  }
}

仅仅将它们进行封闭是不够的。IIFE 只是一个什么都没有的空作用域。它需要包含一点实质内容才能正常工作。

{
  for (var i = 1; i <= 5; i++) {
    (function () {
      var j = i;
      setTimeout(function timer() {
        console.log(j);
      }, j * 1000);
    })();
  }
}

改进 ===> 在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

{
  for (var i = 1; i <= 5; i++) {
    (function (j) {
      setTimeout(function timer() {
        console.log(j);
      }, j * 1000);
    })(i);
  }
}

持续改进 ===> let 声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

{
  for (var i = 1; i <= 5; i++) {
    let j = i; // 闭包的块作用域
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  }
}

for 循环头部的 let 声明指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

{
  for (let i = 1; i <= 5; i++) {
    setTimeout(function timer() {
      console.log(i);
    }, i * 1000);
  }
}

参考书籍:你不知道的 JavaScript (上卷)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值