这段时间看到群里的老哥们在讨论这道面试题, 正好很久以前研究过, 所以结合自己所学,总结一下闭包作用域中遇到的问题,以及这几道奇奇怪怪的题的机制.如果你对JavaScript的运行机制还不是很清楚, 可以先看目录的后半部分, 再回来看题目哦.
本文内容:
废话不多说上题
题目
如果您做对了这几道题, 并且明白其中的机制与运行规律, 那这篇文章您就可以跳过啦
先不要看答案, 自己推算一下哦
{
function aoo(){}
foo = 1
}
console.log(aoo)
{
function aoo(){}
foo = 1
function aoo(){}
foo = 2
}
console.log(aoo)
var a = 1
function func(a, y = function anonymousl() { a = 2 }){
var a = 3;
y();
console.log(a)
}
func(5);
console.log(a)
答案
{
function aoo(){}
foo = 1
}
console.log(aoo) // ƒ foo(){}
{
function aoo(){}
foo = 1
function aoo(){}
foo = 2
}
console.log(aoo) // 1
var a = 1
function func(a, y = function anonymousl() { a = 2 }){
var a = 3;
y();
console.log(a) // 3
}
func(5);
console.log(a) // 1
是不是感觉很意外? “第一题感觉输出是1
才对啊, 第二题输出应该是2
才对啊, 第三题函数内部为啥输出是3
” 我刚开始遇到这个问题的时候, 也十分纳闷, 显然这不符合逻辑. 不过经过查阅, 我大概明白了是怎么回事. 没关系, 我会仔细地解释这其中发生了肾么事, 看完文章, 你肯定也能懂😁
造成这个问题最根本的原因是 块级作用域 的出现, ES6以后出现了块级作用域, 导致了这样奇怪的机制, 这是因为JavaScript需要兼容ES3,ES5,ES6出现的语法. 在旧版浏览器中, 因为没有块级作用域, 所以代码是按照我们常规推算的方式执行的, 不过在ES6以后就有需要注意的地方了.
剖析
这里为了方便各位看官看得舒服, 我先直接抛出我总结的结论, 如果对你有帮助给我点个赞吧👍~
题1/2结论:
-
除了函数 / 对象的大括号外, 在其他
{}
大括号中出现了let
/const
/function
声明的变量时, 这个大括号会生成一个单独的块级作用域 -
在变量提升阶段函数在全局下只声明不提升, 但在块级作用域内的变量提升阶段, 函数正常声明和定义
-
在块级作用域内, 当代码执行到函数声明处, 会把这之前对函数的所有操作映射一份给全局下的步骤2声明的函数变量
概念可能有些难于琢磨, 不过放心下面我会通过题目仔细阐述这个结论
我们先来直接debuger一下题1的代码, 在chrome调试器中看看代码执行是否出现了块级scope.()
如图, 我们可以清晰地得出步骤1
的结论, JavaScript自行产生了一个私有的Block, 并且在变量提升的时候, 它同时会在全局下声明,但变量不会预先定义 ( function变量提升的时候会直接声明 + 定义 )
继续执行代码:
我们可以看到全局下的aoo变成了函数, 这是因为在执行过了function aoo() {}
的代码行之后, JavaScript会将把这之前对aoo的操作映射一份给全局下的Global.
继续执行代码:
因为块级作用域Block在执行完毕后, 被GC
回收了, 就没有办法看到块内的aoo变量,所以我加了一个console.log()
, 我们可以看到块内的aoo
变量, 被替换成了1
这很好理解, 赋值操作就近原则嘛.🤭
题2结论:
作用机制: 如果当前函数使用了形参默认值, 并且函数中有基于 let
/ const
/ var
声明的变量(无论变量名是否与形参一致),则在函数执行时除了生产一个私有的上下文,还会在{}
块内生产一个块级上下文(该块级作用域链指向函数的上下文)
注意: (如果函数体中的变量名与形参一致,则在块级作用域代码执行之前会把形参变量的值同步一份给同名的私有变量)
我们debuger一下函数看一下执行过程:
如图可以看出, 执行到函数体内的时候, 出现了两个块级作用域, 一个是函数体本身(Local)的, 而另一个则是额外产生的块级作用域(Block), 并且由于形参和块内声明的变量名相同, 所以块级作用域的代码执行之前, 把形参变量的值同步了一份给块级作用域内的变量.
由于形参默认值是一个函数, 我们需要考虑他的作用域的问题, 这里为了方便理解, 我画出了他们三者的作用域的关系. 注意: 从图我们可以看出, 块级作用域与形参函数作用域是没有任何关系的.(绿色箭头为作用域链方向)
继续:
根据就近原则, 块内的赋值操作, 是对块内a的操作, 所以将他改为了3
代码继续下行, 执行函数y()
:
根据作用域链, 他的代码会去改变Local内的变量a
, 但是我们从图片可以看出,console.log(a)
印的是lock
的a
, 所以输出的是3.
怎样? 搞懂了吗, 是不是发现明白了其中的执行过程, 似乎也不是一个太难的过程~(●ˇ∀ˇ●)
JavaScript运行机制
-
当浏览器(内核/引擎)渲染和解析JavaScript代码的时候, 会给JS代码提供一个可以运行的环境, 即 “全局作用域(Global/Window scope)”
-
代码自上而下执行(执行前存在变量提升)
什么是栈与堆?
这里我们仅从JavaScript代码的执行角度去探讨这两种数据结构
-
执行环境栈(Execution Context Stack):
- 这是一个供JavaScript代码执行, 全局变量和原始值存储的地方.
-
执行上下文(Execution ContextGlobal)* :
-
区分代码执行环境
-
全局下的代码在此处执行
-
-
变量对象VO(Variable Object )/活跃对象AO( Active Object ):
-
这是每个执行上下文中都会存在的东西, 用来存储他们的值.
-
个人理解, 这两个对象是一个东西, 活跃对象其实就是一个被激活, 正在执行的变量对象
-
-
堆内存(heap):
- 这里是一个存放引用数据类型的地方, 如果我们是在浏览器下运行JavaScript代码, 那就会有一个window的全局对象, 存放着各种各样的API供我们调用.
-
全局对象GO(Global Object):
- 一个存放在堆中的对象, 存储了许多浏览器内置的API, 如:JSON/console/setTimeout
这里我们用图来大概理解一下他们的关系:
一个等号赋值操作的过程
-
原始值: 在栈内存中找一个单独的位置存储.
-
声明变量(Declare) 把变量存储到当前上下文的变量对象(VO)中
-
把创建的变量和值关联在一起也就是定义(Define)
等号赋值优先级的问题大家可以参考MDN, [运算符优先级 - Javascript | MDN]
JavaScript的垃圾回收机制(GC)
垃圾回收机制的核心作用机制: 堆内存被占用(地址被引用的), 则不会被回收, 否则, 则在浏览器空闲时释放内存
可达性: 即当前所有的变量和参数, 是否可以从全局对象延伸访问到, 如不行,则回收
闭包是怎么产生的?
根据上述,我们可以得知, 每一个函数执行,都会产生一个私有的执行上下文, 这么做是为了保护自己私有的变量不被外界污染, 正常来说, 一个函数执行完毕, 他的上下文也会随即被释放, 但是如果发现这个执行上下文中的变量, 仍然在其他地被访问, 那么这个上下文就不会被释放, 这样的机制我们就称之为闭包
总结: 闭包是一种机制, 为了保护函数内的变量不被污染, 另一方面如果内存不被释放, 它又能被保存起来供其他上下文引用, 这样保护
+ 保存
的机制 我们就称为闭包
"那么, 是不是一定要return function(){}
才能产生闭包的?"
那肯定不是嘛, 这只是闭包机制的一种产生形式, 但是无论在什么样的上下文中, 只要我们能做到一个上下文被另一个上下文访问,从而导致他不被回收, 那都能产生闭包, 如:
function A(a) {
// 形参赋值 a: 1
A = function (b) {
console.log(a + b++)
}
}
A(1) //
A(2)
我们来看看这个函数的执行产生的上下文:
最顶上的是函数堆内存, 我们忽略它, 根据代码执行来看, 很明显, 执行代码A(1),再执行完毕后 他就没有作用了, 但是因为它在新A函数执行的上下文中打印的时候要被引用, 所以这个内存会一直保存着, 直到代码A(2)执行完毕.
感谢😘
如果觉得文章内容对你有帮助:
-
❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章
个人公众号: 前端Link
联系作者: linkcyd 😁
往期:- ()[]