阅读 703
闭包其实也不难理解

闭包其实也不难理解

前言

在刚开始学习javascript的时候,要说哪些地方哪些概念不太好理解,那我觉得闭包算一个,但是闭包在前端中又是一个非常重要的部分,可以说我们日常开发中一定都用到了闭包,只是我们自己没有注意,同时闭包也是前端面试频繁涉及的部分,那么今天我们就来重新盘点一下闭包部分相关的知识~

前置概念

在正式看闭包之前,我们先来学习一下前置知识,那就是JS中的作用域,我们知道,在ES5之中,作用域分为两种:全局作用域和函数作用域,随着ES6的到来,新增了块级作用域,想更好的理解闭包,那么搞清楚作用域是首要条件

全局作用域

我们知道,对于变量而言,我们一般会分成两类:全局变量和局部变量,一般定义在最外围环境的为全局变量,定义在函数当中的为局部变量,在web浏览器中,全局变量一般挂载在window对象上,所以全局变量在任何地方都可以进行访问,但是局部变量便只能在所在作用域内才可以被访问

我们结合一个例子来简单的理解一下:

var globalVar = '全局变量'
function func() {
  console.log(globalVar)    // 全局变量
  var localVar = '局部变量'
  console.log(localVar)    // 局部变量
}
func()
console.log(globalVar)     // 全局变量
console.log(localVar)     // 报错:localVar is not defined

========分割线================
function func() {
  globalVar = '全局变量'
}
console.log(globalVar)   // 全局变量
console.log(window.globalVar)   // 全局变量
复制代码

从这段代码我们可以发现,globalVar作为全局变量和localVar作为局部变量的特点:

  • 全局变量拥有全局作用域,无论在哪都可以访问,在web浏览器端是挂载在window对象上面的
  • 局部变量是被定义在特有的局部作用域内,并且只能在所在的作用域内被访问
  • JS中没有经过定义直接被赋值的变量默认为全局变量(不考虑严格模式下)

函数作用域

函数作用域,顾名思义,那就是函数内部的作用域,在函数内部定义的变量称之为函数变量,那么此时函数内部定义的变量便只能在该函数中被调用

一样看一个简单的例子:

function fun() {
  var funVar = '函数内变量'
  console.log(funcVar)   // 函数内变量
}
fun()
console.log(funcVar)   // funcVar  is  not defined
复制代码

我们可以发现我们在函数内定义的变量就只能在函数内部访问,当函数执行完毕之后,该变量就会被销毁掉,所以我们是无法在外部进行访问的

块级作用域

在ES6中,引入了块级作用域的概念,而ES6规定,在某个区块中一旦使用let和const声明了一个变量,那么这个区块就会变成块级作用域(如if/for中的{}里面就是块级作用域)

我们也来看看代码:

console.log(blockVar)    // blockVar is not defined
let blockVar = '块级作用域变量'

for (var i=0; i<6; i++) {}
console.log(i)    // 6

for (let i=0; i<6; i++) {}
console.log(i)   // i is not defined
复制代码

从这段代码中我们可以看出,块级作用域中的变量有着“暂时性死区”的特点,也就是说这些变量不能在定义前被调用了;然后就是使用如let这类关键词声明的变量就只能在所在的块级作用域内被调用,在外面是无法访问的

闭包

看完了上面的关于作用域的前置知识,接下来我们就正式开始理解闭包,先来看看有关闭包的一些概念,先搞清楚闭包到底是什么

闭包的定义

先来看下MDN的定义:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

再来看下红宝书中的定义:闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常用方式,就是在一个函数内部创建另一个函数

是不是感觉不看还好,一看更加懵逼了?不要慌,咱们一条条的分析,整理出我们容易懂的语言:

  • “闭包是指有权访问另一个函数作用域中的变量的函数”——去掉修饰词只留主谓宾,那得出的结论是“闭包是函数
  • “闭包让你可以在一个内层函数中访问到其外层函数的作用域”——换句通俗的话说:闭包是一个可以访问其他函数内部作用域变量的函数
  • “一个函数和对其周围状态的引用捆绑在一起”——那就是说闭包要形成那函数就需要保持对上层作用域的引用

那从上面这些我们可以简单的得出一个结论:那就是通过闭包,我们可以实现在函数外访问这个函数内部的变量的功能

我们来看一个简单的例子:

function func1() {
  var a = 'func1'
  return function func2() {
    console.log(a)    // func1
  }
}
func1()()
复制代码

如果只是根据前面的作用域的内容来看,这个程序是无法正常运行的,会报错,说变量a没有定义,但是我们结合了闭包的定义之后我们发现它是可以正常打印的,也就是说在func2里面访问到了func1中的变量,结合这些现在你是否理解了红宝书中对闭包的定义了呢?

为什么会产生闭包

了解了闭包的定义和基本概念,那接下来我们再来具体分析一下为什么会产生闭包呢? 我们先来了解一个概念:作用域链,其实比较好理解,比如我们在访问一个变量的时候,会首先在所在作用域内查找,如果没有找到就会往上找到上层作用域内,一层层向上直到找到或者到达顶层作用域window(web浏览器端)为止,这整个形成的一个链条状的就是作用域链

我们也来看一个简单的例子:

var b = '全局作用域变量'
function func1() {
  var b = 'func1作用域变量'
  function func2() {
    var b = 'func2作用域变量'
    console.log(b)       // func2作用域变量
  }
  return func2
}
func1()()
复制代码

我们来看这个🌰,此时func1的作用域就指向全局作用域和自己本身作用域,而func2就从下往上依次链接自己本身->func1作用域->全局作用域

所以还记得MDN中那句话吗:“一个函数和对其周围状态的引用捆绑在一起”,也就是说产生闭包的原因就是需要当前函数内保持对上层作用域的引用

闭包的具体应用

前面两部分分析了一下闭包的主要内容,开篇我们就说起闭包在我们日常开发其实是非常常见的,只是可能我们平时并没有太过注意,那接下来我们就来盘点一下闭包的一些具体场景

1、我们平时肯定都有用过定时器、事件监听以及Ajax请求等这类使用回调函数的,基本都利用到了闭包,使用定时器的例子,如防抖/节流:

// 防抖
const debounce = (fn,delayTime) => {
  let timerId, result
  return function(...args) {
    timerId && clearTimeout(timerId)
    timerId = setTimeout(()=>result=fn.apply(this,args),delayTime)
    return result
  }
}
// 节流
const throttle = (fn, delayTime) => {
  let timerId
  return function(...args) {
    if(!timerId) {
      timerId = setTimeout(()=>{
        timerId = null
        return result = fn.apply(this,args)
      },delayTime)
    }
  }
}
复制代码

2、IIFE(立即执行函数),这种函数比较特别,它拥有独立的作用域,不会污染全局环境,但是同时又可以防止外界访问内部的变量,所以很多时候会用来做模块化或者模拟私有方法

举个例子:

var global = '全局变量'
let Anonymous = (function() {
  var local = '内部变量'
  console.log(global)    // 全局变量
})()
console.log(Anonymous.local)   // local is not defined

=======分割线==============

var global = '全局变量'
let Anonymous = (function() {
  var local = '内部变量'
  console.log(global)    // 全局变量
  return {
    afterLocal: local
  }
})()
console.log(Anonymous.afterLocal)   // 内部变量
复制代码

3、函数作为参数传递的形式

var a = '全局变量'
function func1() {
  var a = 'func1内部变量'
  function func2() {
    console.log(a)
  }
  func3(func2)   // func1内部变量
}
function func3(fn) {
  // 闭包产生
  fn()
}

func1()
复制代码

经典面试题

我们先来看具体代码:

for(var i = 1; i < 6; i++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}
复制代码

这道题相信我们很多人曾经都遇到过,我们打印出来结果发现是打印的5个6,那为什么是这个结果呢?那如果我想改造之后让他打印12345该怎么做呢?

首先我们来回答为什么会是这个结果,以前我们主要是是站在eventLoop的角度来说的,现在我们可以从两部分来说:

  • 因为setTimeout是宏任务,但是JS是单线程,由于eventLoop机制,需要先执行主线程同步代码之后才会执行宏任务,所以会打印全是6
  • 因为setTimeout是一种闭包,它引用上层作用域中的全局变量i,而此时i已经是6了,所以就全部打印的都是6

那我们怎么改造让他按照顺序打印结果呢,这里提供几种常见的方法:

1、ES6的let:这是改造成本最小的一种方法,因为let创造了块级作用域,代码的执行以块为单位来进行,便可以达到我们的要求

for(let i = 1; i < 6; i++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}
复制代码

2、IIFE(立即执行函数):利用这种方法每次循环的时候,都将此时的变量i传入到setTimeout当中

for(let i = 1; i < 6; i++){
  (function(j) {
    setTimeout(function() {
      console.log(j)
    }, 0)
  })(i)
}
复制代码

3、利用setTimeout的第三个参数:我们一般就只用前两个参数,第三个参数其实就是可以进行传参数给函数

for(let i = 1; i < 6; i++){
  setTimeout(function(j) {
    console.log(j)
  }, 0, i)
}
复制代码

也还有其他一些比较巧妙的方法也可以实现这个要求,方法很多就不一一举例了,有兴趣可以自己研究一波~

好啦,关于闭包的部分到这就差不多了,感觉这部分不是说有多难,主要是看每个人自己怎么去理解,每个人的理解方式都不太一样,没事儿的时候一起刷一刷红宝书,多思考思考咯,一起学习进步~

文末

最后如果文章对你有用的话,欢迎点赞👍、关注😌,谢谢~ 也欢迎关注【前端光影】公众号,获取更系统完整的前端模块学习!

文章分类
前端
文章标签