闭包【JS深入知识汇点3】

233 阅读5分钟

系列文章:

在看这篇闭包之前,请一定要先看‘作用域链【JS深入知识汇点2】’这篇文章,不然很难把知识点串联在一起。

闭包

什么是闭包?

闭包就是那些引用了外部作用域中变量的函数,当函数可以记住并且访问所在的词法作用域时,并且函数是在当前词法作用域之外执行,此时该函数和声明该函数的词法环境的组合。

通过背包的类比, 无论何时声明新函数并将其赋值给变量,都会携带一个背包,背包里是函数创建时作用域中的所有变量。

闭包解决了什么问题?

由于闭包可以缓存上级作用域,就使得函数外部打破了“函数作用域”的束缚,可以访问函数内部的变量。

闭包有哪些应用场景?

只要有函数类型的值进行传递,此值被调用时,都有闭包的身影。比如一个 ajax 请求的成果回调,一个事件绑定的回调方法,一个 setTimeout 的延时回调。

闭包和纯函数区别:

区分闭包和纯函数:

  • 引用了外部作用域中变量的函数是闭包
  • 没有引用外部作用域中变量的函数,它们通常返回一个值并且没有副作用

经典试题

直接看代码吧,用语言来描述过于空洞。

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

这是一个高频率会看到的题,我们期望的结果是:分别输出数字1 - 5,每秒一个,每次一个。但实际上,会以每秒一次的频率输出五次6。

那代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?缺陷是我们试图假设循环中每个迭代在运行时,都会为自己"捕获"一个 i 的副本。但是实际上,尽管这五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此只有一个i。

如果想要返回的预期结果,可以通过以下方法:

立即执行函数表达式

在迭代内,使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新作用域封闭在每个迭代内部。

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

let 语法

let语法本质上是将一个块转换成一个可以被关闭的作用域,let声明的变量在每次迭代都会声明。

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

var、let 和 const 区别的实现原理是什么

声明方式 变量提升 重复声明 作用域
var 提升 允许 非块级
let 不提升 不允许 块级
const 不提升 不允许 块级

var、let 和 const 内存分配原理

  • var:会直接在栈内存里预分配内存空间,然后等到实际语句执行的时候,再存储对应的变量。
  • let:不会在栈内存里预分配内存空间,而且在栈内存分配变量时,做一个检查,如果有相同变量名存在就报错。
  • const: 不会在栈空间预分配内存空间,也会查重报错。不过 const 存储的是基本类型的话,无法修改,如果是引用类型,无法修改栈内存里分配的指针,但是可以修改指针指向的对象里面的属性。

难题解析

Q1:改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。

for (var i = 0; i< 10; i++){
	setTimeout(() => {
		console.log(i);
    }, 1000)
}
// 方法 1
for (var i = 0; i< 10; i++){
    setTimeout((i) => {
        console.log(i);
    }, 1000, i)
}
// 方法 2
for (var i = 0; i< 10; i++){
    ((i) => setTimeout(() => {
		console.log(i);
    }, 1000))(i)
}
// 方法 3 
for (var i = 0; i< 10; i++){
	setTimeout(console.log, 1000, i)
}
// 方法 4
for (var i = 0; i< 10; i++){
	setTimeout(console.log.bind({}, i), 1000, i)
}
// 方法 5
for (let i = 0; i< 10; i++){
    setTimeout(() => {
		console.log(i);
    }, 1000)
}
// 方法6
for (var i = 0; i< 10; i++){
    let 
	setTimeout(() => {
		console.log(i);
    }, 1000)
}

Q2:

"use strict";

var myClosure = (function outerFunction() {
  var hidden = 1;
  return {
    inc: function innerFunction() {
      return hidden++;
    }
  };
}());

myClosure.inc();  // 返回 1
myClosure.inc();  // 返回 2
myClosure.inc();  // 返回 3

Q3:

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
//问:三行a,b,c的输出分别是什么?
var a = fun(0); //undefined
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0
var b = fun(0).fun(1).fun(2).fun(3); // undefined 0 1 2
var c = fun(0).fun(1); // undefined 0
c.fun(2); // 1
c.fun(3); // 1

Q4:

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn(),
  b = 200
x() // 30

Q5: const 和 let 声明的全局变量是否在 window 上?

ES5中,var 声明的全局变量会提升,并且直接挂载到全局对象的属性上,
所以在 window 上能看到 var 声明的变量。
在 ES6 中,let、const 声明的变量不会提升(所以会出现暂时性死区),
并且会在一个块作用域中,如下所示,所以在window里访问不到
let a = 10;
const b = 20;
相当于:
(function() {
    var a = 10;
    var b = 20
})()