闭包
前言
前面我们了解到变量为什么会存在声明提升,也就是 js 的执行机制。现在,这一篇文章带你攻克号称最难理解的闭包。
词法环境
首先,让我们来看一下这个代码:
function varTest() {
var x = 1;
if (true) {
var x = 2;
console.log(x);
}
console.log(x);
}
通过上一篇文章的学习(JS入门必读:JavaScript 执行机制),我们可以画出他的编译过程:输出为 2 2。
那修改一下 if 语句,输出会有变化吗?
function varTest() {
var x = 1;
if (true) {
let x = 2; //var => let
console.log(x);
}
console.log(x);
}
输出会变成:2 1。 为什么输出会改变呢?
首先我们要知道,let 和 const 定义的变量会被放进词法环境中,词法环境会维护一个新的栈,每一个块级作用域都会形成一个块级上下文,入词法环境的栈(维护块级作用域之间的变量不相互冲突)
让我们再来看看这个函数的执行上下文:
那我们可能会有这样一个问题,第二个 console.log 为什么会找词法环境中的 x 而不去找变量环境中的?
if、while这种语句里,除非用let、const声明,才是块级作用域。块级作用域里的变量不影响块级作用域外的变量。
所以第二个 console.log 放在了块级作用域里,肯定是去找词法环境中的 x 。
========================================================================
我们再来看一个复杂一点的代码,能不能画出这个函数的执行上下文。
function foo() {
var a = 1
let b = 2
{ //这里 花括号{} 和 let 会形成块级作用域,但是 var 不会
let b = 3
var c = 4
let d = 5
console.log(a); //1
console.log(b); //3
}
console.log(b); //2(块级上下文出栈)
console.log(c); //4
console.log(d); //报错no defined
}
foo()
来看看 foo 执行上下文
v8 的规则是,查找变量先查找自己的词法环境,在词法环境的规则是从上往下,没有再从词法环境来到变量环境查找。
也就是这种顺序:
那我们第一个 console.log(a) ,最后是在变量环境中找到,a = 1 ;
console.log(b) ,在词法环境中的最上面的栈中找到,b = 3 ;
第二个 console.log(b) ,因为块级作用域执行完毕,所以( b = 3,d = 6 )这个栈销毁,所以我们在词法环境中的唯一一个栈找到 b = 2 ;
console.log(c) 在变量环境中找到,c = 4 ;
console.log(d) ,没有寻找到,d = undefined ;
作用域链
我们首先看这个代码,打印出来的会是什么?
function bar() {
console.log(myname);
}
function foo() {
var myname = '卢卡'
bar()
console.log(myname)
}
var myname = '奈布'
foo()
让我们来画出这个代码的编译执行情况:
这时候 bar 函数执行,要寻找 myname ,bar 执行上下文中找不到,那应该去哪里找呢?这时候我们要知道:
词法作用域:函数定义在了哪个域,这个域就叫该函数的词法作用域。
执行上下文有一个 outer 指针(外部引用),指向该函数的词法作用域,全局的 outer 为null
我们应该去 bar 函数的词法作用域去找,也就是全局执行上下文去找,找到 myname = '奈布'。
所以输出为:
v8 在查找变量的过程中,顺着执行上下文中的 outer 只想查清一整根链,这种链状关系就叫作用域链
来看看这个代码就懂了:
function main() {
let con = 3;
function foo() {
let con = 4;
function bar() {
let con = 5;
console.log(con)
}
bar()
}
foo()
}
main()
//作用域链:bar => foo => main =>全局
闭包
1. 闭包的概念?
我们先来看一个代码:
function foo() {
function bar() {
var a = 1;
console.log(b);
}
var b = 2
return bar //返回函数体
}
const baz = foo() // baz为bar,foo()执行完毕,执行上下文销毁
baz() //b要去bar的词法作用域找,foo()执行上下文被销毁,留下了闭包
通过我们之前的了解,我们画出的调用栈情况是不是这样子的:
我们现在就有一个疑问?bar 函数要找 b ,自己的执行上下文中没有,那就去找词法作用域,也就是 foo 的执行上下文,但是 foo 函数执行完毕执行上下文销毁,那这个代码会不会报错?
让我们来看看答案:
会输出 2 。
这是因为,foo 执行上下文被销毁,但是留下来了一个小区间,这个区间,也就是闭包(closure)。
在 js 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。当通过调用一个外部函数返回的内部函数时,即使外部函数调用完了(即销毁了其执行上下文),但是内部函数引用了外部函数中的变量,那么这些变量依然需要保存在内存中,我们把这些变量的集合称为闭包。
2. 为什么会有闭包这个概念?
词法作用域的规则 和 函数调用完毕他的执行上下文一定要被销毁 两个规则冲突。
3. 缺点:内存泄漏----调用栈的可用空间变得更少
存在很多个闭包,那么调用栈就会爆栈。
4. 优点:变量私有化,封装模块
//让这个代码实现累加效果,可以把 num 提出来变成全局变量。
//但如果是大型的项目代码,每要实现一个类似累加的功能,都要定义一个全局变量,
//那这个代码的可维护性和可读性将会大打折扣,这时候可以使用闭包,闭包的优点也就是变量私有化。
function add() {
let num = 0;
console.log(++num);
}
add() // 1
add() // 1
//使用闭包
function add() {
let num = 0;
return function () {
console.log(++num);
}
}
const res = add()
res() // 1
res() // 2
练习
面试问 :这个代码输出什么
function fn() {
var arr = []
for (var i = 0; i < 5; i++) {
arr.push(function() {
console.log(i)
})
}
return arr
}
var funcs = fn()
for (var j = 0; j < 5; j++) {
funcs[j]() //数组中的五个函数依次执行
}
// 5 5 5 5 5
面试官会继续问:我想要打印 0 1 2 3 4
function fn() {
var arr = []
for (var i = 0; i < 5; i++) {
function foo(n) { // n 不会被销毁,会保留在闭包中,等到将来 fn 的内部函数生效的时候,n 才会被访问。
arr.push(function() {
console.log(n)
})
}
foo(i)
}
return arr
}
var funcs = fn()
for (var j = 0; j < 5; j++) {
funcs[j]() //数组中的五个函数依次执行
}
//或者var i改成let i
这个是上个代码的调用栈,这里把循环给省略了。