闭包
重新认识函数
●一个函数分为函数的定义和函数的执行
函数的定义
●我们书写一个函数, 但是不会执行函数体内的代码
●那定义一个函数做了什么事情
○在堆内存中开辟一段存储空间
○把你书写在函数体内的代码, 全部以字符串的形式存储在这存储空间中, 此时不会解析变量
○把函数存储空间地址, 赋值给变量名(函数名)
函数的执行
●使用(调用)函数, 会把函数体内的代码执行
●调用函数又具体做了什么事情
○按照变量名存储的地址, 找到函数存储空间
○直接开辟一个新的函数执行空间
○在执行空间内进行形参赋值
○在执行空间内进行函数内部的预解析
○把之前存储的代码在执行空间内完整执行一遍, 此时才会解析变量
○执行完毕这个开辟出来的执行空间销毁
let num = 100
function fn(a, b) {
// 证明确实没有解析 num 存储
// 如果这里在存储的时候, 是解析了 num 存储的, 那么存起来的就是 100
// 后面的 num = 200 这句代码不会影响到这里的值
// 将来打印出来就是 100
// 如果这里在存储的时候, 没有解析 num 存储, 那么存起来的就是 num 变量
// 后面的 num = 200 这句代码会影响到这里的值
// 将来打印出来就是 200
// console.log(num)
const res = a + b
console.log(res)
}
num = 200
fn(10, 20)
fn(100, 200)
复制代码
函数形参赋值和与解析先后问题
function fn(a) {
console.log(a)
function a() { console.log(111) }
}
fn(100)
复制代码
分析
●当函数调用的时候, 如果先进行的 形参赋值
○1、给 a 变量赋值为 100
○2、进行函数内与解析的时候, 给 a 变量赋值为一个 函数体
■此时给 a 变量赋值的函数体就会把 100 覆盖
○3、执行的第一行代码, 打印 a 变量
■在这里就会打印出 函数体
●当函数调用的时候, 如果先进行的 预解析
○1、进行函数内预解析的时候, 给 a 变量赋值为一个 函数体
○2、进行形参赋值的时候, 给 a 变量赋值为 100
■此时给 a 变量赋值的 100 就会把函数体覆盖
○3、执行的第一行代码, 打印 a 变量
■在这里就会打印出 100
●结论
○如果控制台打印出 100, 说明 先 预解析 后 形参赋值
○如果控制台打印出 函数体, 说明 先 形参赋值 后 预解析
不会销毁的函数执行空间
●当你的函数内返回一个 复杂数据类型
●并且在函数外面有变量接受这个复杂数据类型的时候
●此时函数的执行空间不会销毁
不会销毁的函数执行空间有什么作用
●延长了变量的生命周期
如何能让这个空间销毁
●让你的外部变量指向别的位置, 只要不在保存这个内部地址
●这个执行空间就会被 浏览器 销毁掉
function fn() {
const num = 100
const obj = { name: 'Jack' }
return obj
}
let res = fn()
const res2 = fn()
// 销毁了执行空间
res = 200
复制代码
闭包
定义
●官方定义:就是能够读取其他函数内部变量的函数
●个人理解:
○需要一个不会销毁的函数执行空间
○函数内 直接 或者 间接 的返回一个函数
○内部函数使用着外部函数的私有变量
○我们称内部函数是外部函数的闭包函数
作用(优点)
●延长变量的生命周期
●在函数外部操作函数内部的私有变量
1.直接返回一个函数
function outer() {
var num = 100
function inner() {
return num
}
return inner
}
const res = outer()
// 当你需要在 outer 外面访问 outer 内的私有变量 num 的时候
// 没有办法直接访问
// 但是你可以通过调用 res 来访问
// 因为 res 存储的就是 inner 函数地址
// 所以这里就是在调用 outer 内的 inner 函数
// 又因为 inner 内没有私有变量 num, 会自动去到 父级作用域查找
// 也就是去到 outer 内查找私有变量 num , 发现有
// 索引返回的就是 outer 函数内的 私有变量 num
// n 得到的就是 outer 内私有变量 num 的值
const n = res()
复制代码
2.间接返回一个函数
●不直接返回函数, 把函数放在一个容器内返回
function outer() {
var num = 100
var str = 'hello world'
// 如果我向对象内放置一个函数, 那么返回的虽然是对象, 但是对象内有函数
var obj = {
getNum: function () { return num },
setNum: function (val) {
// 给 num 变量进行赋值
// 自己作用域内有, 给自己的赋值, 自己没有给父级的赋值
// 因为 setNum 内没有 num 变量
// 所以, 这里的赋值实际上是给 outer 函数内的私有变量 num 赋值
num = val
}
}
return obj
}
// res 接受的就是 outer 函数内返回的 obj 对象的地址
// res 和 outer 函数内的 obj 指向一个对象存储空间
var res = outer()
// 当你调用 res.getNum() 的时候, 就是在调用 outer 函数内的 obj 里面的 getNum 函数
// 因为 getNum 函数是 outer 的子级作用域
// 所以 getNum 内没有 num 的时候, 访问的还是 outer 函数内的私有变量 num
var n = res.getNum()
console.log(n)
// 因为 res 就是 outer 函数内的 obj 对象
// 所以, res.setNum 就是 obj 内的 setNum 函数
// 调用了 obj 内的 setNum 函数, 并且给 val 赋值为了 500
// 因为 setNum 函数内是在给 outer 函数的私有变量 num 赋值
// 所以这里的 500 被赋值给了 outer 函数的私有变量 num
// 在 outer 函数外面给 outer 函数内部的 私有变量 num 的值修改了
res.setNum(500)
console.log(res.getNum())
复制代码
沙箱模式
●沙箱:
○就是一个箱子里面装的都是沙子
○筛子会一点儿一点儿的漏出来
○不会一下子都漏出来
●利用 函数内 间接返回一个函数
○间接就是不直接
○把函数包裹在另一个数据结构中返回
○一般是 一个函数内返回一个对象 , 对象中书写多个函数
○意义就是为了返回多个函数
function outer() {
let a = 100
let b = 200
let str = 'hello world'
// 创建一个对象
const obj = {
// 书写一个函数, 目的是为了获取私有变量 a
getA: function () { return a },
getB () { return b },
setA: function (val) { a = val }
}
return obj
}
// 目的是为了得到 "箱子"
const res = outer()
// 利用 "箱子" 去访问 outer 函数内的指定数据
// 需要访问 a
console.log(res.getA()) // 100
console.log(res.getB()) // 200
// 需要修改 a
res.setA(1000)
console.log(res.getA()) // 1000
// 重新做了一个 "箱子"
const res2 = outer()
console.log(res2.getA()) // 100
复制代码
沙箱模式语法糖
●尽可能的简化沙箱模式的语法
●利用的是 getter 和 setter 来进行操作数据
●所有的框架的底层使用的大部分是这样的操作
<script>
/*
沙箱语法糖
+ 尽可能的简化沙箱模式的语法
+ 利用的是 getter 和 setter 来进行操作数据
*/
function outer() {
let a = 100
let b = 200
// 准备对象的时候, 按照 getter 获取器 和 setter 设置器的方式来设置
const obj = {
// getA 目的为了获取 a 成员, 使用 getter 获取器
get a () { return a },
// getB 目的为了获取 b 成员, 使用 getter 获取器
get b () { return b },
// setA 目的是为了设置 a 成员, 使用 setter 设置器
set a (val) { a = val },
set b (val) { b = val }
}
return obj
}
// res 得到的是一个 对象数据类型
const res = outer()
// 因为闭包的形式, 导致我们的所有操作
// 对象.方法名()
console.log(res)
// console.log(res.a)
// 在对象内, 直接给设置器名称赋值即可
// res.a = 1000
// console.log(res.a)
</script>
复制代码
案例-循环绑定事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button>1</button><button>2</button><button>3</button><button>4</button><button>5</button>
<script>
// 在没有let出现之前
const btns = document.querySelectorAll('button')
// 循环获取到每一个button按钮
for (var i = 0; i < btns.length; i++) {
// 给每一个按钮添加一个点击事件
// 使用的是一个自执行函数
btns[i].onclick = (function (index) {
// 返回一个函数
return function () {
// 使用传递进来的 i
console.log(index)
}
})(i) // i当做实参传递到函数里面
}
</script>
</body>
</html>
复制代码
函数柯理化
●最简单的柯理化函数就是把
●一次传递两个参数 , 变成两次每次传递一个参数
●利用了闭包 , 把第一次传递的参数保存下来(延长变量的生命周期)
// 需求: 求 10 + x 的和
// 之前的方式
function sum(x) {
return 10 + x
}
// 使用
const res = sum(10)
console.log(res);
// 需求: 求 20 + x 的和
function sum1(x) {
return 20 + x
}
// 使用
const res1 = sum1(10)
console.log(res1);
复制代码
●柯理化函数实现
// 我们利用闭包(柯理化)实现
function curry(init) {
return function (num) {
return num + init
}
}
// 使用
// 求 10 + x 的和
// const sum10 = curry(10);
// console.log(sum10(20));
// 代码优化
console.log(curry(10)(20));
// 求 20 + x 的和
// const sum20 = curry(20)
// console.log(sum20(20));
// 代码优化
console.log(curry(20)(20));
复制代码
函数柯理化封装
●通过我们上面使用的柯理化
●我们发现柯理化有两个功能
○一个是用来保存变量(收集变量)
○一个是用来实现逻辑(功能函数)
●我们柯理化函数的封装有可能是多个参数在相互利用
柯理化函数封装
●把一个功能函数按照 柯理化的方式 封装起来
●让这个功能函数变成一种 柯理化的 调用方式
// 准备功能函数
function fn(a, b, c, d) {
return a + '://' + b + ':' + c + d
}
// 准备柯理化函数
function currying(fn, ...arg) {
// ...arg 把除了功能函数以外的参数全部收集了
// console.log('功能函数 : ', fn)
// console.log('收集的基础参数 : ', arg)
// 保存外部 arg 变量
let _arg = arg || []
// 记录: 功能函数需要几个参数
// 语法: 函数名.length
// 得到: 该函数的 形参的个数
const len = fn.length
// 返回一个内部函数
return function (...arg) {
// 这个函数将来被调用的时候, 依旧需要接受参数
// 把本次调用的时候接受的实参, 和之前准备好的拼接在一起
_arg = [ ..._arg, ...arg ]
// 目前 _arg 就是基础准备的参数 和 本次调用时候传递的参数 放在一起的 数组
if (_arg.length < len) {
// 再次调用 currying 函数, 继续去收集参数
return currying(fn, ..._arg)
} else {
// 执行功能函数
return fn(..._arg)
}
}
}
// 将来使用的时候
// const r1 = currying(fn, 'http', 'localhost', '8080')
// const r2 = currying(fn, 'http', 'localhost')
// const r3 = currying(fn, 'http')
// const r4 = currying(fn)
// 提前准备三个参数
// const r1 = currying(fn, 'http', 'localhost', '8080')
// const r2 = r1()
// const r3 = r2()
// const r4 = r3('/a')
// console.log(r4)
// 提前准备两个参数
// const r1 = currying(fn, 'https', 'localhost')
// const r2 = r1()
// const r3 = r2('9999')
// const r4 = r3('/b/c/d')
// console.log(r4)
// 提前准备两个参数
// const r1 = currying(fn, 'https', 'localhost')
// const r2 = r1()
// const r3 = r2('8080', '/a/b/c')
// console.log(r3)
// 提前准备一个参数
// const r1 = currying(fn, 'https')
// console.log(r1('baidu.com', '8080', '/a'))
// console.log(r1('baidu.com')('8080')('/a'))