闭包
理解作用域链是理解闭包的基础,而闭包在 JavaScript 中几乎无处不在,同时作用域和作用域链还是所有编程语言的基础。
function bar() {
console.log(myName)
}
function foo() {
var myName = '13pro'
bar()
}
var myName = '13press'
foo()
function bar() {
// 加餐:this 指向?
console.log(this.myName)
}
function foo() {
var myName = '13pro'
bar()
}
var myName = '13press'
foo()
很遗憾,打印的都是全局作用域中的变量13press
。也就是说函数执行时的变量查找并非是按照调用栈顺序,实际上是通过另一种机制——作用域链 👇。
作用域链
每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。当一段代码使用了一个变量时,若是在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。我们把这个查找的链条就称为作用域链。
其中 outer 的指向是由词法作用域决定的。
词法作用域
我们常说的作用域就是指词法作用域。
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
也就是说词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。
Closure
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
通俗的讲就是内部函数引用外部函数的变量,就形成了闭包。
闭包的关键
function foo() {
var myName = 'zhang13pro'
let test1 = 1
const test2 = 2
var innerBar = {
setName: function (newName) {
myName = newName
},
getName: function () {
console.log(test1)
return myName
},
}
return innerBar
}
var bar = foo()
bar.setName('Lex')
bar.getName()
console.log(bar.getName())
当代码执行到 16 行时,闭包就产生了。堆内存空间保存着“closure(foo)”对象,被内部的 getName 和 setName 方法引用。虽然 foo 执行上下文销毁了,foo 函数中的对 closure(foo)
的引用也断开了,但是 setName 和 getName 里面又重新建立起了对 closure(foo)
的引用。查看 V8 的堆内存快照,setName 对象下面包含了 raw_outer_scope_info_or_feedback_metadata
,对闭包的引用数据就在这里面。
简言之,产生闭包的核心有两步:
- 第一步是需要预扫描内部函数;
- 第二步是把内部函数引用的外部变量保存到堆内存中。
闭包的作用
- 在函数外部访问函数内部变量,实现变量私有化
- 函数执行完后仍保存变量对象,不被回收。被引用的变量不会被GC.
闭包的应用
从应用中感受闭包:私有化数据,在此基础上保存数据。
- 防抖,延迟指定时间后执行。类比电梯门关闭。
Hello Debounce
function debounce(func, delay = 100) {
let timer
return function () {
if (timer) clearTimeout(timer)
timer = setTimeout(() => void func.apply(this, arguments), delay)
}
}
ele.addEventListener(
'input',
debounce(function (e) {
console.log(e + '防抖')
}, 1000)
)
- 节流,固定时间内执行一次。类比 LOL 技能 CD。
Hello Throttle
function throttle(func, wait = 100) {
let flag = true
return function () {
if (!flag) return
flag = false
setTimeout(() => {
func.apply(this, arguments)
flag = true
}, wait)
}
}
ele.addEventListener(
'input',
throttle(function (e) {
console.log(e + '节流')
}, 1000)
)
如果不使用闭包,再次触发事件时将取不到上次的 timer 或 flag。
小结
var bar = {
myName: 'https://13press.vercel.app/',
printName: function () {
console.log(myName)
},
}
function foo() {
let myName = '13pro'
return bar.printName
}
let myName = '13press'
let _printName = foo()
_printName()
bar.printName()
代码执行输出什么?产生闭包了吗?
answer
13press
13press
没有产生闭包
你可能会好奇为什么调用 bar 对象的 printName 方法获取的不是 bar 的内部属性myName
?特别是对于有其他编程语言经验的 Coder 来说,这无法理解。在对象内部的方法中使用对象内部的属性是一个非常普遍的需求,但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套机制——this。