什么是闭包
wiki定义:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数
的编程语言中实现词法绑定
的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量
(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。
简化下: 闭包是由 函数 和 创建该函数的 静态词法作用域 组合而成的。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
在这个定义下 一个函数只要有能力访问外部作用域的变量 即形成闭包。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
所以任何js函数都可以成为闭包 因为js是静态词法作用域
。 函数的作用域链在解析阶段就已经确定了。任何函数都具备访问外层数据的能力。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
mdn定义:
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域
。在 JavaScript 中,闭包会随着函数的创建而被同时创建。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
在mdn定义下需要满足几个条件:
- 需要 静态词法作用域
- 内部函数和外部函数
- 内部函数访问了外部函数的作用域(不是有能力访问而是实际访问了)
我们这里以mdn的定义为标准,同时这也是浏览器判断是否为闭包的标准...文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
闭包判断
- case1: 普通函数
arduino
function test() {
let text = 'Hello'
console.log('test', text);
}
- case2: 普通函数
arduino
const text = 'Hello'
function test2() {
console.log('test2', text);
}
test2 访问了外部作用域的变量 但是没有外层函数 所以不是一个闭包文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
debug调试也可以证明 浏览器并没有识别这个函数是一个闭包。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
- case3: 这也是一个普通函数
kotlin
function outer() {
const outObj = {
name: 'outer',
};
function inner() {
const innerObj = {
name: 'foo',
};
return innerObj;
}
return inner;
}
const inner = outer();
inner();
这个函数比较让人迷惑 大家可能会认为inner是一个闭包函数 仔细看我们会发现 无论是outer还是inner 都没有访问外部作用域的变量 inner函数只是生命周期被延长了 但这并不是构成闭包的条件。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
我们通过chrome调试也可以看到,并没有形成闭包。 文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
- case4: 在inner函数上做些改动
kotlin
function outer() {
const outObj = {
name: 'outer',
};
function inner() {
const innerObj = {
name: 'foo',
};
console.log('test outObj: ', outObj)
return innerObj;
}
return inner;
}
const inner = outer();
inner();
在inner函数内部访问outer函数作用域中的对象outObj. 可以看到 此时chrome显示有一个闭包 并且被访问的外部变量是outObj.文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html
- case5:在case3的基础上 直接访问全局变量
kotlin
var gObj = {
name: 'global'
}
function outer() {
const outObj = {
name: 'outer',
};
function inner() {
const innerObj = {
name: 'foo',
};
console.log(gObj)
return innerObj;
}
return inner;
}
const inner = outer();
inner();
这个例子满足静态词法作用域 满足内层函数&外层函数 但是内层函数没有访问外层函数数据 而是直接访问了全局的gObj
对象 这情况chrome会认为是闭包嘛?
可以看到浏览器认为这并不形成闭包。
- case6:这次返回一个对象
kotlin
function outer() {
const outObj = {
name: 'outer',
};
function inner() {
const innerObj = {
name: 'foo',
};
console.log(outObj)
return innerObj;
}
return {
inner
};
}
const out = outer();
out.inner();
这里outer执行后返回一个新对象 对象内部引用了inner函数。 同样形成了闭包。
- case7: 没有返回值
ini
let Global_inner;
function outer() {
const outObj = {
name: 'outer',
};
function inner() {
const innerObj = {
name: 'foo',
};
console.log(outObj)
}
Global_inner = inner;
}
outer();
Global_inner();
这里通过直接赋值给Global_inner
,看下是否可以形成闭包 这里如图 浏览器认为是一个闭包。
关于闭包定义的总结
- 闭包必须要有内层函数外层函数嘛?
答:根据mdn定义和浏览器的实测表现 必须要有内外函数 - 闭包必须要返回一个函数嘛?
答: 非必须,只是比较常见的使用方式而已 - 闭包必须要访问函数中的变量嘛?
答:根据浏览器的表现 必须要访问另一个函数的数据 才能形成闭包。 - 函数访问全局中的变量会形成闭包嘛?
答:根据浏览器的表现 不会形成闭包 参考case5。
所以通过实践我们再次确认了满足闭包的三个条件是缺一不可的
一句话概括:在一个函数中访问另一个函数的数据 即形成闭包。
函数的其他形式
从上面的例子中我们可以知道 闭包是函数的一种表现形式
。
在js中函数有多种表现形式,例如作为普通函数
, 对象的方法
, 构造函数
, 立即执行的iife函数
, 箭头函数
, 作为函数的入参
, 作为函数的返回值
, 作为一个函数对象
, 生成器函数
, 闭包函数
。
函数对于js非常重要。我们知道js中可以有多种编程范式或者编程风格,例如 面向对象编程,元编程, 异步编程 ,但是和js最为契合的一定是函数式编程, 因为js中函数是第一公民
如何理解闭包
闭包是函数的一种表现形式 那么他和普通函数在执行上会有什么差异?我们可以通过执行栈和堆内存的引用情况分析下。
普通函数执行机制
scss
function outer() {
const outObj = {
name: 'outer',
};
function inner() {
const innerObj = {
name: 'foo',
};
return innerObj;
}
inner();
}
outer();
执行栈和内存情况:
- 运行前解析阶段:
- 为全局上下文开辟内存空间
- 为outer开辟内存空间 初始化outer作用域链【scope】: 全局对象
- 为inner开辟内存空间 初始化inner作用域链【scope】: outer地址
- 在outer内部声明inner函数引用指向inner内存地址, 声明outObj变量指向outObj对象的内存地址
- 执行阶段
- 执行outer函数 outer函数进入执行栈
- outer函数内部 执行了inner函数
- inner函数入栈
- inner执行完成出栈
- outer函数执行完成出栈
- 清空内存对象
闭包函数执行机制
kotlin
function outer() {
const outObj = {
name: 'outer',
};
function inner() {
const innerObj = {
name: 'foo',
};
console.log('test outObj: ', outObj)
return innerObj;
}
return inner;
}
const inner = outer();
inner();
- 运行前解析阶段和普通函数相同。
- 执行阶段
- 执行outer函数 outer函数进入执行栈
- outer执行完成 返回值被全局属性inner引用了 outer出栈。
- inner函数入栈继续执行
- inner函数执行完成
- 因为inner被全局引用 所以全局有指针指向inner 而inner又可以访问到outer
- 所以垃圾回收机制(GC)后内存依然存在 不会被释放
区别
普通函数执行完成 会出栈 内部数据会被清空。
闭包函数 外层函数执行完成 内层函数持有外层函数的作用域链 因此外层函数中的数据不会被清空 依然可以被内层函数访问到。只有清空了内层函数的引用 才能释放内存。
应用场景
控制数据访问权限
js实现私有属性的六种方式 文章中我们通过闭包函数,实现了数据的私有化,达到了控制变量访问权限的作用。
javascript
function createObject() {
var privateProperty = 'I am private';
return {
getPrivateProperty() {
return privateProperty;
},
};
}
var obj = createObject();
console.log(obj.getPrivateProperty()); // "I am private"
console.log(obj.privateProperty); // undefined
延长变量生命周期 缓存数据
- 防抖
javascript
function debounce(fn, delay) {
let timer;
return function (...arg) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(fn.bind(this, ...arg), delay);
};
}
function test(...args){
console.log('test args', args, this)
return args;
}
const dTest = debounce(test, 200);
dTest.call({name: 'call'}, 1,2,3,4,5);
缓存计时器字段,每次调用判断run函数 可以获取到外层的timer是否存在 存在清空计时器。可以实现防抖功能。
- 节流
javascript
function throttle(fn, delay) {
let lastCallTime = 0 ;
return function (...arg) {
if (Date.now() - lastCallTime < delay) {
return;
}
lastCallTime = Date.now();
fn.call(this, ...arg);
};
}
缓存上一次执行时间。
- react中的闭包
scss
function MyComp = () => {
const [count, setCount] = useState(0)
useEffect(() => {
setInterval(() => {
console.log('test timer count', count)
}, 1000)
}, [])
return <div onClick={() => setCount((pre) => pre + 1)}>{count}</div>
}
相信每个刚刚接触react + hooks的同学都有这样的疑问 点击按钮视图正常更新,为什么定时器里获取到的count一直是0。
- 首先计时器传入的回调函数访问了外部的count数据 因此形成了闭包 即使MyComp执行完成,依然可以访问到作用域的数据
- 因为react的useEffect hooks的依赖列表为【】。因此多次执行MyComp函数这个hook回调 只会执行一次。读取到的一直是之前的旧数据。
闭包与内存泄漏
内存泄漏的本质:不被使用的变量没有被及时释放,这通常是因为变量的生命周期大于其作用域
导致的。
闭包可以延迟变量的生命周期,这是闭包的主要用途之一,因此闭包容易引起内存泄漏。
闭包为什么对js这么重要
闭包贯穿了多个核心基础概念。
首先,闭包的本质是函数,捕获了其静态词法作用域,从而形成了作用域链。这使得函数能够跨越其定义环境访问外部变量,是对作用域链和内存引用管理的深刻理解。
其次,闭包揭示了 JavaScript 函数作为一等公民的重要特征。函数不仅可以作为值传递,还能保留其定义时的上下文,这为函数式编程提供了强大支持。
通过闭包,我们能够创建私有变量、实现数据封装和模块化,提升代码的安全性和可维护性。因此,掌握闭包是全面理解和高效使用 JavaScript 的关键。
评论