执行上下文与 this 指向

执行上下文

执行上下文又称为执行环境,每当 JS 引擎解析可执行代码时,就会进入一个执行环境

本篇博客将在浏览器环境下讲述执行上下文。在浏览器环境中,执行上下文分为三种:

  1. 全局执行上下文 默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。

  2. 函数执行上下文 每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤。

  3. Eval 函数执行上下文 这种上下文一般不考虑

执行上下文是什么?

可以把执行上下文看成一个对象 {} 来理解,有三个重要属性,

  1. 变量对象(Variable Object, 简称 VO),存入声明的变量,也是一个对象 {},里面存有
  • 函数的所有形参 (如果是函数执行上下文)
  • 函数声明
  • 变量声明
  1. 作用域链([[scope]]),扩连作用域链

    作用域链是由当前环境与上层环境的一系列变量对象组成,它保证了对执行环境有权访问的所有变量和函数的有序访问

    上文的作用域中讲到过函数的作用域在函数定义的时候就决定了,因为函数有一个内部属性 [[scope]],当函数创建 的时候,就会保存所有父变量对象到其中,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到, 就会从自己的 [[scope]] 中保存的父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对 象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

  2. 确定 this 的指向 见 [this 到底是什么](#this 到底是什么 "#jump")

执行上下文的生命周期

创建阶段(生成变量对象, 建立作用域链, 确定 this 指向) => 执行阶段(变量赋值,函数引用、执行其它的代码) => 回收阶段

js 引擎如何管理执行上下文

html 复制代码
<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>浏览器环境下的执行上下文</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <script>
      // 全局执行上下文
      var author = "qinghuanI";

      function foo() {
        var author = "qinghuanI";
        // 函数执行上下文
      }

      foo();

      // eval 函数创建新执行上下文
      eval("var author = 'qinghuanI'");
    </script>
  </body>
</html>

javascript 引擎创建了执行上下文栈(Execution context stack, ECS) 来管理执行上下文。以 index.html 为例,

  1. 当 JS 引擎读取 script 标签时,会创建一个全局执行上下文,并将其推入当前的执行上下文栈中。创建变量对象,里面有未初始化的 author 变量,作用域链此时没有,确定 this 指向 window 对象。
  2. 引擎继续往下执行,执行 foo() 时,会为 foo 函数创建一个新的执行上下文并将其推到当前执行栈的顶端。此时会创建变量对象,建立作用域链,确定 this 指向 window 对象。

用伪代码解释上述过程:

js 复制代码
// 解析过程从
[
  {
    AO: { this: window },
    [[scope]]: {}
  }
];

// 变为

[
  {
    AO: {},
    this: window,
    [[scope]]: {},
  },
  {
    AO: {
      this: window,  // 函数执行时动态绑定
      arguments: [], // 为类数组
      author: "qinghuanI";
    },
    [[scope]]: {
      AO: {
        this: window,
        arguments: [], // 为类数组
        author: "qinghuanI";
      },
      GO: {
        this: window,
        window: {...},
        author: "qinghuanI";
      }
    }
  }
]

这种标识符(变量)的解析过程,与函数的生命周期有关。函数的生命周期可以分为创建和激活(调用时)两个阶段。在函数创建时,函数的内部存在一个 [[scope]] 属性(内部属性),[[scope]]是所有变量对象的层级链。[[scope]] 属性在函数创建时被存储,永远不变,直到函数被销毁。函数可以不被调用,但该属性一直存在。

csharp 复制代码
作用域链的长度与函数嵌套有关,比如在递归场景下。
this 指向与函数被调用时的所属对象有关
变量对象与函数体内声明变量的关键字有关(var 与 const let 有区别)

闭包

当函数可以记住并访问所在的词法作用域时,即时函数是在当前词法作用域之外执行,这时就产生了闭包

index.html 为例讲解闭包

html 复制代码
<!-- index.html -->

<script>
  function foo() {
    var author = "qinghuanI";
    function bar() {
      console.log(this); // window
      return author;
    }
    return bar;
  }

  var fn = foo(); // fn === function bar() {/* code */}
  fn();
</script>

调用 foo 函数,返回值是一个函数,由上文提到的执行上下文可知,每调用一个函数,就会创建一个执行上下文,如果声明了一个函数,没有调用,但函数本身依旧有一个作用域,作用域链由词法环境决定,只要调用了 fn 函数,即为依旧与 foo 的函数作用域构成作用域链,依旧可以访问 author 变量。

什么情况下会产生闭包?

  • 为创建内部作用域而调用了一个包装函数;
  • 包装函数的返回值必须至少包括一个对内部函数的引用

this 到底是什么


this 提供了一种更优雅的方式来隐式"传递"上下文对象

this 是在运行时进行绑定的,并不是在编写时绑定的,它的指向取决于函数调用时的各种 条件,确定 this 的指向和函数声明的位置没有任何关系,只取决于函数的调用方式

我把 this 指向分为两种情况:

  1. 能找到调用该函数的对象,即 this 指向该对象 (new 调用一个函数, this 指向实例)
  2. 不能找到调用该函数的对象,即 this 执行 window 这个兜底对象
html 复制代码
<!-- index.html -->
// ...

<script>
  console.log(this); // window

  const blog = {
    title: "再谈 this 指向和执行上下文",
    author: function () {
      console.log(this); // blog
    },
  };

  blog.author();

  const foo = function () {
    console.log(this); // window
  };

  foo();
</script>

ES6 中的箭头函数

  • 箭头函数没有自己的 this,只能继承上一个执行上下文的 this 指向;
  • call/apply/bind 无法改变箭头函数中 this 的指向;
  • 箭头函数不能作为构造函数使用,没有 arguments,prototype 属性;
  • 箭头函数不能作 Generator 函数,不能使用 yield 关键字。

call、apply 和 bind

call、apply 和 bind 为了改变某个函数运行时的上下文而存在,换句话说,就是为了改变函数体内部 this 指向。

html 复制代码
// index.html //...
<script>
  const blog = {
    title: "再谈 this 指向和执行上下文",
  };

  const getTitle = function () {
    console.log("title:", this.title); // '再谈 this 指向和执行上下文'
    console.log("arguments:", [arguments]); // [1, 2]
  };

  getTitle.call(blog, 1, 2);
</script>

//...
相关推荐
小行星125几秒前
前端把dom页面转为pdf文件下载和弹窗预览
前端·javascript·vue.js·pdf
Lysun00110 分钟前
[less] Operation on an invalid type
前端·vue·less·sass·scss
土豆湿16 分钟前
拥抱极简主义前端开发:NoCss.js 引领无 CSS 编程潮流
开发语言·javascript·css
J总裁的小芒果25 分钟前
Vue3 el-table 默认选中 传入的数组
前端·javascript·elementui·typescript
Lei_zhen9628 分钟前
记录一次electron-builder报错ENOENT: no such file or directory, rename xxxx的问题
前端·javascript·electron
辣条小哥哥29 分钟前
electron主进程和渲染进程之间的通信
javascript·electron·ecmascript
咖喱鱼蛋30 分钟前
Electron一些概念理解
前端·javascript·electron
yqcoder31 分钟前
Vue3 + Vite + Electron + TS 项目构建
前端·javascript·vue.js
鑫宝Code1 小时前
【React】React Router:深入理解前端路由的工作原理
前端·react.js·前端框架
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法