在写这篇文章之前,我对闭包的概念和原理还比较模糊。 我仍然通过用简单的内部函数包装外层来误导自己......我并不是说这些说法是对还是错。 我只是不想有从众心理。 或者我可以说,如果说的再好一点,低一点的话,颜值都会提升好几个档次。 。 。
说到闭包,它们是 JavaScript 的两大核心技术之一(异步和闭包)。 笔试和实际应用都离不开它们。 甚至可以说,它们是判断js工程师实力的重要指标。 下面我们将列出几个关于闭包的常见问题,并从回答问题的角度来理解和定义大家心目中的闭包。
问题如下:
1.什么是闭包?
2.闭包的原理可不可以说一下?
3.你是怎样使用闭包的?
闭包简介
我们先来看看几本书中的大致介绍:
2、函数对象可以通过作用域进行关联,函数体内的变量可以存储在函数作用域中。 这在计算机科学文献中称为“封闭”。 所有 javascript 函数都是闭包。
4.函数之间可以通过作用域链相互关联,函数内的变量可以存储在其他函数的作用域中。 这些特征在计算机科学文献中被称为闭包。
可见,它们各自都有自己的定义,但要解释的含义却因时而异。 笔者之前就知道这一点,但不知道为什么。 最后我花了三天时间从词法作用域到作用域链的概念再到闭包的生成做了一个总体的回顾,发现我的生活变得清晰了很多。 ..
让我们抛开那些具体的、难以理解的叙述,从头开始理解、内化并最终总结出我们自己的那句关于封闭的话语。 我认为这对于笔试和丰富开发者自身的理论知识很有帮助。
要理解词法作用域,就得先说说JS的编译阶段。 大家都知道JS是弱类型语言。 所谓弱类型是指不使用预定义变量的存储类型。 并不能完全概括JS与其他语言的区别。 这里引用黄皮书中给出的解释编译语言(《你不知道的JavaScript》)。
编译语言
编译型语言在执行之前必须经历三个阶段。 这三个阶段就像过滤器一样,将我们编写的代码转换为语言内特定的可执行代码。 比如我们写的代码是vara=1;,而JS引擎内部定义的格式是var,a,=,1,那么它们就需要在编译阶段进行转换。 这只是一个比喻,实际上这只是编译阶段第一阶段所做的事情。 下面我们总结一下这三个阶段所做的工作。
这就是我们里面讲的。 虽然我们编写的代码是字符串,但在编译的第一阶段,这些字符串被转换为词法单元(toekn)。 我们可以将词汇单位想象为在我们体内分解的表达式。 那样。 (注意,这一步有两种可能。目前这是一个动词,词法分析将在下面与词法作用域一起讨论。)
有了词法单元后,JS 还需要继续分解代码中的句型,以减轻 JS 引擎的负担(总不能让引擎运行时经历那么多轮的转换规则吧?) ,通过词汇单元生成。 抽象语法树(AbstractSyntaxTree)javascript 块作用域,它的作用是为JS引擎构造一个程序语句树,我们简称AST。 这时,我们不禁想起了Dom树(有点远)。 是的,它们都是树。 以var,a,=,1为例。 它将分层定义它们。 例如:顶层有一个stepA包含With“v”,stepA下有一个stepB,stepB包含“a”,所以它们一层层嵌套……
3. 代码生成(rawcode)
这个阶段主要要做的就是将 AST 转换成 JS 语言内部识别的代码(这是语言内部制定的,不是补码代码)。 在生成过程中,编译器会询问作用域。 仍取vara=1; 例如,编译器首先会询问作用域当前是否存在变量 a。 如果有,它将被忽略。 否则,将在当前作用域中创建名为 a 的变量。
词汇阶段
哈哈,终于到了词汇阶段了。 看完前面的三个阶段,你是不是一头雾水? 你没想到js会有这么长的经历吗? 虽然里面总结的只是所有编译型语言最基本的流程,但是对于我们JS来说,它在编译阶段做的事情可不仅仅是这些。 它会提前为js引擎做一些性能优化等工作。 事实上,编译机做了所有脏活累活。
说到词法阶段的概念,我们就得把它和未完成的动词/词法分析阶段结合起来……
词法作用域发生在编译阶段的第一步,即动词/词法解析阶段。 它有两种可能性,动词分析和词法分析。 动词是无状态的,而词法分析是有状态的。
那么我们如何判断是否存在状态呢? 以vara=1为例,如果token生成器判断a是否是一个独立的token单元,它会调用一个有状态的解析规则(生成器不知道它是否依赖于其他token单元,因此需要进一步解析)。 另一方面,如果不需要生成器来确定,并且是不需要赋予语义的代码(可以暂时理解为不涉及范围的代码,因为我们不知道js内部定义了什么规则),那么就会包含在动词中。
现在我们知道,如果token生成器不确定当前token是否独立,就会进入词法分析,否则就会进入动词阶段。
简单来说javascript 块作用域,词法作用域就是词法阶段定义的作用域。 词法作用域由编写代码时变量和块级作用域的写入位置决定。 词法解析器(这里只看做解析词法模式的解析器,后面会介绍)处理代码时,作用域会保持不变(动态作用域除外)。
本节我们只需要了解:
什么是词法范围?
词汇阶段动词/词汇分析的概念是什么?
它们对词法作用域的创建有什么影响?
本节有两个被忽视的知识点(词法解析器、动态作用域),由于题目限制没有写下来。 稍后有机会我会向大家介绍他们。 范围从下面开始。
作用域链 1.执行环境
执行环境定义函数可以访问的变量或其他数据。
环境栈可以暂时理解为一个链表(JS引擎的存储栈)。
在网页浏览器中,全局环境即窗口是最里面的执行环境,每个函数也有自己的执行环境。 当一个函数被调用时,该函数将被扔进环境堆栈中。 当他和他的依赖成员被执行后,堆栈弹出它的环境。
先看一张图吧!
环境栈也称为函数调用栈(是同一个东西,只是前者的命名方式更倾向于函数),这里我们也称之为栈。 位于环境堆栈的最内层是window,只有当浏览器关闭时才会从堆栈中销毁。 每个函数都有自己的执行环境。
此时我们应该知道:
每个函数都有对应的执行环境。
当函数执行时,当前函数的环境将被压入环境堆栈中。 当前函数执行完成后,环境将被销毁。
函数调用栈和环境栈的区别。 这三个就像JS中的原始类型和基本类型 | 引用类型、对象类型和复合类型!
执行环境,所谓环境很容易与房子的概念联系起来。 是的,它就像一座大房子。 它不是独立的。 它会承载或与其他概念相关联,以便完成更多的任务。
每个执行环境都有一个代表变量的对象--------变量对象。 该对象存储当前环境中的所有变量和函数。
变量对象对于执行环境很重要,并且在函数执行之前创建。 它包含当前函数中的所有参数、变量和函数。 这个创建变量对象的过程实际上就是初始化函数内部数据(函数参数、内部变量、内部函数)的过程。
当前环境执行之前,无法访问变量对象中的属性! 而进入执行阶段后,变量对象转变为活动对象,可以访问上述属性,然后开始执行阶段操作。 所以活动对象实际上是变量对象实际执行时的另一种方式。
function fun (a){
var n = 12;
function toStr(a){
return String(a);
}
}
在 fun 函数的环境中,存在三个变量对象(在被推入环境堆栈之前)。 第一个是参数,即变量 n 和函数 toStr。 在被压入环境堆栈之后(在执行阶段),它们都属于fun的活动对象。 一开始,活动对象仅包含一个变量,即参数对象。
此时我们应该知道:
每个执行环境都有一个对应的变量对象。
环境中定义的所有变量和函数都存储在该对象中。
对于函数来说,执行前的初始化阶段称为变量对象,执行过程中成为活动对象。
3.作用域链
当代码在环境中执行时,会创建变量对象的作用域链。 以数据格式表示的作用域链的结构如下。
[{当前环境的变量对象},{内层变量对象},{内层变量对象},{窗口全局变量对象}]每个链表单元都是作用域链的一块,这个块就是我们的变量目的。
作用于链上的后端始终是当前执行代码所在环境的变量对象。 全局执行环境的变量对象始终是链中的最后一个对象。
看前面的简单例子,我们可以先想一想,各个执行环境中的变量对象是什么? 这两个函数的变量对象是什么?
我们以乐趣为例。 当我们调用它时,将创建一个包含参数 a 和 b 的活动对象。 对于一个函数来说,在执行的一开始,它的活动对象只包含一个变量,即arguments(当执行流程步骤并创建其他活动对象时)。
在活动对象中,它仍然代表当前的参数集。 对于函数的主动对象,我们可以将其想象为两部分,一是固定参数对象,二是函数中的局部变量。 在此示例中,a 和 b 都算作局部变量。 尽管 a 已经包含在参数中,但它仍然属于。
大家有没有注意到,在环境栈中,所有的执行环境都可以形成相应的作用域链。 我们可以非常直观地拼接成环境堆栈中的相对作用域链。
我们简单说一下这段代码的执行过程:
当 foo 创建时,作用域链已经包含一个全局对象,并存储在内部属性 [[Scope]] 中。
执行完foo函数并创建执行环境和活动对象后,取出函数的内部属性[[Scope]],建立当前环境的作用域链(取出后只剩下全局变量对象,然后附加它自己的活动对象)。
执行过程中遇到了fun,所以继续使用之前对fun的操作。
fun 的执行结束并从环境堆栈中删除。 因此,foo也被执行并继续被删除。
JavaScript 监听到 foo 没有被任何变量引用,开始实现垃圾回收机制,清除占用的显存。
虽然作用域链是指向当前执行环境的变量对象的指针列表,但它只是一个引用,但并不包括在内。 ,因为它的形状就像一条链,而且它的执行过程也非常一致,所以我们称之为作用域链。 当我们明白了其中的奥秘后,我们就可以抛开这些方法的约束,从原则上出发了。
此时我们应该知道:
什么是作用域链。
作用域链的生成过程。
内部属性的概念[[范围]]。
使用闭包
从头到尾,我们把涉及到的所有技术点都过一遍了。 写得不是很详细,有些不准确。 由于没有事实论证,我们只能粗略地了解这个过程的概念。
所涉及的理论已经充实,所以现在我们要使用它。 让我们从一些最简单的计数器示例开始:
var counter = (!function(){
var num = 0;
return function(){ return ++num; }
}())
function counter(){
var num = 0;
return {
reset:function(){
num = 0;
},
count:function(){
return num++;
}
}
}
function counter_get (n){
return {
get counte(){
return ++n;
},
set counte(m){
else {
n = m; return n;
}
}
}
}
相信很多朋友听完这句话,都已经预料到了他们的执行结果。 它们都有一个小特点,就是执行过程返回一个函数对象,并且返回的函数中包含对外部变量的引用。
为什么我们必须返回一个函数?
由于函数可以提供一个执行环境,当在这个环境中引用其他环境的变量对象时,前者不会被js内部的回收机制移除。 所以当你在当前执行环境中访问它时,它仍然在显存中。 这里不要混淆环境堆栈和垃圾回收这两个非常重要的过程。 环境堆栈就是调用堆栈,调用在其中移入并在调用后移出。 垃圾收集正在窃听引用。
为什么总是能增加呢?
前面提到,返回的匿名函数构成了一个执行环境。 该执行环境作用域链下的变量对象不是自己的,而是其他环境中的变量对象。 仅仅因为它引用了其他人,js 就不会对其进行垃圾收集。 所以这个值仍然存在,并且每次执行都会递增。
以这个函数为例。 我们使用闭包来实现它,但是当我们使用完它之后会发生什么呢? 不要忘记还有一个变量对其他变量对象的引用。 这时,为了让js能够正常回收,我们可以自动将形参设置为null;
以第一个为例:
我们看一下里面的代码。 第一个返回一个函数,后两个类似于方法。 它们都可以非常直接地展示闭包的实现,尽管更值得我们关注的是闭包实现的多样性。
结束语笔试题
1. 使用属性访问器实现关闭计时器
参见上面的例子;
2. 看代码猜输出
function fun(n,o) {
console.log(o);
return {
fun:function(m){
return fun(m,n);
}
};
}
vara=fun(0);a.fun(1);a.fun(2);a.fun(3);//未定义,?,?,?varb=fun(0).fun(1).fun (2).fun(3);//未定义,?,?,?varc=fun(0).fun(1);c.fun(2);c.fun(3);//未定义,?, ?,?
这道题的难点不只是闭包,还有递归等过程。 作者在回答这个问题的时候也答错了,真是恶心。 下面我们来分析一下。
我们先来说一下闭包部分。 Fun 返回一个可由 .fun 访问的 fun 方法。 运算符(这更容易理解)。 在返回的方法中,其活动对象可以分为[arguments[m],m,n,fun]。 在问题中,变量引用(接收返回的函数)用于此活动对象。
返回的函数中,有一个外部形参m。 收到形参后,再次调用fun函数并返回。 这次执行fun的时候,附加了两个参数。 第一个是刚才的外部参数(也就是调用时赋值的)。 注意第二个是上次fun的第一个参数。
第一种是将返回的fun赋值给变量a,然后单独调用返回的fun。 返回的fun函数中的第二个参数n只是取回我们上次调用内部fun时使用的参数。 但它没有被束缚。 可以看到我们调用了四次,但这四次只是在第一次调用外部fun的时候传入的。 之前通过a调用的内部fun并不会影响o的输出,所以仔细想一想,不难看出最终的结果是undefine0,0,0。
第二个是链式调用。 乍一看,和第一个没有什么区别,只是第一个多了一个中间变量a。 不要被你所看到的所迷惑!!!
// 第一个的调用方式 a.fun(1) a.fun(2) a.fun(3)
{
fun:function(){
return fun() // 外层的fun
}
}
//第二个的调用方式 fun(1).fun(2).fun(3)
//第一次调用返回和上面的一模一样
//第二次以后有所不同
return fun() //直接返回外部的fun
看里面的return,第二个区别是,第二次调用的时候又收到了{fun:returnfun}的返回值,但是第三次调用的时候是外部的fun函数。 当你明白了第一点和第二点之后,我相信你也会明白第三点。 最终结果我就不告诉你了,你可以自己测试一下。
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, 1000 );
}
for (var i = 1; i <= 5; i++) {
(function(i){
setTimeout( function () {
console.log(i);
}, 1000 );
})(i);
}
上面例子中的两段代码,我们在笔试的时候一定见过第一段。 这是一个异步问题。 它不是闭包,但是我们可以通过闭包的方法来解决。
第二段代码会输出1-5,因为在循环中的每次bounce中都引用了参数i(即活动对象),而在之前的循环中,每次bounce都引用了一个变量i,尽管我们仍然可以用其他更简单的方法解决它。
for (let i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, 1000 );
}
let 为我们创建了一个本地作用域,这和我们刚才使用的闭包解决方案是一样的,只不过这是 js 内部创建的一个临时变量。 我们不必担心过多的引用它会导致内存溢出问题。
总结我们所知道的
本章涵盖的范围稍广,主要是为了让你对闭包有一个更全面的了解。 那么到目前为止你知道什么? 我想每个人心里都有答案。
1.什么是闭包?
闭包是词法作用域的必然结果。 在函数中使用对活动对象的伪装引用可以防止其被回收,但会产生仍然可以通过引用访问其作用域链的结果。
```
(function(w,d){
var s = "javascript";
}(window,document))
```
一些观点将这些方法称为封闭,并说封闭可以防止全球污染。 首先,在这里你应该有自己的答案。 上面的例子是闭包吗?
确实可以防止全球污染,但不能说封城。 它最多在全局执行环境之上创建一个新的辅助作用域,从而避免需要全局定义其他变量。 请记住,这并不是真正的闭包。
2. 能解释一下闭包的原理吗?
结合我们之前所说的,它的症结是从词法阶段开始的,词法作用域是在这个阶段产生的。 最后根据调用环境形成的环境栈生成由变量对象组成的作用域链。 当一个环境正常不被js垃圾回收时,我们仍然可以通过引用访问它的原始作用域链。
3. 如何使用闭包?
使用闭包的场景有很多。 作者最近一直在研究函数式编程。 可以说,闭包似乎是js中函数式编程的重要基础。 以不完整功能为例。
前面的栗子是保留fun函数的活动对象(arguments[])。 其实我们日常开发中还有比较复杂的情况,需要很多的功能块。 在某些时候,我们关闭的真正力量可以凸显出来。 。
文章到这里就差不多结束了。 都是我自己的观点和书中的一些内容。 希望能给你带来一些影响。 其实这是一个积极的...如果文章中有不恰当的描述或者你有更好的我想强调一下我的观点,谢谢。
主题:
阅读一篇文章或阅读一本书的几页只需要几分钟。 而理解它需要一个个人内化的过程,从输入到理解到内化到输出。 这是一个非常合理的知识体系。 我认为这不仅仅是为了结束,对于任何知识来说都同样重要。 当个人知识融入我们的身体时,就需要将其输出并告诉他人。 这不仅是“奉献”的精神,也是自我完善的过程。