添加控制器 提示找到不到上下文_从面试题的角度深入理解执行栈和执行上下文

作用域

来看看我们的面试题 01

var b = 1;
function fn1() {
  console.log(b); // ?
}

function fn2() {
  var b = 2;
  fn1();
}

fn2();

这道题考的就是作用域的概念,我们要做出这道题,就需要深入的理解作用域

作用域名词解释

首先来看看作用域的名词解释

作用域是一个 规则,它定义了当前作用域及嵌套子作用域如何查找变量的规则

用我们实际中常用到的讲解就是当前函数可以访问当前函数内部的变量和外部变量,但在外部无法直接读取函数内部变量

也就是说作用域是一个规范是一个规则,它并不是一个当前作用域可以访问的数据集合

作用域生成位置

我们再来一部一部分析面试题 01,当浏览器拿到这段字符串的时候,肯定不能直接执行fn2,它会将这段字符串编译成可执行的代码

v8引擎举列,它首先会对代码进行词法分析

上面代码太长,我们只取第一行进行理解

var b = 1;

首先将这段字符串词法分析转换为记号流

2724cca0682e9205f5f550381de06eed.png

然后进行语法分析形成AST抽象语法树

f1a3589b317254ed87125849a20ade68.png

词法和语法分析可以查看这个网站

然后js进行预编译,这个时候便确定了作用域,大家要注意此时还在编译阶段,js并没有进行执行,列子中的fn2也没有执行

所以通过编译分析,我们知道作用域是在代码执行前就已经被确定了的

我们再来看看此时经过编译后的面试题 01,此时函数fn1fn2的作用域规则被确定

fn1的作用域规则是它只能访问fn1中定义的变量和全局变量

fn2的作用域规则是它只能访问fn2中定义的变量和全局变量

当然为了解出这道题,光了解作用域是不够的,因为作用域的确定是在编译阶段,代码压根没执行怎么知道答案呢?

这里就要牵扯到另外一个概念作用域链

作用域链

同样的我们在理解作用域链前,来看看它的名词解释

作用域链是由当前作用域的变量和上层可访问作用域的变量链式连接而成的数据集合

作用域链和作用域其实是容易混淆的,但它们有本质的区别

作用域是一个规则,它在js编译中确定

作用域链是一个集合,它在函数的执行中确定

换句话来说,作用域链通过作用域规则的定义形成一个可访问的变量集合[[ scope chain ]]

我们来看看fn2执行的时候,作用域链长啥样

  • fn2
  • 内部变量 b = 2
  • 全局变量 b = 1
  • 全局变量 fn1
  • 全局变量 fn2
  • 其它全局变量

同样的fn2内部执行了fn1,当执行到fn1的时候,fn1的作用域链也被确定

  • fn1
  • 没有内部变量
  • 全局变量 b = 1
  • 全局变量 fn1
  • 全局变量 fn2
  • 其它全局变量

可以看到fn1执行时能访问的b的值仅有全局变量b,虽然fn1是在fn2内部执行,但能访问的只有全局变量b,所以这道题的答案是1

动态作用域

词法作用域还有一个别名叫静态作用域,因为作用域的确定并不是在执行阶段动态创建的,所以名字就相对取了个静态

有同学可能感觉到疑问,有静态作用域难道还有动态作用域吗

其实是有的,如果你写过自动发布的sh脚本,shell它就是动态的

我们将面试题 01 用shell写出来如下

#!/bin/bash
b=1

fn1() {
  echo $b
}

fn2() {
  b=2
  fn1
}

fn2

这道题的输出就是 2 而不是 1

执行上下文

执行上下文的阶段

我们来看面试题 02

这两者的执行栈区别是啥?

// 1.js
function fn1() {}
function fn2() {}
fn1();
fn2();

// 2.js
function fn1() {
  function fn2() {}
  fn2();
}
fn1();

这两个方法都是先执行了fn1再执行了fn2,看似相同,但它们的执行栈的推入和推出是不同的

js有一个调用栈的概念,每个栈存储的叫执行上下文

我们来看看执行上下文的名词解释

每次当控制器转到可执行代码的时候,就会进入一个执行上下文,执行上下文可以理解为当前代码的执行环境

js中有三个执行上下文

  • 全局执行上下文
  • 函数执行上下文
  • eval 执行上下文

每进入以上三个环境就会创建一个新的执行上下文,然后将执行上下文推入函数调用栈的顶端,当此执行上下文进行完毕,推出调用栈

我们来看看面试题 02 的前面部分

function fn1() {
  debugger;
}
function fn2() {
  debugger;
}
fn1();
fn2();

我们加上了断点执行这段代码,首先执行到fn1时,触发debugger,此时调用栈如下,可以看到调用栈顶端是fn1函数,底部有一个匿名函数anomymous其实就是全局执行上下文

60001e5adf8c2e1af6f090db8ded54f8.png

执行完fn1,fn1被推出调用栈,此时执行fn2,调用栈如下

69eae157c0442349ae16f591343902e7.png

我们再来看看面试题 02 的后半部分

function fn1() {
  debugger;
  function fn2() {
    debugger;
  }
  fn2();
}
fn1();

先执行fn1,触发debugger,此时调用栈推入fn1

84868e2e3de12744f509aa9674707029.png

然后执行fn2,触发debugger,此时调用栈推入fn2

bb8b88505bdd7d4671a572f4c4dd91e5.png

然后fn2执行完毕,推出fn2,fn1执行完毕,推出fn1

这就是两者在调用栈上的区别,虽然看似都是执行的fn1fn2,调用栈的推入推出确实不一样的

变量提升

我们再来看看面试题 03

// 1
console.log(a); // ?
var a;
function a() {}

// 2
function b() {}
var b;
console.log(b); // ?

// 2
var c = 1;
function c() {}
console.log(c); // ?

这套题的考点在变量提升

根据上面的讲解我们知道,每进入一个函数会生成一个执行上下文

而执行上下文的生成又分为两个阶段,一个是创建阶段,一个是执行阶段

创建阶段有三个步骤

  1. 生成变量对象(变量提升)
  2. 生成作用域链
  3. 确定 this 指向

执行阶段也有三个步骤

  1. 变量赋值
  2. 函数引用
  3. 执行其他代码

解决这个问题我们需要首先了解执行上下文创建阶段的第 1 个步骤变量提升

变量提升首先会找到当前执行上下中需要提升的变量并在当前执行上文中提升

这里需要提升的变量指的就是var定义的变量function定义的函数

但这两个也有先后顺序,会首先找到函数并将整个函数存入内存中

注意此时并没有对函数进行引用,换句话来说只是开辟了内存地址,函数并没有被赋值

然后寻找var定义的变量,将它赋值为undefined

注意此时并没有对var定义的相关变量进行具体赋值,而是赋值为默认的undefined

这里会存在一个问题,我们知道function的提升在var之前,如果function已定义了一个变量,var又去提升这个变量为默认的undefined不是重置了之前已经定义好的function了吗

其实这里var提升有一个规则,当发现这个变量已存在function定义时,会跳过默认的定义为undefined这个步骤

我们来看面试题 03 的第 1 和第 2 部分

// 1
console.log(a); // ?
var a;
function a() {}

// 2
function b() {}
var b;
console.log(b); // ?

这两个的意思其实是一样的,它们首先都会进行变量提升,function定义被首先提升,然后遇到var,此时已经存在了function所以var定义默认undefined这个步骤跳过

这里的输出便是function afunction b

我们再来看面试题 03 第 3 部分

// 3
var c = 1;
function c() {}
console.log(c);

同样的function被提升,然后提升var此时已经定义好了function,所以默认undefined步骤跳过

同样和之前要加深的一样注意,这里并没有进行赋值,只是进行了提升

当执行上下文执行的时候才会赋值,此时c被赋值为了1,所以这里的答案是1

大家可能会有疑问,为啥上面的var a这种写法不是赋值为undefined,其实这根本不是一个赋值语句,这只是一个简单的定义,像var c = 1这才会触发赋值

我们再来看看面试题 04

// 4
var a = 'a';
function fna() {
  console.log(a); // ?
  var a = 'aa';
  console.log(a); // ?
}
fna();

这套题的考点在于我们之前强调的一句话

变量提升首先会找到当前执行上下中需要提升的变量并在当前执行上文中提升

变量的提升仅提升到当前执行上下文

首先我们执行fna,然后创建fna的执行上下文,此时第一步进行变量提升,a变量提升并赋值为默认的undefined

所以第一个console打印的是undefined

然后a被进行赋值,此时打印的便是aa

this 指向

我们来看面试题 05

function fn1() {
  console.log(this);
}
// 1
fn1(); // ?
// 2
new fn1(); // ?

// 3
fn1.apply({ a: 1 }); // ?
fn1.bind({ a: 1 }).call({ a: 2 }); // ?
fn1.bind({ a: 1 }).bind({ a: 2 }).call({ a: 3 }); // ?

const obj1 = {
  fn() {
    console.log(this);
  },
};
// 4
obj1.fn(); // ?
const fn2 = obj1.fn;
fn2(); // ?

我们上面讲过,this的确定是在执行上下文创建的时候

换句话来说,this的指向取决于函数的调用更取决于函数是怎么调用的

函数的调用分为 4 种情况,所以this的指向也对应 4 种情况

  1. 当我们直接调用函数时,此时this指向全局window
  2. 当我们对构造函数用new操作符执行的时候,此时this指向当前构造函数实例
  3. 当我们调用apply bind call方法的时候,this指向方法的第一个参数,当然他们也是有优先级的,bind的优先级高于callapply,且bind第一次后再bindthis无效
  4. 当函数在一个对象上调用的时候,this指向当前对象

我们再来看看面试题

第 1 个位置直接调用的fn1,此时对应第一种情况,输出的this就是全局window

第 2 个位置new了一个构造函数,此时对应第二种情况,输出的this指向当前构造函数实例

第 3 个位置我们调用了applybind方法,此时对应第三种情况,因为bind的优先级最高,所以即使bind了很多次也只取第一次bind的对象,所以第 3 个位置所有的this输出都是{ a: 1 }

第 4 个位置我们在对象obj1上调用了fn,此时对应第四种情况,输出的this为对象obj1

但第 4 个位置还通过obj1直接读取了fn赋值给了一个变量fn2,此时直接执行fn2对应的情况是第一种,所以输出全局window

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值