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

紧跟潮流

大前端和全栈是以后前端的一个趋势,懂后端的前端,懂各端的前端更加具有竞争力,以后可以往这个方向靠拢。

这边整理了一个对标“阿里 50W”年薪企业高级前端工程师成长路线,由于图片太大仅展示一小部分

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

词法环境有两种类型:

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

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

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

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

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

简而言之,

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

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

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

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

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

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

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

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 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 时,函数执行上下文才会被创建。

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

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

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

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

执行阶段

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

注意 — 在执行阶段,如果 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. 首先进入全局执行环境,创建全局执行上下文环境并加入栈中

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

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

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

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

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

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

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

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

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

  11. fn1()函数出栈

  12. 全局执行上下文出栈

题解

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

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 中的const与let来说

因为呢在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是在哪里来的,因为在一般的情况下会在当前作用域中取值,那在当前作用域没找到的话,会去上级作用域中寻找,一直到找到全局作用域。这么一个寻找的过程中呢就会形成一个链条,就叫做作用域链

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

最后

其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《前端开发四大模块核心知识笔记》

最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值