# 一、 执行上下文
js引擎在执行每个代码段(全局代码、函数体)前,浏览器已经做了一些“准备工作”(预编译):
全局代码
- 变量、函数表达式 —— 仅声明不赋值(默认赋值undefined)
- this —— 赋值
- 函数声明 —— 赋值
函数体
- 参数 —— 赋值
- arguments —— 赋值
- 自由变量的取值作用域 —— 赋值 (注:自由变量的取值是在函数定义时而非函数执行时确定的)
经过以上“准备工作”就生成了执行上下文(执行上下文环境),所以可以给执行上下文下个定义:
执行代码段之前(预编译阶段),对所有变量或函数声明进行赋值(有些默认赋值undefined),所形成的环境就是执行上下文环境。
上述两种代码段分别形成了全局执行上下文和函数块执行上下文,当函数调用完成时,对应的执行上下文及其中的变量就会被销毁,再重新回到全局执行上下文环境,处于活动状态的执行上下文只有一个。而这种压栈出栈形成的数据栈就是执行上下文栈。
# 二、 作用域
JS作用域分为:全局作用域、函数作用域、块作用域(ES6)。
函数作用域在函数定义时就确定了,而不是函数调用时。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
自由变量: 在 B 作用域中使用了变量 x,却未在 B 作用域中定义,那么 x 就称为 B 作用域的自由变量。
自由变量 x 应该到创建 B 作用域的那个作用域(假设为 A 作用域)中取值。若 A 作用域中也未定义变量 x,则继续到创建 A 作用的那个作用域中寻找,直至寻找到全局作用域,这就是作用域链。
总结:
- 作用域是一个区域,用于隔离变量的有效范围。它是在定义时确定的,与是否调用无关。
- 执行上下文是预编译后,由所有变量和函数所组成的变量环境。它是在调用时确定的,不同的调用会产生不同的执行上下文。
- 沿着定义时形成的作用域包含关系,向上寻找自由变量取值的方式,就是作用域链。
注:沿着作用域链查找变量,但实际是到变量所在的执行上下文的 VO(变量对象)中取到变量的值。
# 三、 闭包
由于作用域链的存在,我们很容易理解下边的代码:
// 全局作用域 A
var a = 6;
function fn() {
// 函数作用域 B
var b = 8;
console.log(a + b);
}
fn(); // 14
因为作用域 B 中的 a 是自由变量,所以要到定义 B 的 A 中寻找 a 的取值,即在正常作用域链中,函数内部可以取到函数外部的变量值,反之则不行。
那么,有没有办法让函数外部取到函数内部的变量呢? —— 这就是闭包所要实现的功能
闭包有两种情况:函数作为返回值,函数作为传参。 函数作为返回值
// 全局作用域 A
function fn() {
// 函数作用域 B
var a = 6;
return function getA() {
// 函数作用域 C
console.log(a)
}
}
var f1 = fn()
f1() // 6
函数作为传参
var a = 6,
getA = function() {
// 函数作用域 B
console.log(a)
}
(function fn(f) {
// 函数作用域 A
var a = 8
f() // 6
})(getA)
要讲清楚闭包实现的原理,就要结合作用域和执行上下文来理解。
以第一种情况为例:
作用域是在定义时形成的,自由变量会沿作用域链寻找取值。
定义时形成了作用域 A、B、C,且 a 是 C 中的自由变量,所以 a 可以沿着作用域链找到 6。执行上下文是在调用时的预编译形成的,执行结束后就会销毁。
- 当代码开始执行时,全局执行上下文入栈,处于活动状态;
- 当执行 var f1 = fn() 时,fn执行上下文入栈,处于活动状态;
- 执行完 var f1 = fn(),正常来说应该销毁fn执行上下文,但返回了一个函数getA,并引用了fn作用域下的fn执行上下文中的变量a,因为被引用那么a不能被销毁,所以a所在的fn执行上下文不能被销毁,依然存在于执行上下文栈中。这个被返回的函数getA就是闭包。(所以说闭包会增加内容开销,使用不慎会导致内存泄漏)
- 当执行 f1() 时,即在执行闭包getA,getA执行上下文入栈,并处于活动状态,由上分析可知,getA中的自由变量a可沿作用域链找到fn执行上下文中的a的值,即为6。
- 执行完 f1,getA执行上下文、fn执行上下文、全局执行上下文依次出栈并销毁。
# 四、综述
# 1. 什么是闭包?
- 从功能上来讲,闭包可以简单理解成“可以读取其它函数内部变量的函数”;从形式上来讲,通常是“定义在一个函数内部的函数”或者“被作为参数传入另一个函数中的函数”。
- 闭包的实现原理与作用域及执行上下文密切相关。正常情况下,当代码执行结束时相应的执行上下文就会销毁,但是当闭包中的自由变量引用了上游作用域中的变量时,上游作用域对应的执行上下文就无法销毁,那么当闭包函数执行时,就能获取到上游作用域对应执行上下文中的变量。
- 闭包会使得函数中的变量被保存在内存中,增加内存消耗,一旦滥用,会造成网页性能问题。
# 2. 闭包会导致内存泄漏吗?
根据司徒正美的一篇文章 js闭包测试 (opens new window),结合个人理解得出以下结论:
内存泄漏与闭包本身没有关系,而是因为不同浏览器的js引擎,对闭包情形下采用了不同的垃圾回收策略;
举例来说,对于以下情形:
function outer() { var a = {xxx} var b = 'bbb' return function() { console.log(b) // 闭包中只使用了父作用域中的 b,直观理解未使用 a,执行var inner = outer()后,a 应该被垃圾回收机制回收 } } var inner = outer()
对于 IE8 (JScript) 及以下版本,执行 var inner = outer() 后,a 依然存在于内存中,当大量执行 var inner = outer() 后将暂用大量内存资源,甚至导致内存泄漏;
对于 chrome (V8),执行 var inner = outer() 后,a 不存在了。针对 IE8 (JScript) 及以下版本因垃圾回收机制导致的内存泄漏,可采取以下措施:
function outer() { var a = {xxx} var b = 'bbb' // 退出函数之前,将闭包中不使用的变量删除 a = null return function() { console.log(b) } } var inner = outer()
# 3. 闭包的应用场景?
封装私有变量
function f1() { var sum = 0; var obj = { inc:function () { sum++; return sum; } }; return obj; } var result = f1(); console.log(result.inc()); //1 console.log(result.inc()); //2 console.log(result.inc()); //3
setTimeout 或 事件回调 传参数
原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果function f1(a) { function f2() { // 这是实际想传给setTimeout的函数 console.log(a); } return f2; } var fun = f1(1); setTimeout(fun, 1000); // setTimeout不支持直接传参 f2(1)
与setTimeout类似,原生的addEventListener传递的回调函数不能带参数,通过闭包可以实现传参效果
function changeSize(size){ return function(){ document.body.style.fontSize = size + 'px'; }; } var size12 = changeSize(12); var size14 = changeSize(20); var size16 = changeSize(30); document.getElementById('size-12').addEventListener(size12) document.getElementById('size-20').addEventListener(size20) document.getElementById('size-30').addEventListener(size30)
事件防抖中计时
事件触发n秒后再执行回调,如果n秒内再次被触发,则重新计时。
由于需要一个变量保存计时,为了保持全局纯净,可以借助闭包来实现。// fn 需要防抖的函数 // delay 需要延迟触发的时间 function debounce(fn, delay) { let timer = null return function() { if (timer) { clearTimeout(timer) } timer = setTimerout(fn, delay) } } // 使用 function fn() {...} let fun = debounce(fn, 100) document.querySelector('#btn').addEventListener(fun)
← 字符串和数组常用方法 欺骗词法作用域 →