前端冲刺必备指南 - 执行上下文 / 作用域链 / 闭包 / 一等公民
哪吒人生信条:如果你所学的东西 处于喜欢 才会有强大的动力支撑。
每天学习编程,让你离梦想更新一步,感谢不负每一份热爱编程的程序员,不论知识点多么奇葩,和我一起,让那一颗四处流荡的心定下来,一直走下去,加油,2021
加油!欢迎关注加我vx:xiaoda0423
,欢迎点赞、收藏和评论
不要害怕做梦,但是呢,也不要光做梦,要做一个实干家,而不是空谈家,求真力行。
前言
每天努力一点点💪,就能升职加薪💰当上总经理出任 CEO 迎娶白富美走上人生巅峰🗻,想想还有点小激动呢😎。
我并不希望把👉这篇文章内容成为笔记去记,或者说是总结一些要点。而是希望通过这篇文章真正地去理解,掌握,一行一行的解析其内容本质,去思考✅每一行,每一段的内容。
希望能够把每一处知识点,说明白,(当然,如果哪一处不了解,可以在评论区进行探讨哦!)⏰,计时开始!
如果您发现本文有帮助,请您点赞,收藏,评论,留下您学习的脚印👣,我很乐意谈论😃
1. 执行上下文/作用域链/闭包
什么鬼,这是什么鬼?😑,想必有部分开发者懂,但是对于初学者或者说是(浅入学习者)来说,执行上下文和执行堆栈,在脑袋中想必是一片空白呢?📖,您说是不是?
1.1 那么什么是执行上下文?
执行上下文,它是比较抽象的概念,就是当前 JavaScript 代码被解析和执行时所在环境,so,在 JavaScript 中运行任何的代码都是在执行上下文中运行的。
执行上下文有三种类型:
🔷第一种类型:全局执行上下文
记住全局执行上下文,只有一个即一个程序中只能有一个全局执行上下文,如果是在浏览器中,那么全局对象就是 window 对象,this 指向就是这个全局对象
🔷第二种类型:函数执行上下文
函数执行上下文可以存在多个,甚至是无数个;只有在函数被调用时才会被创建(函数执行上下文),每次调用函数都会创建一个新的执行上下文
🔷第三种类型:Eval 函数执行上下文
Eval 函数执行上下文,什么鬼!这是神马?想必一部分程序员很少用过这,so,不必解释,但记住这是运行在 eval 函数中的代码,只有在 eval 函数中的代码才有 eval 函数执行上下文
理解了执行上下文(即环境),那么需要了解在 JavaScript 程序中的执行流,以及控制机制(执行堆栈)流程。
1.2 执行栈
其实执行堆栈(调用堆栈)具有后进先出结构的堆栈,该结构用于存储在代码执行执行期间创建的所有执行上下文。
压栈出栈的过程——执行上下文栈
当 JavaScript 引擎运行 JavaScript 代码时它会创建一个全局执行上下文并将其 push 到当前执行堆栈。(函数还没解析或者是执行、调用)仅存在全局执行上下文,每当引擎发现函数调用时,引擎都会为该函数创建一个新的函数执行上下文,并将其推入到堆栈的顶部(当前执行栈的栈顶)
当引擎执行其执行上下文位于堆栈顶部的函数之后,将其对应的函数执行上下文将会从堆栈中弹出,并且控件到达当前堆栈中位于其下方的上下文(如果有下一个函数的话)
执行上下文的生命周期:
创建过程:1.生成变量对象,2.建立作用域链,3.确定 this 的指向。
执行过程:1.变量赋值,2.函数引用,3.执行其他代码。
销毁阶段:执行完毕后出栈,,等待被回收。
现在,我们了解了 JavaScript 引擎如何管理执行上下文,那么如何创建呢?😲
1.3 执行上下文的创建
学习如何创建执行上下文,执行上下文分两个阶段创建:
第一种:创建阶段-执行上下文
第二种:执行阶段-执行上下文
执行上下文是在创建阶段创建的,创建阶段发生的事情:
创建阶段-执行上下文
确定 this 的指向,this 确定或设置的值
在全局执行上下文中,this 的值指向全局对象,在浏览器中,this 的值➡window 对象;在 nodejs 中指向的是➡module 对象
在函数执行上下文中,this 的值取决于函数的调用方式(即如何被调用的)。当它被一个引用对象调用,则将的值 this 设置为该对象,否则 this 的值将的值 this 设置为全局对象或 undefined(在严格模式下)
下面来看看 this 的代码示例:
抽象地,词汇环境在伪代码中看起来像这样:
LexicalEnvironment(词法环境)组件已创建
首先看到词法环境?究竟什么是词法环境呢?这个名词概念如何理解?😮
那么首先上来就是,词法环境的定义:
官方规范对词法环境的说明,词法环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联。
词法环境是保存标识符,变量映射的结构。(这里的标识符是指变量/函数的名称,而变量是对实际对象(包括函数对象和数组对象)或原始值的引用)
词法环境由一个环境记录和可能为空引用(Null)的外部词法环境组成。通常,词法环境和 ECMAScript 代码的特定语法结构相关联。
环境记录是在词法环境中存储变量和函数声明的地方。
环境记录主要适用两种环境记录:声明性环境记录和对象环境记录。环境记录分别是声明式环境记录,对象环境记录和全局环境记录。(全局环境记录在逻辑上是单个记录,但是它被指定为封装对象环境记录和声明性环境记录的组合)
声明性环境记录(绑定了包含在其作用域内声明定义的标识符集),就是它存储变量和函数声明,功能代码的词法环境包含一个声明性环境记录。
对象环境记录(绑定对象),全局代码的词法环境包含一个客观环境记录,除了变量和函数声明外,对象环境记录还存储全局绑定对象。so,对于每个绑定对象的属性,将在记录中创建一个新的条目。
so,对于功能代码来说,环境记录中包含一个 arguments 对象,该对象包含传递给该函数的索引和参数与传递给该函数的参数的长度之间的映射。
如下代码:
对外部环境的引用,意味着它可以访问其外部词法环境,如果在当前词法环境中找不到变量,则 JavaScript 引擎可以在外部环境中查找变量。
这里就听不懂了,词法环境有两个组成部分:
环境记录,记录相应环境中的形参,函数声明变量声明等(存储变量和函数声明的实际位置)
对外部环境的引用,可以访问其外部词法环境
用伪代码表示:
环境记录记录了在其关联的词法环境作用域内创建的标识符绑定。👇
其实词法环境就是描述环境的对象,先确定当前环境的外部引用,环境记录初始化,就是常遇到的声明提前,全局代码执行之前,先初始化全局环境;函数代码执行之前,先初始化函数环境。
全局环境(用于表示在共同领域中处理所有共享最外层作用域的 ECMAScript Script 元素)是一个没有外部环境的词法环境,so,全局环境的外部环境引用为 null。
模块环境是一个包含模块顶层声明绑定的词法环境,它的外部环境是一个全局环境。
函数环境是一个对应于 ECMAScript 函数对象调用的词法环境。
现在用代码表示词法环境:
这段代码的词法环境表示:
执行阶段-执行上下文
在此阶段,将完成对所有这些变量的分配,最后执行代码。
VariableEnvironment(变量环境)组件已创建
在 ES6 中,词法组件和变量环境组件之间的区别是前者用于存储函数声明和变量(let 和 const)绑定,而后者仅用于存储变量 var 绑定。
说说变量提升的原因,在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined 或保持未初始化。
so,这就是为什么可以在声明之前访问 var 定义的变量,但如果在声明之前访问 let 和 const 定义的变量就会提升引用错误的原因。
现在举个例子:
js 在执行这段代码时,创建了一个词法环境(global environment- ge),确定(ge)的环境记录,里面包含了 da1,da2,foo 标识符的记录,设置外部词法环境的引用,因为(ge)已经在最外面了,so,外部词法环境引用就是 Null,到此(ge)就确立完毕了。
接着执行代码,当执行到 foo(),js 调用了 foo 函数,foo 函数是一个(FunctionDeclaration),js 开始执行函数创建了一个新的词法环境表示为(ge2),设置(ge2)的外部词法环境引用,很明显就是(ge),(ge2)的环境记录(da3,da4)。
所有创建词法环境以及环境记录都是不可见的,在编译器内部完成
示例词法环境:
1.4 JavaScript 执行上下文栈过程
思考,JavaScript 引擎并非一行一行分析和执行程序,而是一段一段地分析执行。如何管理创建的那么多执行上下文?
so,JavaScript 引擎创建了执行上下文栈来管理。
1.5 面试题
解释如下:
因为 JavaScript 采用的是词法作用域,函数的作用域基于函数创建的位置。
第一个内部函数 f 在初始化时,会建立一个活动对象,它会添加一个属性名为 scope 的属性,会给它建立一个隐藏属性[[scope]],这个就是用于指向父级活动对象的。在到这个函数执行时,scope 会被赋值,顺着它的[[scope]]就可以找到父级的值,返回一个代指的变量,继续返回到函数外部。输出 local scope
第二个内部函数 f 在初始化的时候也是建立一个活动对象,这个活动对象上会添加一个属性名为 scope 属性。也会建立一个指向父级活动对象的[[scope]]隐藏属性。在 checkscope 第一次执行进入 checkscope 函数体的时候返回的是 f 指针值(对内部函数的一个引用),而非第一个返回的直接就是个原始值变量。第二次执行才进入 f 函数体,内部活动对象及[[scope]]私有属性已经建立,它便顺着这条链查找 scope 变量的值,并返回,形成闭包。
对于函数对象来说,当外层函数执行完就该销毁所有变量的,但此时一个函数指针被返回了,就意味着外部跟函数内部建立了联系,这个指针指向函数内部区域,它无法销毁,作用域链还在,so,内部那个函数就可以访问到私有变量了。
变量对象,每一个执行上下文都会分配一个变量对象,变量对象的属性由变量和函数声明构成。在函数上下文情况下,参数列表也会被加入到变量对象中作为属性,变量对象与当前作用域相关。
不同作用域的变量对象互不相同,它保存了当前作用域的所有函数和变量。
> 只有函数声明会被加入到变量对象中,而函数表达式不会。
```
// 函数声明
function da(){}
console.log(typeof da); // "function"
// 函数表达式
var da2 = function da1(){};
console.log(typeof da2); // "function"
console.log(typeof da1); // "undefined"
```
当 Js 编辑器开始执行的时,会初始化一个全局对象用于关联全局的作用域,对于全局环境而言,全局对象就是变量对象。
之前提到变量对象对于程序而言是不可读的,只有编译器才有权访问变量对象。在浏览器端,全局对象被具象成 window 对象,即全局对象===window===全局环境的 variable object。
当函数被调用,那么一个活动对象就会被创建并分配给执行上下文。则将其活动对象作为变量对象,活动对象由特殊对象 arguments 初始化。
arguments 对象,这个对象在全局环境中是不存在的
示例如下:
dada 被调用时,在 dada 的执行上下文会创建一个活动对象 AO,并且被初始化为 AO=[arguments],随后 AO 被当做变量对象 variable object,vo 进行变量初始化,此时 VO=[arguments].concat([name,love,jog])。
词法作用域,词,单词,法,语法,就是单词(标识符,原始值,操作符等),语法就是 JavaScript 中的各种语法规则,so,词法作用域在 js 中,一种全局,一种函数。
作用域控制着变量和参数的可见性以及生命周期,在一块代码块中定义的所有变量在代码块的外部是不可见的 ,定义在代码块中的变量在代码块执行结束后会释放。在函数中的参数和变量在函数外部是不可见的,在一个函数内部任何定义的变量,在该函数内部都是可见的。
JavaScript 采用词法作用域,也就是静态作用域,函数的作用域在函数定义的时候就决定了。
1.6 动态作用域
动态作用域,函数的作用域是在函数调用的时候才决定的。
总而言之,作用域的好处是内部函数可以访问定义他们的外部函数的参数和变量,除 this 和 arguments。
综上,每个执行上下文,都有变量对象,作用域链,this。
1.7 作用域链
这篇说明了作用域链知识点:JavaScript之从原型到原型链
作用域链:当查找某个变量时,会先在当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量的对象中查找,一直找到全局上下文的变量对象,也就是全局对象。(即由多个执行上下文的变量构成)
函数内部有一个内部属性[[scope]],当函数创建时,会保存所有父变量到这个属性中,[[scope]]为所有父变量对象的层级链,不代表全部完整的作用域链。
1.8 闭包
第一:如何使用闭包;第二:什么是闭包;第三:闭包是什么时候被创建的;第四:什么时候被销毁的。
面试题
使用闭包让其输出5 -> 0,1,2,3,4
优化:
使用 es6 编写:
window.setTimeout
setTimeout()
方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。
var id = setTimeout(fn, delay)
启动单个计时器,该计时器将在延迟后调用指定的功能,返回一个唯一的 id,以后可以使用该 id 取消计时器。
而var id = setInterval(fn, delay)
类似于setTimeout
但连续调用该函数,直到被取消。clearInterval(id)
,clearTimeout(id)
,接收计算器 id,并停止计算器回调。
不能保证计算器的延迟,由于浏览器中所有 JavaScript 都在单线程上执行,so,异步事件仅在执行中存在空缺时才运行。
由于 JavaScript 一次只能执行一段代码,因此这些代码块中的每一个都“阻塞”了其他异步事件的过程,当发生异步事件时,它将排队等待稍后执行。
setTimeout
和setInterval
:
Promise
Promise
对象用于表示一个异步操作的最终完成或失败,以及其结果值。
示例:
Promise
对象是一个代理对象,被代理的值在Promise
对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise
对象。
一个Promise
有几种状态😬:
pending
初始状态,即不是成功,也不是失败状态;fulfilled
表示操作成功完成;rejected
表示操作失败。
Promise.prototype.then 和 Promise.prototype.catch 方法返回 promise 对象,所以它们可以被链式调用。
方法:
Promise.prototype.catch(onRejected)
添加一个拒绝回调到当前 promise,返回一个新的 promise。
Promise.prototype.then(onFulfilled, onRejected)
添加解决和拒绝回调到当前 promise,返回一个新的 promise,将以回调的返回值来 resolve。
Promise.prototype.finally(onFinally)
添加一个事件处理回调于当前 promise 对象,并且在原 promise 对象解析完毕后,返回一个新的 promise 对象。
示例:
async function
async function
用来定义一个返回AsyncFunction
对象的异步 hash。
示例:
一个 async 异步函数可以包含 await 指令,该指令会暂停异步函数的执行,并等待 Promise 执行,然后继续执行异步函数,并返回结果。
await 关键字只在异步函数内有效。如果你在异步函数外使用它,会抛出语法错误。
1.9 强大的闭包
示例:😦
全局环境中运行的代码:😫
没有被嵌套的函数😊
当 myFunc 被执行的时候,对象之间的关系如下图所示
闭包是同时含有对函数对象以及作用域对象引用的最想,实际上,所有 JavaScript 对象都是闭包。so,当你定义一个函数的时候,你就定义了一个闭包。当闭包不被任何其他的对象引用时,会被销毁。
闭包是一个可以访问外部作用域的内部函数。通过 var 创建的变量只有函数作用域,通过 let 和 const 创建的变量既有函数作用域,也有块作用域。
嵌套作用域:
内部函数可以访问外部函数:
词法作用域是指内部函数在定义的时候就决定了其外部作用域,闭包的外部作用域是在其定义的时候就决定了。
示例:
dada()的函数作用域是 da()函数的词法作用域
外部作用域执行完毕后,内部函数还在(在其他地方被引用),闭包才真正发挥作用。😏
闭包只存储外部变量的引用,而不会拷贝这些外部变量的值,注意,这玩意用多了内存泄漏了就不好了😂😂
闭包可以引用函数外部变量,并且会沿着原型链向上查找,闭包引用的变量在闭包存在时不会被回收,函数的词法作用域在函数声明的时候已经决定了,所以闭包可引用的外部变量只能是父级。
在垃圾回收中😱,局部变量会随着函数的执行完毕而被销毁😱,除非还有指向他们的引用。当闭包本身被垃圾回收后,闭包中的私有状态随后也会被垃圾回收。
函数是一等公民
您是不是常常听到-“函数是一等公民”这样的描述,在编程中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。😘
例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,可以作为函数返回值,也可以赋值给变量。
so,函数在 JavaScript 中是一等公民。一等公民具有最高的优先权,当函数被看作是“一等公民”, 就是函数优先。
函数可以存储到变量中
函数可以存储为数组的一个元素
函数可以作为对象的成员变量
函数与数字一样可以在使用时直接创建出来
函数可以被传递给另一个函数
函数可以被另一个函数返回
参考文献
How do JavaScript closures work under the hood
Understanding Execution Context and Execution Stack in Javascript
JavaScript 高级程序设计(第 3 版)
JavaScript 权威指南
❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章
点赞、收藏和评论
我是Jeskson
(达达前端),感谢各位人才的:点赞、收藏和评论,我们下期见!(如本文内容有地方讲解有误,欢迎指出☞谢谢,一起学习了)
我们下期见!
文章持续更新,可以微信搜一搜「 程序员哆啦 A 梦 」第一时间阅读,回复【资料】有我准备的一线大厂资料,本文 http://www.dadaqianduan.cn/#/ 已经收录
github
收录,欢迎Star
:https://github.com/webVueBlog/WebFamily
版权声明: 本文为 InfoQ 作者【魔王哪吒】的原创文章。
原文链接:【http://xie.infoq.cn/article/5f4c4ada2b6eac108afb53f01】。未经作者许可,禁止转载。
评论