Lambda博客
  • 自我介绍
  • SSR
    • 浅谈SSR
  • React
    • 页面路由——React-Router
    • 管好你的状态——React-Redux
    • 一个哪行——React详解
    • 左膀右臂一个不少——React初探
  • 问题记录
    • js-xlsx实现纯前端导出excel表格
    • 前端开发需要了解的东西
    • 打造高性能网站
  • JavaScript学习笔记
    • 语法和API
    • js-垃圾回收
    • 收集的JS使用技巧
    • 执行机制
    • 原型和原型链
    • 执行上下文
    • 事件循环
    • JavaScript手写代码
    • 43道JS面试题
    • 悄悄的带走了你的东西——闭包详解
    • 你是个富二代吗?——JavaScript作用域链
    • 捉摸不透的女生——JavaScript类型转换
    • 这是你的女神!——JavaScript
  • 网络学习笔记
    • 强不强——HTTP-协商缓存VS强缓存
    • 网络缓存
    • 我要飞的更高——计算机网络
    • 快点!再快点!——CDN
    • 喜怒哀乐多状态——HTTP状态码
    • 你会爱上我的(ಥ_ಥ) ——TCP详解
    • 隔壁老王想篡位?门都没有——同源策略
  • 软技能
  • 安全专题
    • 保护好自己——网站安全,预防
  • 浏览器兼容性
    • 我是个好人——浏览器兼容性
  • 多图片加载——懒加载详解
  • 数据结构
  • Node
    • Node初探
  • Typescript
    • JS Plus 真香——初探 TypeScript
  • 设计模式
    • 不要花里胡哨,要一套一套——设计模式(1)
    • 学会“套路”——设计模式(2)
  • Vue
    • 实操实干——vue实例记录
    • 停下来,问问自己——Vue-刨根问底
    • 你有喘息的机会吗?——Vue,逐步了解
    • 你累吗?来来来,安利框架——Vue-初次见面
  • 小程序
    • 今天天气怎么样——记一次小程序开发
  • Webpack
    • 蛋糕分割整合工具——Webpack-前端工程化
  • 你一块,我一块——Web-modules 前端模块化
  • HTML5
    • 你会画小猪佩奇吗?—— canvas
    • 画个矢量图——HTML5-SVG
    • 听说你爱闹腾——HTML5-多媒体
    • 动画神器——HTML5-requestAnimationFrame
    • 留下痕迹——HTML5-客户端存储
    • 你知道指北针吗?——HTML5-DeviceMotionEvent
    • 你在哪啊?我在这啊——HTML5-Geolocation
    • 你到这,你到那——HTML5-拖拽
    • 你从这,到那——HTML5-拖拽上传文件
    • 开启前端之路——HTML 标签
  • 瀑布流(无限滚动)
  • 我是怎么来的?——浏览器渲染原理
  • Css
    • 由大变小,你行吗 —— 移动端
    • 动起来,这样比较炫—— CSS3 动画
    • 请不要拐弯抹角 —— CSS3 选择器
    • 想炫吗?—— CSS3 属性
    • 最后的最后——CSS自问自答
    • 万事开头难?——深入学习前端常见布局
    • 一入前端深似海——BFC剖析
    • 还有哪些好玩的东西——CSS提升
    • 那些好看的页面是怎么形成的——CSS 初识
  • 拿个小本本记下——Cookie
由 GitBook 提供支持
在本页
  • EC (执行上下文)
  • ECS (执行环境栈)
  • VO (变量对象) / AO (活动对象)
  • 提升 (Hoisting)
  • 总结
在GitHub上编辑
  1. JavaScript学习笔记

执行上下文

首先明确几个概念:

  • EC:函数执行环境(或执行上下文)

  • ECS:执行环境栈

  • VO:变量对象

  • AO:活动对象

  • scope chain: 作用域链

EC (执行上下文)

每当控制器转到 ECMAScript 可执行代码的时候,就会进入到一个执行上下文

那什么是可执行代码呢?

可执行代码的类型

  • 全局代码(

    Global code

    )

    • 这种类型的代码是在”程序”级处理的:例如加载外部的 js 文件或者本地<script></script>标签内的代码。全局代码不包括任何 function 体内的代码。这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。

  • 函数代码(

    Function code

    )

    • 任何一个函数体内的代码,但是需要注意的是,具体的函数体内的代码是不包括内部函数的代码。

  • Eval 代码(

    Eval code

    )

    • eval 内部的代码

ECS (执行环境栈)

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

由于浏览器中的 JS 解释器被实现为单线程,这也就意味着同一时间只能发生一件事情,其他行为将会放在叫做执行栈里面排队。

当浏览器首次载入你的脚本,它将默认进入全局执行上下文。当在全局代码中调用一个函数,程序将暂停当前全局代码而进入调用的这个函数,并创建一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。

如果在此函数中又有其他调用的函数执行,则相同的事情会再次上演。程序的进去执行新的内部函数,创建一个新的执行上下文并把它压入执行栈的顶部。浏览器总会执行位于栈顶的执行上下文,一旦当前上下文函数执行结束,它将从栈顶移出,并将上下文控制权交给其他的栈。这样,堆栈中的上下文就会依次执行并且弹出堆栈,直到回到全局的上下文。

VO (变量对象) / AO (活动对象)

按照字面解释,AO 其实就是被激活的 VO

变量对象 (Variable object) 是说 JS 的执行上下文中都有个对象用来存放执行上下文中可被访问但不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字,对象属性的值对应它们的值。但这个对象是 规范上或者说是引擎实现上不可在 JS 环境中访问到活动对象。

激活对象 (Activation object) 有了变量对象存放每个上下文中的东西,但是它什么时候能被访问到呢?就是没进入一个执行上下文时,这个执行上下文中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了。

EC 建立的细节

  • 创建阶段【当函数被调用,但未执行任何其内部的代码之前】

    • 创建作用域链 (Scope Chain)

    • 创建变量,函数和参数

    • 求”this“的值

  • 执行阶段

  • 初始化变量和值得引用,解释/执行代码

我们可以将每个执行上下文抽象为一个对象,这个对象具有三个属性

ECObj: {
    scopeChain:{/* 变量对象  +  所有父级执行上下文的变量对象 */}
    variableObject: {/* 函数  arguments/参数  内部变量和函数声明 */}
    this: {}
}

解释器执行代码的伪逻辑

  • 查找调用函数的代码。

  • 执行代码之前,先进入创建上下文阶段

    • 初始化作用域链

    • 创建变量对象:

      • 创建 arguments 对象,检查上下文,初始化参数名称和值并创建引用的复制

      • 扫描上下文中的函数声明(而非函数表达式)

      • 给发现的每一个函数,在变量对象上创建一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用。

      • 如果函数的名字已经存在,引用指针将被重写。(后声明的函数覆盖之前声明的函数)

      • 扫描上下文的变量声明:

      • 给发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为 undefined

      • 如果变量的名字已经在变量对象里存在,将不会进行任何操作并继续扫描。

      • 求出上下文内部”this”的值

  • 激活/代码执行阶段:

    • 在当前上下文运行/解释函数代码,并随着代码一行行执行指派变量的值

VO 对应的就是上述第二个阶段

function foo(i) {
  var a = "hello";
  var b = function() {};
  function c() {}
}

foo(2);

当我们调用foo(2)时,整个创建阶段是这样的

ECObj = {
    scopChain: {...},
    variableObject:{
        arguments:{
            0 : 2,
            length:1
        },
        i:2,
        c:fn c,
        a:undefined,
        b:undefined
    },
    this:{...}
}

在上下文创建阶段,VO 的初始化过程如下(函数的形参 ==> 函数声明 ==> 变量声明): - 函数的形参 (当进入函数执行上下文时) ———— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值,对于没有传递的参数,其值为 undefined - 函数声明 (FunctionDeclaration FD) ———— 变量对象的一个属性,其属性名和值都是函数对象创建出来的,如果变量对象已经包含了相同名字的属性,则替换它的值。 - 变量声明 (VariableDeclaration var) ———— 变量对象的一个属性,其属性名即为变量名,其值为 undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。

如何理解函数声明过程中如果变量对象已经包含了相同名字的属性,则替换它的值这句话?

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

foo(20); //'function a (){}'

根据上面的介绍,我们知道 VO 创建过程中,函数形参的优先级是高于函数的声明的,结果是函数体内部声明的function a(){}覆盖了函数形参a的声明,因此最后输出a是一个function

如何理解变量声明过程中如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性这句话?

//与参数名相同
function foo1(a) {
  console.log(a);
  var a = 10;
}
foo1(20); // '20'

//与函数名相同
function foo2() {
  console.log(a);
  var a = 10;
  function a() {}
}
foo2(); //'function a(){}'

函数声明比变量优先级要高,并且定义过程不会被变量覆盖,除非是赋值

function foo1(a) {
  var a = 10;
  function a() {}
  console.log(a);
}
foo1(20); //'10'

function foo2(a) {
  var a;
  function a() {}
  console.log(a);
}
foo2(20); // 'function a (){}'

function foo3(a) {
  console.log(a);
  var a = 10;
  function a() {}
  console.log(a);
}
foo3(20); //'function a(){}  10'

AO 对应第三个阶段 创建的过程仅负责处理定义属性的名字,而并不为它们指派具体的值,当然还有对形参/实参的处理。一单创建阶段完成,执行流进入函数并激活/代码执行阶段。

ECObj = {
    scopeChain:{ ... },
    variableObject:{
        arguments:{
            0:2,
            length:1
        },
        i:2,
        c:fn c,
        a:'hello',
        b:fn b
    },
    this:{ ... }
}

提升 (Hoisting)

(function() {
  console.log(typeof foo); // 函数指针
  console.log(typeof bar); // undefined

  var foo = "hello",
    bar = function() {
      return "world";
    };

  function foo() {
    return "hello";
  }
})();

1.为什么我们在 foo 声明之前能访问它? 在VO的创建阶段,函数在该阶段就已经被创建在变量对象中,所以在函数开始执行之前,foo 已经被定义了。

2.Foo 被声明了两次,为什么 foo 显示函数,而不是 undefined 或字符串呢? 在创建阶段,函数声明优先于变量被创建。而在变量创建中,发现VO已经存在形同名称的属性,则不会影响已经存在的属性。 因此,对foo()函数的引用首先被创建在活动对象,当我们执行到var foo时,发现foo属性名已经存在,所以代码什么都不做并继续执行。

3.为什么 bar 的值是 undefined? bar采用的是函数表达式的方式来定义的,所以bar实际上是一个变量,变量的值是一个函数,在变量创建阶段他们被初始化为undefined。这也是函数表达式不会被提升的原因。

总结

  1. EC分为两个阶段,创建执行上下文和执行代码阶段。

  2. 每个EC可以抽象为一个对象,这个对象具有三个属性,分别是:作用域链Scope,VO|AO(AO,VO只能有一个)以及this。

  3. 函数EC中的AO在进入函数EC时,确定了 Arguments 对象的属性;在执行函数EC时,其他变量属性具有一体化。

  4. EC创建的过程是有先后顺序的: 参数声明 => 函数声明 => 变量声明。

上一页原型和原型链下一页事件循环

最后更新于3年前

这是上述例子的输出流程图

流程图