前置概念 在正式看闭包之前,我们先来学习一下前置知识,那就是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 如果只是根据前面的作用域的内容来看,这个程序是无法正常运行的,会报错,说变量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) } 等等。。。。。。。。。。。。。。