开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第28天,点击查看活动详情
闭包作为最重要的
JavaScript
特性之一,是我们在工作当中某些场景需要经常使用的,也是面试中经常问到的,而对于这一问题并没有一个最标准的答案,因为其并不是规范,而是一种前端程序员默认的特性,接下来我们从多维度来剖析一下他~
首先要了解闭包,就要知道其根源,为什么会出现闭包,这点我们就要从 JavaScript
的作用域机制、变量访问机制,来入手,而作用域又分为几种:全局作用域、函数作用域、块级作用域,我们就一一复习一下~
从新认识作用域
全局作用域
需要配合预解析理解
-
- 全局作用域在页面打开的时候创建,页面关闭时销毁
- 编写在 script 标签中的变量和函数,作用域为全局,在页面的任何位置都可以访问到
- 在全局作用域中有全局对象 window,代表一个浏览器窗口,由浏览器创建,可以直接调用
- 全局作用域中声明的变量和函数会作为window对象的属性和方法保存
- 全局作用域中有name属性 window.name = ''
全局作用域预解析
-
- 创建 GO 对象
- 找变量声明 将变量名作为GO对象的属性名 值是undefined
- 找函数声明 值赋予函数体
块级作用域
-
- 将函数书写在块语句中,命名函数只会预解析,不会预赋值,执行块语句的时候,赋值函数 如果块语句中出现变量和函数名相同,执行语句块内最后打印的是正常顺序赋值的结果 在块语句中,不管有几个同名函数,都会被最后一个同名函数覆盖掉
- 在语句块外,变量是最后一个
同名函数上面的赋值变量
结果 语句块中,变量和函数名相同时,只有一个变量和函数名,函数在变量之前,则打印的是函数
console.log(a) // undefined
var a = 10
console.log(a) // 10
{
console.log(a) // 函数a
a = 2
console.log(a) // 2
function a(){}
console.log(a) // 2
a = 1
console.log(a) // 1
}
console.log(a) // 2 如果块语句中没有 a = 2 则打印的是函数
- ES6 默认值参数形成的 单独作用域
- 设置了参数的默认值,函数声明在进行初始化的时候,会形成一个单独的作用域(content),初始化结束后,这个作用域就会消失.在使用的时候,优先在自身作用域查找
- 形参里面有,优先在自身查找,形参里面没有同名的变量,就会去外部作用域找
- 函数的默认值只会向上查找,不会去函数作用域查找
// 函数形参里面使用了默认值,那么就会形成一个单独的作用域,在使用的时候,优先在自身作用域查找
var x = 1
function f(x , y = x) {
let x = 10 ; // 这里的 x 定义不会影响到形参的赋值
console.log(y)
} // 形参里面有,优先在自身查找
f(2) // 2
// 全局变量不存在的话,就会报错
function fn(y = n , a = a) { // a = a 这种情况也会报错 因为 a = a 形成了暂时性死区,如果传参了 就不会报错
let n= 2
console.log(y)
}
fn() // ReferenceError: x is not defined
函数作用域
-
- 调用函数时,函数作用域被创建,函数执行完毕,函数作用域销毁
- 每调用一下函数,就会创建一个新的函数作用域,互相之间独立
- 在函数作用域中可以访问到全局作用域的变量,函数外无法访问函数作用域内的变量
函数作用域的预解析
-
- 创建一个 ao 对象 { }
- 找到形参和变量声明,将变量和形参名,当作 ao 对象的属性名,值为undefined
- 实参 赋值 给形参
- 在函数体里面 找到声明式函数, 预解析函数名当作 ao 对象属性名 值赋予函数体
- 这里函数名和变量名重名的话,函数体的值会覆盖变量名的值,不论变量名的值是啥
- 赋值式函数 和 var 变量声明 预解析一样
作用域链
内层作用域能够访问外层作用域的变量, 每一个函数都存在他的一个作用域,这个作用域除了他本身作用域的变量外,还存储了外部作用域的变量,一层一层的存储下去就形成了作用域链 (函数嵌套最多64层, 一旦展开, 作用域链占用的内存就越大, 一般达到64层达到占用内存的顶峰)
-
会被保存到一个隐式的属性中去,[[scope]] ([[ ]] 包裹的时候运行时属性) 这个属性是我们用户访问不到的,但是是存在的,是让 js 引擎来访问的,里面存储的就是作用域链 AO 和 GO的集合
-
预解析的时候
- 全局作用域预解析会产生一个 GO (Global)是个对象 ,全局声明的函数在定义的时候,就会预解析,被挂载到 GO 对象里面
- 函数在执行的时候,预解析会产生一个 AO 是个对象,里面放的都是函数内的预解析出来的变量和函数
- 函数定义的时候, 函数这里的作用域链最顶端是GO
- 函数执行的时候, 函数这里的作用域链最顶端是AO 下面一层是GO
// 下图就是 一下函数的作用域链 function a() { var a = 123 function b() { var bb = 234 } b() } a() // 当函数调用结束的时候, 作用域链被销毁 a() // 从新调用函数,作用域链从新连接
从新认识函数
函数定义阶段
- 书写函数,不会执行函数体内的代码
-
- 在堆内存中开辟一段存储空间
- 会把书写在函数体内的代码,全部以字符串的形式储存,此时不会解析变量
- 把函数存储空间地址赋值给函数名
函数调用阶段
- 使用函数,会把函数体内的代码执行
-
- 按照变量存储的地址,找到函数存储空间
- 直接开辟一个新的函数执行空间
- 在执行空间内进行形参赋值
- 在执行空间内进行函数内部的预解析
- 把之前存储的代码在执行空间内完整执行一遍,此时才会解析变量
- 执行完毕,这个开辟出来的执行空间销毁
function fn(a) {
console.log(a)
function a() { console.log(111)}
}
fn(100)
+ 如果控制台打印出 100, 说明 先 预解析 后 形参赋值
+ 如果控制台打印出 函数体, 说明 先 形参赋值 后 预解析
结论 打印函数体
不会销毁的函数执行空间
- 当在函数体内返回一个复杂数据类型的时候
- 在函数外面有变量接收这个复杂数据类型
- 此时函数的执行空间不会销毁
- 作用: 延长了变量的生命周期
- 如何让这个空间销毁: 给变量重新赋值,或指向另外的地址
认识闭包
函数内部能够沿着作用域链 访问外部的变量, 这种特性叫做闭包
函数套函数是利用闭包的特性 形成闭包的作用域
- 概念:
- 函数嵌套函数 (函数内直接或间接的返回一个函数)
- 内部函数使用着外部函数的私有变量
- 参数和变量不会被垃圾回收机制回收 (一个不会销毁的函数执行空间)
- 特点:
- 延长了变量的生命周期
- 在函数外部可以操作内部的私有变量
闭包语法糖
- getter 获取器
- setter 设置器
function fn(){
let num = 100
return {
// 在函数内直接返回一个对象,利用获取器来返回num
get num () { return num },
//利用设置器进行对num的修改,传递的值就会覆盖到里面的num
set num (val) { num = val }
}
}
// 在外面接受函数返回值 是一个对象
const res = fn()
res.num // 就是拿到里面变量的值
res.num = 200 // 给里面的变量赋值
闭包循环添加事件
// 利用 for 循环 var 声明添加事件只会执行到最后一次
// 在 let 以前 都是用闭包 如下
var btns = document.querySelectorAll('button')
for(var i = 0 ; i < btns.length ; i++){
btns[i].onclick = (function(i){ // 自执行函数,点击之前已经执行
return function(){ //这个函数才是添加给点击事件的函数
// 这里用到 i 就会形成闭包 i 就是每一次点击的下标
console.log(i)
}
})(i)
}
函数柯里化
是一种函数的封装形式,多个参数的时候,把第一个参数提取出来
主要就是为模块化服务,让变量减少暴露
- 都是以闭包的形式进行封装
- 正则验证
// 例, 封装一个正则验证
function testName(reg){
// 直接返回了一个函数,返回的函数也用了外面函数的变量 reg
return function (username){
return reg.test(username)
}
}
// 在使用的时候,定义一个变量,传进去一个参数,返回一个方法
const res = testName(/^[^_]\w{5,11}$/)
// res 得到的是一个函数 res() 就是执行 拿到return结果
如: res('lihaibo') 这里就能够得到验证结果 是 true 或false
-
标准柯里化
目的是分步分时去统计结果,触发结果的条件是不带参数
每一次调用传参都是修改一次内部状态,只有当最后一次调用且没有传参的之后,将之前所有传入的参数打包,执行回调函数
function currying(callBack){
let arr = []
return function(){
if(arguments.length !== 0){
arr = arr.concat(Array.from(arguments)) // ES5写法: arr = [].concat.apply(arr,arguments)
return arguments.callee // 返回这个匿名函数,使得可以连续执行
}else{
let value = callBack(...arr) // ES5写法: callBack.apply(null,arr)
arr.length = 0
return value
}
}
}
反柯里化
Function.prototype.unCurring = fucntion(){
let fn = this
return function(...rest){
// 这里的 rest[0] 就是执行函数fn时,替代fn函数中的this的对象
// 这里的 rest[1...] 就是执行函数 fn 时传入的参数
return Function.prototype.call.apply(fn.rest) // 或 fn.apply(rest[0],rest.slice[1])
}
}
// call.apply(fn.reset) 这里的fn,是替代 call 里面的this, rest 是 依次传入 call里面 fn的参数