目录

  • 执行上下文与执行上下文栈
  • this
    • 全局环境
    • 函数环境
  • 总结
  • 参考

1.执行上下文与执行上下文栈

(1)什么是执行上下文?

在 JavaScript 代码运行时,解释执行全局代码、调用函数或使用 eval 函数执行一个字符串表达式都会创建并进入一个新的执行环境,而这个执行环境被称之为执行上下文。因此执行上下文有三类:全局执行上下文、函数执行上下文、eval 函数执行上下文。

执行上下文可以理解为一个抽象的对象,如下图:

Variable object:变量对象,用于存储被定义在执行上下文中的变量 (variables) 和函数声明 (function declarations) 。

Scope chain:作用域链,是一个对象列表 (list of objects) ,用以检索上下文代码中出现的标识符 (identifiers) 。

thisValue:this 指针,是一个与执行上下文相关的特殊对象,也被称之为上下文对象。

(2)什么是执行上下文栈?

在全局代码中调用函数,或函数中调用函数(如递归)等,都会涉及到在一个执行上下文中创建另一个新的执行上下文,并且等待这个新的上下文执行完毕,才会返回之前的执行上下文接着继续执行,而这样的调用方式就形成了执行上下文栈

示例代码:

  1. function A() {
  2. console.log('function A')
  3. B()
  4. }
  5. function B() {
  6. console.log('function B')
  7. C()
  8. }
  9. function C() {
  10. console.log('function C')
  11. }
  12. A()

上述示例代码,当执行到函数 C时,此时的执行上下文栈如下图:

2.this

首先需要清楚,this 是执行上下文的一个属性,而不是某个变量对象的属性,是一个与执行上下文相关的特殊对象。由于在开发中不推荐或应尽量避免使用 eval 函数,所以在这里我们主要讨论全局执行上下文(全局环境)和函数执行上下文(函数环境)中的 this。

(1)全局环境

无论是否在严格模式下,在全局环境中(在任何函数体外部的代码),this 始终指向全局对象(在浏览器中即 window)。

示例代码(浏览器中):

  1. console.log(this === window) // true
  2. a = 1;
  3. console.log(window.a) // 1
  4. console.log(this.a === window.a) // true
  5. this.b = "test"
  6. console.log(window.b) // test
  7. console.log(b) //test

(2)函数环境

在大多数情况下,函数的调用方式决定了 this 的值。 this 是不能够在执行期间被赋值修改的,并且在每次函数被调用时其 this 可能不同(通过 apply 或 call 方法显示设置 this 等)。

另外,ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数如何被调用的。ES6 引入了支持 this 词法解析的箭头函数(它在闭合的执行环境内设置 this 的值)。

接下来我们主要分析:函数的调用方式是如何决定 this 的值?(对于 bind 方法以及箭头函数将留于下一篇文章进行详细分析)

要弄明白这个问题,我们来看看 EcmaScript 5.1标准的规定,了解一下 函数调用 的规范:

11.2.3 函数调用

产生式 CallExpression : MemberExpression Arguments 按照下面的过程执行 :

  1. 令 ref 为解释执行 MemberExpression 的结果 .
  2. 令 func 为 GetValue(ref).
  3. 令 argList 为解释执行 Arguments 的结果 , 产生参数值们的内部列表 (see 11.2.4).
  4. 如果 Type(func) is not Object ,抛出一个 TypeError 异常 .
  5. 如果 IsCallable(func) is false ,抛出一个 TypeError 异常 .
  6. 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 , 令 thisValue 为 GetBase(ref).ImplicitThisValue().
  7. 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.
  8. 返回调用 func 的 [[Call]] 内置方法的结果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表

产生式 CallExpression : CallExpression Arguments以完全相同的方式执行,除了第1步执行的是其中的CallExpression。

简单解析:

第1步,令 ref 为 MemberExpression 解释执行的结果。

11.2 左值表达式 中有提到,MemberExpression 可以是以下五种表达式中的任意一种:

  • PrimaryExpression // 原始表达式
  • FunctionExpression // 函数定义表达式
  • MemberExpression [ Expression ] // 属性访问表达式
  • MemberExpression . IdentifierName // 属性访问表达式
  • new MemberExpression Arguments // 对象创建表达式

简单理解 MemberExpression 就是调用一个函数的()左侧的部分。

第2~5步,获取调用函数的参数列表以及检测所调用的函数是否合法,否则抛出相应异常(GetValue的作用是获取 Reference 类型具体的值)。

第6、7步,就是决定函数调用的 this 的值的关键步骤,翻译一下,如同下面的伪代码:

  1. var thisValue = getThisValue(ref)
  2. function getThisValue(ref) {
  3. // 判断 ref 的类型是否是 Reference,如果不是,直接返回 undefined
  4. if(Type(ref) !== Reference) return undefined
  5. // 是否是 Object, Boolean, String, Number
  6. if(IsPropertyReference(ref)) {
  7. return GetBase(ref)
  8. } else {
  9. // 是一个环境记录项(Environment record),调用其 ImplicitThisValue 方法
  10. return GetBase(ref).ImplicitThisValue()
  11. }
  12. }

关于 GetBase 和 IsPropertyReference 方法:

  • GetBase(V), 返回引用值 V 的基值 (Reference 的基值 base,详见下面提到的 Reference 的组成)。
  • HasPrimitiveBase(V), 如果基值是 Boolean, String, Number,那么返回 true。
  • IsPropertyReference(V), 如果基值是个对象或 HasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false。

而对于 ImplicitThisValue 方法,其属于环境记录项(Environment record)的方法。而环境记录项分为两种:

  • 声明式环境记录项:每个声明式环境记录项都与一个包含变量和(或)函数声明的 ECMA 脚本的程序作用域相关联。声明式环境记录项用于绑定作用域内定义的一系列标识符。其 ImplicitThisValue 永远返回 undefined。

  • 对象式环境记录项:每一个对象式环境记录项都有一个关联的对象,这个对象被称作 绑定对象 。对象式环境记录项直接将一系列标识符与其绑定对象的属性名称建立一一对应关系。其 ImplicitThisValue 通常返回 undefined,除非其 provideThis 标识的值为 true。具体如下:

    1. 令 envRec 为函数调用时对应的声明式环境记录项。
    2. 如果 envRec 的 provideThis 标识的值为 true,返回 envRec 的绑定对象。
    3. 否则返回 undefined。

    对象式环境记录项可以通过配置的方式,将其绑定对象合为函数调用时的隐式 this 对象的值。这一功能用于规范 With 表达式(12.10 章 )引入的绑定行为。该行为通过对象式环境记录项中布尔类型的 provideThis 值控制,默认情况下,provideThis 的值为 false。(只有使用了 with 表达式,才会将 provideThis 标识的值为 true)

而上面提到了两种新的类型: 引用规范类型 (Reference)与 环境记录项(Environment record)都是属于ECMAScript 的规范类型,相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。

而与规范类型相对于的就是语言类型:就是开发者直接使用的类型,即Undefined, Null, Boolean, String, Number, 和 Object。(ECMAScript的类型分为语言类型和规范类型)

从上面的伪代码中可以看到 thisValue 的值与 ref 是否是引用规范类型(Reference)有直接关联,即调用一个函数时,其()左侧的部分的解释执行的结果的类型是不是 Reference 类型,将直接影响 thisValue 的值。

EcmaScript 5.1标准中的 Reference 的规范:

8.7 引用规范类型 (Reference)

Reference 类型是用来说明 delete,typeof,赋值运算符这些运算符的行为。

一个 Reference 是个已解决的命名绑定。其由三部分组成, 基值 (base) , 引用名称(referenced name) 和布尔值 严格引用 (strict reference) 标志。

基值是 undefined, Object, Boolean, String, Number, Environment record 中的任意一个。基值是 undefined 表示此引用可以不解决一个绑定。引用名称是一个字符串。严格引用标志表示是否在严格模式下解释执行的代码。

而引用规范类型(Reference)会被用在标识符解析中,标识符执行的结果总是一个 Reference 类型的值。

EcmaScript 5.1标准中的 标识符解析 的规范:

10.3.1 标识符解析

标识符解析是指使用正在运行的执行环境中的词法环境,通过一个 标识符 获得其对应的绑定的过程。在 ECMA 脚本代码执行过程中,PrimaryExpression : Identifier 这一语法产生式将按以下算法进行解释执行:

  1. 令 env 为正在运行的执行环境的 词法环境 。
  2. 如果正在解释执行的语法产生式处在 严格模式下的代码 中,则仅 strict 的值为 true,否则令 strict 的值为 false。
  3. 以 env,Identifier 和 strict 为参数,调用 GetIdentifierReference 函数,并返回调用的结果。

解释执行一个标识符得到的结果必定是 Reference 类型的对象,且其引用名属性的值与 Identifier 字符串相等。

GetIdentifierReference 函数就是返回一个 Reference 类型的对象,类似如下对象:

  1. var valueOfReferenceType = {
  2. base: <base object>, // Identifier 所处的环境(Environment Record)或者 Identifier 属性所属的对象
  3. propertyName: <property name>, // 与 Identifier 字符串相等
  4. strict: <boolean>
  5. };

因此,我们可以来看一些相关的示例代码。

第一组:非严格模式和严格模式的全局函数

  1. function foo() {
  2. console.log(this)
  3. }
  4. function bar() {
  5. 'use strict'
  6. console.log(this)
  7. }
  8. foo() // global
  9. bar() // undefined
  10. // foo 标识符对应的 Reference
  11. var fooReference = {
  12. base: EnvironmentRecord,
  13. propertyName: 'foo',
  14. strict: false
  15. }
  16. // bar 标识符对应的 Reference
  17. var barReference = {
  18. base: EnvironmentRecord,
  19. propertyName: 'bar',
  20. strict: true
  21. }

上述代码中,对于 fooReference,根据函数调用规范可知其 this = getThisValue(fooReference) = GetBase(fooReference).ImplicitThisValue() = undefined,而 barReference 也是一样。

但为什么 foo() 输出的是 global 全局对象而不是 undefined 呢?这是因为在非严格模式下, 当 this 的值为 undefined 时,会被隐式转换为全局对象。而在严格模式下,指定的 this 不再被封装为对象。

第二组:对象的属性访问

  1. var foo = {
  2. bar: function () {
  3. console.log(this)
  4. }
  5. }
  6. foo.bar() // foo
  7. // foo 的 bar 属性对应的 Reference
  8. var barReference = {
  9. base: foo,
  10. propertyName: 'bar',
  11. strict: false
  12. }

上述代码中,对于 barReference,根据函数调用规范可知 this = getThisValue(barReference) = GetBase(barReference) = foo

foo.bar()中,MemberExpression 计算的结果是 foo.bar,为什么它是一个 Reference 类型呢?

EcmaScript 5.1标准中的 属性访问 的规范:

11.2.1 属性访问

  1. 返回一个 Reference 类型的值,其基值为 baseValue 且其引用名为 propertyNameString, 严格模式标记为 strict.

这里只引用了最后一步,属性访问最终返回的值是一个 Reference 类型。

第三组:非 Reference 类型的函数调用

首先,需要简单了解一下 GetValue 方法,其作用是获取 Reference 类型具体的值,返回结果不再是一个 Reference。例如:

  1. var foo = 1
  2. // foo 标识符对应的 Reference
  3. var fooReference = {
  4. base: EnvironmentRecord,
  5. propertyName: 'foo',
  6. strict: false
  7. }
  8. GetValue(fooReference) // 1

示例代码:

  1. value = 1
  2. var foo = {
  3. value: 2,
  4. bar: function () {
  5. console.log(this.value)
  6. }
  7. };
  8. foo.bar(); // 2
  9. (foo.bar)(); // 2
  10. (false || foo.bar)(); // 1
  11. (foo.bar = foo.bar)(); // 1
  12. (foo.bar, foo.bar)(); // 1

在上述示例代码中:

  1. 对于 (foo.bar),foo.bar 被 () 包住,使用了分组运算符,查看规范 11.1.6 分组操作符,可知分组表达式不会调用 GetValue 方法, 所以 (foo.bar)仍旧是一个 Reference 类型,因此 this 为 Reference 类型的 base 对象,即 foo。
  2. 对于 (false || foo.bar),有逻辑与算法,查看规范 11.11 二元逻辑运算符,可知二元逻辑运算符调用了 GetValue 方法,所以false || foo.bar不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。
  3. 对于 (foo.bar = foo.bar),有赋值运算符,查看规范 11.13.1 简单赋值,可知简单赋值调用了 GetValue 方法,所以foo.bar = foo.bar不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。
  4. 对于 (foo.bar, foo.bar),有逗号运算符,查看规范 11.14 逗号运算符,可知逗号运算符调用了 GetValue 方法,所以foo.bar, foo.bar不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。

3.总结

  1. 在全局环境(全局执行上下文)中(在任何函数体外部的代码),this 始终指向全局对象
  2. 在函数环境(函数执行上下文)中,绝大多数情况,函数的调用方式决定了 this 的值,这与调用函数的()左侧的部分 MemberExpression 的解释执行的结果的类型是不是 Reference 类型直接关联。

4.参考

this 关键字 - JavaScript | MDN - Mozilla

深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)

深入理解JavaScript系列(13):This? Yes,this!

JavaScript深入之从ECMAScript规范解读this

ECMAScript5.1中文版

ECMAScript 5.1 pdf(英)

深入理解JS:执行上下文中的this(一)的更多相关文章

  1. 从一道看似简单的面试题重新理解JS执行机制与定时器

     壹 ❀ 引 最近在看前端进阶的系列专栏,碰巧看到了几篇关于JS事件执行机制的面试文章,因为我在之前一篇 JS执行机制详解,定时器时间间隔的真正含义 博文中也有记录JS执行机制,所以正好用于作为测试自 ...

  2. 深入理解JS 执行细节

    javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之前,我们需要引入几个相关的概念:执行环境栈.全局对象.执行环境.变量对象.活动对象.作用域和作用域链等 ...

  3. 深入理解JS执行细节(写的很精辟)

    来源于:http://www.cnblogs.com/onepixel/p/5090799.html javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之 ...

  4. 深入理解js——执行上下文

    什么是"执行上下文"?暂且不下定义,先看一段代码: 第一句报错,a未定义,很正常.第二句.第三句输出都是undefined,说明浏览器在执行console.log(a)时,已经知道 ...

  5. 深入学习JS执行--创建执行上下文(变量对象,作用域链,this)

    一.介绍 本篇继上一篇深入理解js执行--单线程的JS,这次我们来深入了解js执行过程中的执行上下文. 本篇涉及到的名词:预执行,执行上下文,变量对象,活动对象,作用域链,this等 二.预执行 在上 ...

  6. JS执行机制详解,定时器时间间隔的真正含义

     壹 ❀ 引 通过结果倒推过程是我们常用的思考模式,我在上一篇学习promise笔记中,有少量关于promise执行顺序的例子,通过倒推,我成功让自己对于js执行机制的理解一塌糊涂,js事件机制,事件 ...

  7. 深入理解JS:执行上下文中的this(二)

    目录 序言 Function.prototype.bind() 方法 箭头函数 参考 1.序言 在 深入理解JS:执行上下文中的this(一) 中,我们主要深入分析全局环境和函数环境中函数调用的 th ...

  8. js的基础(平民理解的执行上下文/调用堆栈/内存栈/值类型/引用类型)

    与以前的切图比较,现在的前端开发对js的要求似乎越来越高,在开发中,我们不仅仅是要知道如何运用现有的框架(react/vue/ng), 而且我们对一些基础的知识的依赖越来越大. 现在我们就用平民的方法 ...

  9. 10分钟理解JS引擎的执行机制

    首先,请牢记2点: (1) JS是单线程语言 (2) JS的Event Loop是JS的执行机制.深入了解JS的执行,就等于深入了解JS里的event loop 1.灵魂三问 (1) JS为什么是单线 ...

随机推荐

  1. 手把手教你用Rancher创建产品质量数据库设置

    目标:在本文中,我们将介绍如何运行一个分布式产品质量数据库设置,它由Rancher进行管理,并且保证持久性.为了部署有状态的分布式Cassandra数据库,我们将使用Stateful Sets (有状 ...

  2. 使用cpplint检测代码规范

    0. cpplint - python脚本, google使用它作为自己的C++代码规范检查工具: 1. 安装 方法一: $sudo apt-get install python-pip $pip i ...

  3. CodeForces - 1176A Divide it! (模拟+分类处理)

    You are given an integer nn. You can perform any of the following operations with this number an arb ...

  4. 一只简单的网络爬虫(基于linux C/C++)————守护进程

    守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程.它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.守护进程常常在系统引导装入时启动, ...

  5. Nginx模块开发(4)————使用subrequest访问第三方服务

    该模块可以完成如下的功能,当我们输入http://你的ip/lcw?s_sh000001时,会使用subrequest方式得到新浪服务器上的上证指数,代码如下: //start from the ve ...

  6. idea 将项目托管到 Git 报错:Can't finish Gitee sharing process

    在idea中报: Can't finish Gitee sharing processSuccssully created project 'dmp' on Gitee. but initial co ...

  7. js 跳出循环

    js 循环主要有 for while 主要有三种方式 :break continue return break是跳出当前整个循环语句,循环终止会继续执行该循环之后的代码 而continue是跳过当前循 ...

  8. P1635 跳跃

    传送门 观察到\(4x+3=2(2x+1)+1\)以及\(8x+7=2(2(2x+1)+1)+1\) 所以可以把\(xx->2x+12x+1\)当成一个基本变化 则\(xx->4x+3\) ...

  9. Jekyll 解决Jekyll server本地预览文章not found的问题

    layout: post tags: [Jekyll] comments: true 执行Jekyll本地浏览器预览指令 bundle exec jekyll serve 进入浏览器输入127.0.0 ...

  10. [hdu]5202

    思路:把所有'?'用'a'代替,如果冲突则最后一个改为'b',注意特判最后一个问号在中间的情况.