【面试题】三道面试题让你掌握JavaScript中的执行上下文与作用域以及闭包

news2025/4/19 18:29:14

前言

大厂面试题分享 面试题库

前后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库


大家好,笔者呢最近再回顾JavaScript知识时,又看到了JavaScript的一些较为常见的内容,仔细看了之后发现之前理解的并不深,所以给记录了下来,加深印象。执行上下文与执行栈作用域与作用域链闭包

执行上下文


例题

大家先来看一道较为简单的题,看下是否能看出来结果

var a = 10;
functionfn(b) {
  b = 20;
  console.log(a, b);
}
functionfn1() {
  a = 100;
  fn(a);
}
fn(200); //输出结果fn1(); // 输出结果复制代码

大家可以看出来输出结果是什么吗?

如果你已经算出来的话,那么说明你对执行上下文还是有一些理解的,欢迎继续往下看加深印象

如果你没算出来或者输出结果与你算的不相符,那也先不要着急,先看下边内容,看完后再回来算

概念

大家都知道,JavaScript代码的在运行的时候都是自上而下按顺序执行的,但是呢实际并非是一行一行的执行,那大家有没有了解过它在执行代码的时候做过哪些准备,做过哪些事情,比如代码解析、分配内容都是在哪处理的,那这个地方呢就是执行上下文,是准备工作的所在环境

执行上下文类型

执行上下文呢有三种类型,分别是

  • 全局执行上下文

  • 函数执行上下文

  • 还有就是eval函数执行上下文

那么我们继续,执行上下文呢是在代码编译阶段创建的,来看看执行上下文的生命周期

执行上下文生命周期

  • 创建阶段

  • 执行阶段

创建阶段

执行上下文的创建阶段具体做了什么事呢,又分为三部分

ExecutionContext = {
  ThisBinding = <thisvalue>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}
复制代码
确定this指向

在全局执行上下文中,this指向的是全局对象

在函数执行上下文中,this指向取决于该函数是如何被调用的

看下这个demo

const obj = {
  fn: function(){
    console.log(this)
  }
}
​
obj.fn(); //fn: f();
​
const func = obj.fn;
​
func(); // Window复制代码
词法环境

官方的 ES6 文档把词法环境定义为

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用

  1. 环境记录器是存储变量和函数声明的实际位置。

  1. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

词法环境有两种类型:

  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。

  • 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型(如上!):

  1. 声明式环境记录器存储变量、函数和参数。

  1. 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。

简而言之,

  • 全局环境中,环境记录器是对象环境记录器。

  • 函数环境中,环境记录器是声明式环境记录器。

注意 — 对于函数环境声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length

抽象地讲,词法环境在伪代码中看起来像这样:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
    }
    outer: <null>
  }
}
​
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
    }
    outer: <Globalorouterfunctionenvironmentreference>
  }
}
复制代码
变量环境

它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 var 变量绑定。

我们看点样例代码来理解上面的概念:

let a = 20;
const b = 30;
var c;
​
functionmultiply(e, f) {
 var g = 20;
 return e * f * g;
}
​
c = multiply(20, 30);
复制代码

执行上下文看起来像这样:

GlobalExectionContext = {
​
  ThisBinding: <Global Object>,
​
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },
​
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      c: undefined,
    }
    outer: <null>
  }
}
​
FunctionExectionContext = {
  ThisBinding: <Global Object>,
​
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },
​
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}
复制代码

注意 — 只有遇到调用函数 multiply 时,函数执行上下文才会被创建。

可能你已经注意到 letconst 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined

这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefinedvar 情况下),或者未初始化(letconst 情况下)。

这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 letconst 的变量会得到一个引用错误。

这就是我们说的变量声明提升。

执行阶段

这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。

注意 — 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined

执行栈

那根据上述执行上下文的理解,那我们知道在执行代码中会有很多的执行上下文,那么执行上下文是怎么确定执行顺序的。

执行上下文存放的位置就是在执行上下文栈,也叫调用栈。具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。

那我们来看下之前的例题,来分析下

var a = 10;
functionfn(b) {
  b = 20;
  console.log(a, b);
}
functionfn1() {
  a = 100;
  fn(a);
}
fn(200); //输出结果fn1(); // 输出结果复制代码
  1. 首先进入全局执行环境,创建全局执行上下文环境并加入栈中

  1. fn()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈

  1. 执行 console.log(a, b);代码

  1. console.log(a, b);代码出栈

  1. fn()函数执行完毕后出栈

  1. fn1()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈

  1. 继续fn()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈

  1. 执行 console.log(a, b);代码

  1. console.log(a, b);代码出栈

  1. fn()函数执行完毕后出栈

  1. fn1()函数出栈

  1. 全局执行上下文出栈

题解

那我们再来分析下例题的答案

var a = 10;
functionfn(b) {
  b = 20;
  console.log(a, b);
}
fn(200);
复制代码

在执行fn函数时,此fn活动对象为

AO : {
  a: 10,
  b: 20,
  arguments: {0 : 20, length:0} 
}
复制代码

所以此时输出结果为10,20

继续看

var a = 10;
functionfn(b) {
  b = 20;
  console.log(a, b);
}
functionfn1() {
  a = 100;
  fn(a);
}
fn1();
复制代码

在执行fn1函数时,此fn1活动对象为

AO : {
  a: 100,
  fn: reference to functionfn(){}
  arguments: {length: 0} 
}
复制代码

在继续执行fn函数时,此fn活动对象为

AO : {
  a: 100,
  b: 20,
  arguments: {0 : 20, length:0} 
}
复制代码

所以此时输出结果为100,20

作用域


例题

大家先来看下下边的题,看下是否能看出来结果

var a = 1;
​
functionfn() {
    var b = 10;
    c = 100;
    let d = 20;
    console.log(1000)
    returnfunction() {
        console.log(a); 
        console.log(b);
        console.log(d)
    }
}
var func = fn();
func();
console.log(b);
console.log(d);
复制代码

大家可以看出来输出结果是什么吗?

概念

作用域是可访问的变量,对象,函数的结合,同时也决定了这些变量的可访问性。JavaScript中有三种作用域,分别是全局作用域函数作用域块级作用域。那就来聊聊这三种作用域

全局作用域

什么是全局作用域呢,先来看下概念

  • 最外层的函数和在最外层函数外面定义的变量是拥有全局作用域的

  • 所有的未声明的变量自动声明为拥有全局作用域的变量

我们开来接着看下上边例题

var a = 1;
​
functionfn() {
    var b = 10;
    c = 100;
    console.log(1000)
    returnfunction() {
        console.log(a); 
        console.log(b);
    }
}
fn();
console.log(c)
复制代码

那我们根据上述概念来分析下

变量a呢是拥有全局作用域的全局变量,是可以在程序任何位置都可以访问到的

函数fn也是拥有全局作用域的函数

接着来看下输出结果

fn(); // 1000console.log(c); //100复制代码

这个就很简单了吧。相信99%的前端 应该都会吧

函数作用域

来看下函数作用域的概念

  • 函数作用域呢也被称为局部作用域,因为一般只有在固定的代码片段中可以访问到,也就是说只能在函数内部访问,函数外部是访问不到的。

那我们接着来看下上述例题,相信小部分人是卡在console.log(b)这个输出结果上了,没关系继续往下看

var a = 1;
​
functionfn() {
    var b = 10;
    c = 100;
    console.log(1000)
    returnfunction() {
        console.log(a); 
        console.log(b);
    }
}
var func = fn();
func();
console.log(b)
复制代码

来继续根据函数作用域的概念来分析下

变量b呢是在函数内部,所以称为局部变量,只能在函数内部访问,外部是无法访问的

那根据这个解释看的话,这个输出结果就很明显了吧

var func = fn(); //1000func(); //1, 10console.log(b); //b is not defined复制代码

那么现在知道为什么console.log(b);输出结果是b is not defined了吗

块级作用域

我们来看看块级作用域的概念

  • 简单来说变量是存在于一个大括号之内,在大括号之外是不能访问这些块级作用域中的变量,当然是有局限性的只针对于ES6 中的constlet来说

因为呢在ES6之前呢,JavaScript是没有块级作用域的

那继续看例题,例题中有let声明的变量

var a = 1;
​
functionfn() {
    let d = 20;
    returnfunction() {
        console.log(d)
    }
}
var func = fn();
func();
console.log(d);
复制代码

看上述块级作用域的解释

那我们知道let d = 20;是在大括号中的变量,那根据概念括号外是无法访问的,那也应该知道答案是什么了吧

console.log(d); //d is not defined复制代码

那最后在回头看初始例题,是不是一切都变的很简单

最后输出结果

var func = fn(); // 1000func(); // 1,10,20console.log(b); // b is not defined// 不过console.log(d); 是输出不了结果的,因为上一步已经报错了console.log(d); // d is not defined复制代码

作用域链

那我们继续看上述例题

var a = 1;
​
functionfn() {
    var b = 10;
    c = 100;
    let d = 20;
    console.log(1000)
    returnfunction() {
        console.log(a); 
        console.log(b);
        console.log(d)
    }
}
var func = fn();
func();
console.log(b);
console.log(d);
复制代码

其中console.log(a)输出a,那这个a是在哪里来的,因为在一般的情况下会在当前作用域中取值,那在当前作用域没找到的话,会去上级作用域中寻找,一直到找到全局作用域。这么一个寻找的过程中呢就会形成一个链条,就叫做作用域链

总结

  • 作用域是可访问的变量,对象,函数的结合,同时也决定了这些变量的可访问

  • 全局作用域是说在最外层的函数以及不在任何函数或者打括号中声明的变量,都在全局作用域下,程序中任务位置都可以访问的变量

  • 函数作用域呢是变量声明在函数中只能在函数内部访问,函数外部是访问不到的

  • 块级作用域呢是说对于ES6 中的constlet来说,在大括号之内的变量是存在于块级作用域中的,大括号之外不能访问这些块级作用域中的变量

  • 作用域链在寻找变量值的时候,层层往上形成的链条

闭包


例题

大家先来看下这道题,看下是否能看出来输出结果

var data = [];
​
for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}
​
data[0](); // 输出结果
data[1](); // 输出结果
data[2](); // 输出结果复制代码

优雅永不过时,答对了说明你对闭包还是有一定研究的,没答对的继续往下看

概念

那我们先来看看闭包的概念,什么是闭包,看看MDN与高级程序设计给出的概念

  • 能够访问其它函数内部变量的函数,称为闭包

  • 能够访问自由变量的函数,称为闭包

分析

那我们来分析下上边的例题

在分析之前你需要对作用域以及执行上下文有一定的了解,如果不太明确的话,可以优先看下这两篇文章

再继续阅读下面内容

var data = [];
​
for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}
​
data[0](); // 输出结果
data[1](); // 输出结果
data[2](); // 输出结果复制代码

首先我们记录下这道例题的执行上下文栈的变化

  1. 首先进入了全局执行上下文,然后创建全局执行上下文,将全局上下文放入执行上下文栈中

  1. 然后继续初始化执行全局上下文,创建作用域链以及变量对象等

那么此时的全局上下文VO为

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}
复制代码
  1. 执行data[0](),然后创建data[0]()执行上下文,继续放入执行上下文栈内

  1. 然后呢初始化data[0]()执行上下文,创建作用域链以及变量对象等

那么此时的data[0]()AO为

data[0]Context = {
    AO: {
       arguments: {length: 0} 
    }
}
复制代码

那么此时的data[0]()作用域链为

data[0]Context = {
    Scope: [AO, globalContext.VO]
}
复制代码

因为在data[0]Context活动对象AO中是没有i值的,所以去全局上下文的变量对象中查找,此时全局上下文的变量对象中i值为3

所以data[0]()输出结果为3

  1. 执行完毕后在data[0]()执行上下文在执行上下文栈给弹出

  1. 至于data[1]()data[2]()与步骤3-5是一样的,所以在这就不多说了

场景

至于闭包的使用场景,其实在日常开发中使用到是非常频繁的

  • 防抖节流函数

  • 定时器回调

  • 等就不一一列举了

优缺点

优点

闭包帮我们解决了什么问题呢

内部变量是私有的,可以做到隔离作用域,保持数据的不被污染性

缺点

同时闭包也带来了不小的坏处

说到了它的优点内部变量是私有的,可以做到隔离作用域,那也就是说垃圾回收机制是无法清理闭包中内部变量的,那最后结果就是内存泄漏

大厂面试题分享 面试题库

前后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/396671.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1-1 微服务架构概述

文章目录微服务架构概述1-1. 系统进化理论概述集中式系统&#xff1a;分布式系统1-2. 系统进化理论背景1-3. 什么是微服务架构1-4. 微服务架构的优缺点1-5. 为什么选择 Spring Cloud 构建微服务认识 Spring Cloud2-1. Spring Cloud 是什么2-2. Spring Cloud 的版本2-3 Spring C…

2.2操作系统-进程管理:前趋图、前趋图与PV操作

2.1操作系统-进程管理&#xff1a;前趋图\前趋图与PV操作前趋图前趋图与PV操作练习前趋图与PV操作&#xff0c;一般出现了&#xff0c;分值在2~3分左右&#xff0c;技巧性很强。 前趋图 前趋图是为了描述一个程序的各部分间的依赖关系&#xff0c;或者是一个大的计算的各个子…

【c++类与对象 】

目录&#xff1a;前言一、基础引入1.类的定义2.类的权限3.类的封装4.类的实例化5.计算类对象的大小结构体内存对齐规则空类的大小二、this指针this引入this指针的特性经典例题三、类的六个默认成员函数1、构造 && 析构构造函数析构函数2、拷贝 && 赋值拷贝构造…

display:inline-flex使用

凡是使用了display:inline-flex布局的容器&#xff08;不管是行内元素还是块级元素&#xff09;&#xff0c;将会变为弹性容器&#xff0c;它的宽高都将可以被设置&#xff0c;并且该容器整体对外表现为一个行内块元素。 span也可以设置宽高&#xff0c;并且div和span它们都没有…

【C++】缺省参数函数重载

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;C的学习之路 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录前言一、缺省参数1.1 缺省参数的概念1…

关于new和delete的一些思考,为什么不能在析构函数中调用delete释放对象的内存空间,new和delete的原理

最近在写代码的时候&#xff0c;觉得每次new出来的对象都需要去delete好麻烦&#xff0c;于是直接把delete写到了析构函数中&#xff0c;在析构函数里面写了句delete this&#xff0c;结果调用析构函数的时候死循环了&#xff0c;不是很理解原因&#xff0c;于是去研究了一下。…

盘点全球10大女性技术先驱

盘点全球10大女性技术先驱 人们普遍认为技术是男性主导的领域&#xff0c;但事实&#xff0c;技术或编程与性别无关&#xff0c;几乎任何人都可以成为技术大神。已经有很多案例证明女性同样可以在技术领域施展才能。在女神节来临之际&#xff0c;我为大家盘点一下为编程做出卓越…

AB测试——流程介绍(定义问题和指标选取)

前言&#xff1a; 作为AB测试的学习记录&#xff0c;本文主要介绍了AB测试的基本流程&#xff0c;以及指标类型和如何选取合适指标。 相关文章&#xff1a;AB测试——原理介绍 AB测试的基本流程是什么&#xff1f; AB测试&#xff08;也称为分流测试&#xff09;是一种常用的实…

visual studio的team使用问题小结

visual studio的team使用问题小结一、visual studio&#xff08;2017&#xff09;默认浏览器打开team任务和bug二、visual studio&#xff08;2017&#xff09;上传team时&#xff0c;文件超过一万个会上传失败。三、visual studio&#xff08;2017&#xff09;拉取team代码时&…

C++面向对象编程之二:构造函数、拷贝构造函数、析构函数

构造函数和析构函数C利用构造函数和析构函数&#xff0c;完成对象的初始化和清理工作。对象的初始化和清理工作&#xff0c;是编译器强制我们要做的事情&#xff0c;如果我们不提供构造函数和析构函数&#xff0c;编译器会提供3个函数&#xff1a;默认无参构造函数默认拷贝构造…

004+limou+HTML——(4)HTML表格

000、前言 表格在实际开发中的应用还是比较多的&#xff0c;表格可以更加清晰地排列数据 001、基本结构 &#xff08;1&#xff09;构成 表格&#xff1a;<table>行&#xff1a;<tr>&#xff08;table row&#xff0c;表格行&#xff09;&#xff0c;由多少组t…

【每日随笔】中国当前社会阶层 ( 技术无关 | 随便写写 )

文章目录一、阶层划分根据收入划分的阶层根据分工逻辑划分根据权利划分二、根据社会地位和掌握的资源划分的阶层三、赚钱的方式四、如何进入高阶层看了一个有意思的视频 , 讲的是中国当前的社会阶层 , 感觉好有道理 , 搜索了一些资料 ; 参考资料 : 关于中国的社会阶层社会在分…

【一】【socket聊天室】-多线程,socket编程

本文主要实现基于socket编程的聊天室&#xff0c;主要分为下面三个步骤&#xff1a; &#xff08;1&#xff09;多用户聊天&#xff1a;一个服务器多个客户端&#xff0c;客户端信息显示在公共的服务端窗口&#xff0c;利用多线程实现&#xff1b; ——客户端双线程&#xff1a…

OpenCV基础(一)

1.认识图像&#xff08;彩色图中每一个像素点都包含三个颜色通道RGB&#xff0c;数值范围为0~255&#xff0c;0代表黑色&#xff0c;255代表白色&#xff09; import cv2 #opencv 读取的格式为BGRimg cv2.imread(cat.png) #读取图像 cv2.imshow(cat, img) #显示图像img&#x…

Matlab实现FFT变换

Matlab实现FFT变换 文章目录Matlab实现FFT变换原理实现手算验证简单fft变换和频谱求取功率谱结论在信号处理中&#xff0c;快速傅里叶变换&#xff08;FFT&#xff09;是一种非常常见的频域分析方法。本文将介绍如何使用Matlab实现FFT变换&#xff0c;并通过Matlab代码演示实际…

SAP ABAP 深度解析Smartform打印特殊符号等功能

ABAP 开发人员可以在 Smartform 输出上显示 SAP 图标或 SAP 符号。例如,需要在 SAP Smart Forms 文档上显示复选框形状的输出。SAP Smartform 文档上可以轻松显示空复选框、标记复选框以及 SAP 图标等特殊符号。 在 SAP Smartform 文档中添加一个新的文本节点。 1. 单击“更…

开发一款系统软件的流程步骤是什么

在如今的数字化时代&#xff0c;软件开发成为了一个重要的行业。无论是大型企业还是小型创业公司&#xff0c;软件开发都是不可或缺的一环。在本文中&#xff0c;我将介绍一些网上常见的软件开发步骤&#xff0c;以便开发者能够更好地理解和实践。1、需求分析需求分析是开发系统…

基于transformer的多帧自监督深度估计 Multi-Frame Self-Supervised Depth with Transformers

Multi-Frame Self-Supervised Depth with Transformers基于transformer的多帧自监督深度估计0 Abstract 多帧深度估计除了学习基于外观的特征外&#xff0c;也通过特征匹配利用图像之间的几何关系来改善单帧估计。我们采用深度离散的核极抽样来选择匹配像素&#xff0c;并通过一…

基于Jeecgboot前后端分离的ERP系统开发代码生成(六)

商品信息原先生成的不符合要求&#xff0c;重新生成&#xff0c;包括一个附表商品价格信息表 一、采用TAB主题一对多的模式 因为主键&#xff0c;在online表单配置是灰的&#xff0c;所以不能进行外键管理&#xff0c;只能通过下面数据库进行关联录入&#xff0c;否则online界面…

案例19-遇见问题的临时解决方案和最终解决方案

目录1、背景介绍2、两种解决方案的概念1、临时解决方案&#xff1a;2、最终解决方案&#xff1a;3、排查问题过程4、总结站在用户的角度思考作为软件开发者5、升华1、背景介绍 首先说明这是系统很早之前的时候的一个功能&#xff0c;当时和学习通还有很强的耦合关系。在学习通…