JavaScript 运行机制 —— 串联一堆散装概念

2021年09月15日 阅读数:5
这篇文章主要向大家介绍JavaScript 运行机制 —— 串联一堆散装概念,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

javascript运行机制

写在前面

看下面两段代码,控制台上会输出什么?javascript

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log('this.a', this.a);
      console.log('a', a);
}

foo();
function foo() {
    var a = 2;
    bar();
}

function bar() {
    console.log('this.a', this.a);
      console.log('a', a);
}

foo();

答案是:html

两段代码结果相同java

this.a undefined
ReferenceError: a is not defined

​ 若是你答对了,恭喜你已经对做用域、this 有必定认识,若是你想更深刻了解代码执行过程当中发生了什么,请往下看。segmentfault

阅读本文,你将搞清楚如下概念:浏览器

  1. this 究竟是什么
  2. 执行上下文 Execution Context
  3. 调用栈 Context Stack
  4. 做用域链 Scope Chain
  5. 变量对象 Virable Object(VO) 和 活动对象 Active Object(AO)
  6. 词法环境 Lexical Environment 和 变量环境 Variable Environment

阅读本文前,你须要如下知识储备闭包

  1. var函数声明 具备提高的特性,而 const let 函数表达式 没有
  2. js 中做用域的只有 全局做用域函数做用域,ES6 中新增了块级做用域
  3. 每一个函数在执行时会建立一个活动对象,记录函数调用位置、变量等等

1. 什么是执行上下文

​ 当一个函数被调用时,会建立一个活动记录。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。这个活动记录又称为执行上下文、执行环境app

小结:执行上下文和执行环境是一个概念,记录了不少信息

1.1 执行上下文 / 执行环境的分类

  • 全局执行上下文 / 全局执行环境
  • 函数执行上下文 / 函数执行环境

1.2 执行栈

​ 在分析执行上下文内容以前,还须要了解执行栈函数

​ 在咱们的代码执行以前,引擎初始化一个执行栈,初始化完毕后,将全局执行环境压入执行栈,当每当一个函数开始执行,就建立一个函数执行环境压入栈,函数执行完毕后,将该函数执行环境弹出,关闭浏览器时,弹出全局执行环境post

function foo(i) {
  if (i < 0) return;
  console.log('begin:' + i);
  foo(i - 1);
  console.log('end:' + i);
}
foo(2);

/*
begin:2
begin:1
begin:0
end:0
end:1
end:2
*/

image.png

图片及代码来自 https://juejin.im/post/5c2052...

想有更多理解,请访问 https://www.cnblogs.com/wangf...this

2. 执行环境的建立与激活

执行环境有两个阶段

  1. 建立阶段
  2. 激活 / 执行阶段

ES3 执行环境生成规范

  • 建立阶段

    • 建立做用域链 Scope Chain
    • 建立变量对象 Variable Object
    • 为 this 赋值
  • 激活 / 执行阶段

    • 完成变量分配,变 VO 为 AO,执行代码

ES6 执行环境生成规范

  • 建立阶段

    • 为 this 赋值
    • 建立词法环境 Lexical Environment
    • 建立变量环境 Variable Environment
  • 执行阶段

    • 完成变量分配,执行代码

3. ES3 中的 Execution Context

3.1 做用域链 Scope Chain

个人理解是,生成Scope Chain时,会扫描函数做用域,注意要与执行上下文区分,函数中访问父级做用域的操做,就是顺着Scope Chain实现的。举个例子

var a = 1;
function foo() {
    var a = 2;
    bar();
}

function bar() {
      console.log('a', a);
}

foo();

打印结果如何?

1

为何是这个结果?

在执行 bar() 时,生成的执行上下文中没有变量 a 存在,因而,顺着 scope chain 向上寻找父级做用域,因而,找到了 bar 父级做用域 window 的变量 a,而不是 foo 做用域中的变量 a

如今你大概明白做用域是如何在函数执行时起做用的了吧

也许你对做用域的理解产生动摇,请访问 深刻理解javascript原型和闭包(12)——简介【做用域】

Scope Chain 链接的是做用域,而不是执行上下文!
PS:这里的做用域在《你不知道的js》中被叙述为词法做用域,对以上打印结果有质疑的同窗,能够检索下 动态做用域。也能够翻阅《你不知道的js 上卷》第58 59页

3.2 变量对象 VO,活动对象 AO

  • 变量对象:存储 js 执行上下文中的函数标志符、形参 argument、变量声明,这个对象在 js 环境下是不可访问的。
  • 活动对象:存储变量对象 VO 中的声明、形参 argument、函数标识符,与 VO 不一样的是,AO 是“激活版”的 VO,其存储的内容是能够被访问到的
对于两者区别,下面将中伪代码展现

建立 VO 分三步

  1. 建立 argument 对象
  2. 扫描上下文中的函数声明,以函数名为 key,在堆中对应的地址为 value,记录在 VO 中(涉及概念提高、堆栈模型)。若是有两个同名函数,之后出现的为准,VO 中后出现的函数覆盖先出现的函数
  3. 扫描上下文中 var 变量声明,在以变量名为 key,并初始化 value 为 undefined。若同 var 变量出现第二个声明,则忽略并继续扫描
// 以上能够理解
function foo(){ console.log('foo声明第一次') }
function foo(){ console.log('foo声明第二次') }
foo()
// 'foo声明第二次'
ES3 时期变量声明只有 var,尚未 const let

3.3 this

其实 this 的绑定规则很简单,this指向调用该函数的对象,如

var a = 1;

var obj = {
    a: 2,
    foo: foo
}

function foo() {
    console.log(this.a);
}

obj.foo(); // 2
foo();

按规则,执行 obj.foo() 时,this 指向obj。执行 foo() 等价于 window.foo() ,因此 this 指向 window

​ 固然,bind apply call 能够改变函数内部的 this 指向,本文不作讨论。我想代表的是,this 很简单,只是执行函数时,生成执行上下文中的一个步骤。this 赋值颇有规律,this指向调用该函数的对象,并非什么牛鬼蛇神,只是一个小步骤

3.4 实战演习一波

function foo(param1, param2) {
  var name = 'cregskin';
  var age = 20;
  
  function bar() {
    var name = 'jellyFish'
    var age = 20;
  }
  
  bar();
}

foo();
// 1. 初始化,建立全局执行上下文
GlobalExecutionContext = {
  ScopeChain: null,
  VariableObject: { // 建立(js 不可访问)
    foo: pointer to foo
  },
  this: window
}
GlobalExecutionContext = {
  ScopeChain: null,
  ActiveObject: { // 激活(js 能够访问)
    foo: pointer to foo
  },
  this: window
}

// 2. 执行foo() ,建立 foo() 执行上下文
fooExecutionContext = {
  ScopeChain: Global{},
  VariableObject: { // 建立(js 不可访问)
    name: undefined,
    age: undefined,
    bar: pointer to bar
  }
}
fooExecutionContext = {
  ScopeChain: Global{},
  ActiveObject: { // 激活(js 能够访问)
    name: undefined,
    age: undefined,
    bar: pointer to bar
  }
}

// 3. 执行 bar(),建立 bar() 执行上下文
barExecutionContext = {
  ScopeChain: foo{},
  VariableObject: { // 建立(js 不可访问)
    name: undefined,
    age: undefined
  },
  this: window
}
barExecutionContext = {
  ScopeChain: foo{},
  ActiveObject: { // 激活(js 能够访问)
    name: undefined,
    age: undefined
  },
  this: window
}

怎么样!是否是很通透了,我再插一句话

ES3 中只有全局做用域和函数做用域,执行上下文是函数执行时才生成的。请再次回忆一下,执行上下文的 VO AO 包含的内容,不就是 函数做用域自身中的变量声明和函数声明吗!Scope Chain 链接的做用域,不就是 函数定义时的做用域嵌套关系吗!

执行上下文和做用域居然有如此高度的一致性!妙趣横生

3.5 小结一下

列一下 ES3 中执行上下文的结构

  • Scope Chain
  • Variable Object / Active Object

    • var 变量声明
    • 函数声明
  • this

结构简单。下面我将介绍 ES5 中的执行上下文,大同小异

4. ES6 中的 Execution Context

4.1 词法环境 Lexical Environment

4.1.1 概念

官方 给出的概念是:词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。

词法环境在 js 中也有不少别称:词法做用域、做用域

是否是能串起来了!相比 ES3, ES6 中的词法环境做用域在命名上有更深的羁绊!

4.1.2 组成

  • Laxical Environment 词法环境

    • Environment Record 环境记录器

      • 变量声明
      • 函数声明
    • Outer 外部环境引用

类比一下以前讲到的 ES3 版本的执行上下文,你的脑中可能有大体的对应关系了

ES3 ES6
Scope Chain Outer
Viriable Object / Active Object Environment Record

哈哈好厉害,我第一看的时候都缕了很久

下面我将介绍Environment Record ,本质上与 VO AO 仍是有一些区别的

注意 ES6 引入了块级做用域,const let 没有 提高,在这里是要有体现的

4.1.3 Environment Record 环境记录器

对应两种执行环境(全局执行环境、函数执行环境),环境记录器也有两种,

  • 声明式环境记录器(在函数执行环境中使用)

    • 储存 const let 变量
    • 储存 argument 参数
    • 储存函数声明
  • 对象环境记录器(在全局执行环境中使用)

    • 变量声明
    • 函数声明
var 声明呢?有很多 var const let 混用的场景啊。

别急,下文中会介绍

4.1.4 Outer 外部环境引用

这个就很少介绍了,与 ES3 中 Scope Chain 同样。

做用是访问父级做用域

4.2 Variable Environment 变量环境

还记得咱们以前埋的坑吗——var 变量存储在哪里?

4.2.1 组成

  • Variable Environment 变量环境

    • Environment Record 环境记录器(在函数执行环境中使用)

      • 存储 var 变量声明
    • Object environment records 对象环境记录器(在全局执行环境中使用)

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

4.3 实战演练

function foo() {
    const a = 2;
    this.bar();
}

function bar() {
    console.log('this.a', this.a);
      console.log('a', a);
}

foo();

建立做用域链以前须要了解一下做用域

深刻理解javascript原型和闭包(12)——简介【做用域】

步骤以下

看到这里,文章开头的那两道题你应该能够解了。 我先给出执行环境的内容

// 1. 初始化,建立全局执行上下文
GlobalExecutionContext = {
    Outer: null,
    LexicalEnvironment: { // 建立(js 不可访问)
      foo: pointer to foo
      arguments: []
    },
    this: window
  }
  GlobalExecutionContext = {
    Outer: null,
    LexicalEnvironment: { // 激活(js 能够访问)
      foo: pointer to foo,
      arguments: []
    },
    this: window
  }
  
  // 2. 执行foo() ,建立 foo() 执行上下文
  fooExecutionContext = {
    Outer: Global{},
    LexicalEnvironment: { // 建立(js 不可访问)
      a: <undefined>, // 注意 const 没有变量提高
      bar: pointer to bar,
      arguments: []
    }
  }
  fooExecutionContext = {
    Outer: Global{},
    LexicalEnvironment: { // 激活(js 能够访问)
      a: <undefined>, // 注意 const 没有变量提高
      bar: pointer to bar,
      arguments: []
    },
    this: window
  }
  
  // 3. 执行 bar(),建立 bar() 执行上下文
  barExecutionContext = {
    Outer: Global{},
    LexicalEnvironment: { // 建立(js 不可访问)
      arguments: []
    },
    this: window
  }
  barExecutionContext = {
    Outer: Global{},
    LexicalEnvironment: { // 激活(js 能够访问)
      arguments: []
    },
    this: window
  }

为方便阅读,我把代码拉下来

function foo() {
    const a = 2;
    this.bar();
}

function bar() {
    console.log('this.a', this.a);
      console.log('a', a);
}

foo();
  1. 建立全局执行环境
  2. 执行 foo(),建立 foo 函数执行环境
  3. 执行 bar(),建立 bar 函数执行环境

    1. 访问 this.a => window.a,打印 undefined
    2. 访问 a,bar 函数执行环境中找不到,因而顺着 Outer 向上寻找到 window 的执行环境,没有 a 的声明。抛出错误 ReferenceError: a is not defined
注意,这里3中2说 顺着 Outer 向上寻找到 window 的执行环境,指的是window的做用域和执行环境已经高度一致了。 严格来讲因该是 顺着 Outer 向上寻找到 window 的词法做用域*

通透!

别忘点赞!?很是感谢


转载请标明出处!

Reference

傻傻分不清的javascript运行机制 - 掘金翻译计划

[[译] 理解 JavaScript 中的执行上下文和执行栈 - 掘金翻译计划](https://juejin.im/post/5ba321...

深刻理解javascript原型和闭包(完结)- 王福朋