JavaScript运行机制解析(三)this还能这样学?

前言

在第一节中,我们介绍了可执行代码以及执行上下文的相关知识,对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object, VO
  • 作用域、作用域链(Scope chain
  • this指向问题

这节重点介绍this问题,这也就面试中高频遇到的。

注:全文硬核烧脑,不建议碎片化时间阅读。

一、类型

在JavaScript中,说到类型,你们第一反应可能是基础数据类型和引用数据类型,但这节说的类型并非如此。类型又分为ECMAScript 语言类型规范类型

ECMAScript语言类型

ECMAScript 语言类型 是 ECMAScript 程序员使用 ECMAScript 语言直接操作的值对应的类型。ECMAScript 语言类型包括未定义 (Undefined)空值 (Null)布尔值(Boolean)字符串 (String)数值 (Number)对象 (Object)

规范类型

规范类型 是描述 ECMAScript 语言构造与 ECMAScript 语言类型语意的算法所用的元值对应的类型。规范类型包括 引用 、 列表 、 完结 、 属性描述符 、 属性标志符 、 词法环境(Lexical Environment)、 环境纪录(Environment Record)。规范类型的值是不一定对应 ECMAScript 实现里任何实体的虚拟对象。规范类型可用来描述 ECMAScript 表达式运算的中途结果,但是这些值不能存成对象的变量或是 ECMAScript 语言变量的值。

如果不能理解,没关系,只需要知道JavaScript中还存在一种用于规范类型,它的作用是用于描述语言的底层行为逻辑,并不真实存在于js代码中。

简单解释一下上面的规范类型:

词法环境

词法环境 是一个用于定义特定变量和函数标识符在 ECMAScript 代码的词法嵌套结构上关联关系的规范类型。一个词法环境由一个环境记录项可能为空的外部词法环境引用构成。

环境纪录项

环境记录项记录了在它的关联词法环境域内创建的标识符绑定情形。

分两类:声明式环境记录项对象式环境记录项 。声明式环境记录项用于定义那些将 标识符 与语言值直接绑定的 ECMA 脚本语法元素,例如 函数定义 , 变量定义 以及 Catch 语句。对象式环境记录项用于定义那些将 标识符 与具体对象的属性绑定的 ECMA 脚本元素,例如 程序 以及 With 表达式 。

通俗的理解就是:在创建执行上下文的时候,JS需要初始化函数声明、变量声明、函数形参定义,决定内部函数或者代码块的作用域链情况,不同类型的代码会记录在不同的变量对象(环境记录项)中。

这节重点要讲解的是引用(Reference),它跟我们要介绍的this的指向有很大的关联。

二、引用-Reference

1、什么是Reference?

引用ECMAscript中的原话是:

The Reference type is used to explain the behaviour of such operators as delete , typeof, and the assignment operators.

翻译过来是 Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。

2、Reference具体包含哪些内容?

A Reference is a resolved name binding. A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag. The base value is either undefined , an Object, a Boolean, a String, a Number, or an environment record (10.2.1). A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.

官方这段话描述的是Reference的构成,其中有三个部分组成,如下:

  1. 基值 (base value)
  2. 引用名称(referenced name
  3. 布尔值 严格引用 (strict reference) 标志

base value值是 undefined、Object、Boolean、 String、Number、environment record 中的任意一种。

referenced name就是属性名称。

strict reference用于表示当前环境类型,即是否为严格环境。

举例说明:

js 复制代码
// 举例一
var yc = 123;
// 对应的Reference为
var ycReference = {
  base: EnvironmentRecord,
  name: 'yc',
  strict: false
}
​
// 举例二
var yc = {
  fn: function() {
    return this;
  }
}
yc.fn();
​
//对应fn的Reference为
var fnReference = {
  base: yc,
  name: 'fn',
  strict: false
}
​

问题来了,规范内部如何确认basename的值是什么?

接下来我们就要说到GetBase(V)IsPropertyReference(V)

GetBase

GetBase(V). Returns the base value component of the reference V.

返回reference的base value

IsPropertyReference

IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.

简单理解为:如果base value是一个对象,就返回true

3、GetValue

GetValue是在规范Reference 8.7.1中提供了一个用于在Reference类型中获取对应值的方法。它的作用在于为一些表达式或者操作符的计算过程提供运算支持。

具体来看下面例子:

js 复制代码
// 举例一
var yc = 123;
// 对应的Reference为
var ycReference = {
  base: EnvironmentRecord,
  name: 'yc'
}
// 非js或者浏览器提供的方法,故不能在实际中进行计算
GetValue(ycReference) // 123

如上,它返回了Reference类型的值,属于真实的值。

结论:调用GetValue方法返回具体的值,而不再是一个Reference 谨记!这个结论会在后面的计算中使用非常频繁!

4、推算this的指向

上面一大篇幅在讲Reference类型相关的概念,可能听下来你还是没法理解具体的作用以及它跟this的指向到底存在什么样的关联。下面进入高能阶段~

直接上栗子🌰:

js 复制代码
'use strict'
function foo() {
  console.log(this);
}
foo();

我们一般在分析this指向的时候,通常会拿函数调用的方式来说明,比如上面的例子,我们从经验上一眼可以看清楚this指向的是undefined

再来一个栗子🌰:

js 复制代码
​
var name = '小明';
var yc = {
  name: '云场',
  fn: function() {
    console.log(this.name)
  }
}
// 示例一
yc.fn();
​
// 示例二
(false || yc.fn)();
​
// 示例三
(0, yc.fn)();
​

这时候,你还能快速准确的说出this的指向情况吗?自测一下~

想要搞清楚这个问题,首先我们先来看下规范中对于函数调用是如何描述的。

The production CallExpression : MemberExpression Arguments is evaluated as follows:

  1. Let ref be the result of evaluating MemberExpression.

  2. Let func be GetValue(ref).

  3. Let argList be the result of evaluating Arguments , producing an internal list of argument values (see 11.2.4).

  4. If Type(func ) is not Object, throw a TypeError exception.

  5. If IsCallable(func ) is false , throw a TypeError exception.

  6. If Type(ref ) is Reference, then

    1. If IsPropertyReference(ref ) is true, then

      1. Let thisValue be GetBase(ref).
    2. Else, the base of ref is an Environment Record

      1. Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).
  7. Else, Type(ref ) is not Reference.

    1. Let thisValue be undefined.

上面一串描述的是关于函数调用结果和表达式判断的概念,1,2,3,4,5表示的是函数调用结果的内部判断,1,6,7表示的是函数表达式的内部判断。表达式判断步骤如下:

  • 计算MemberExpression的结果并复制给ref
  • 判断ref是不是一个Reference类型
    • 如果ref是一个Reference类型,并且IsPropertyReference(ref)true,那么this值为GetBase(ref)返回值
    • 如果ref是一个Reference类型,并且base valueEnvironment Record,那么this值为ImplicitThisValue(ref)返回值
    • 如果ref不是一个Reference类型,那么this值为undefined

那么问题来了,第一步中MemberExpression是什么东西?

我们通过规范11.2 Left-Hand-Side Expressions 看到MemberExpression定义如下:

js 复制代码
// 成员表达式
MemberExpression :
  // 原始表达式
  PrimaryExpression 
  // 函数定义表达式
  FunctionExpression
  // 属性访问表达式
  MemberExpression [ Expression ]
  // 属性访问表达式
  MemberExpression . IdentifierName
  // new 创建对象表达式
  new MemberExpression Arguments

拿上面的例子来说明一下:

js 复制代码
​
// 示例一
yc.fn(); // yc.fn 是 MemberExpression
​
// 示例二
(false || yc.fn)(); // (false || yc.fn) 是 MemberExpression
​
// 示例三
(0, yc.fn)(); // (0, yc.fn) 是 MemberExpression

简单理解就是:括号() 左侧部分便是MemberExpression

如何判断ref是不是一个Reference类型?

判断一个ref是不是一个Reference,关键要看规范是如何处理不同类型的MemberExpression

拿上面的例子进行说明:

js 复制代码
'use strict'
var name = '小明';
var yc = {
  name: '云场'
  fn: function() {
    console.log(this.name)
  }
}
// 示例一
yc.fn();
​
// 示例二
(false || yc.fn)();
​
// 示例三
(0, yc.fn)();
​
// 示例四
function fn(){
  console.log(this);
}
fn();
示例一:yc.fn()

首先,我们知道这个MemberExpressionyc.fn,并且yc.fn为属性访问表达式Property Accessors,看规范11.2.1。

  1. Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString , and whose strict mode flag is strict.

翻译过来就是这个表达式会返回一个Reference类型。

根据上面的判断步骤,那IsPropertyReference(ref)返回的结果是什么?

还记得我们一开始说过IsPropertyReference是如何计算的吗?如果base value是一个对象,就返回true

通过上面的内容,我们知道yc.fnReference的值为:

js 复制代码
var name = '小明';
var yc = {
  name: '云场'
  fn: function() {
    console.log(this.name)
  }
}
//对应fn的Reference为
var fnReference = {
  base: yc,
  name: 'fn',
  strict: false
}

如上,base value的值为yc,是一个对象,那么IsPropertyReference(ref)返回true。那么这时候this的值为GetBase(ref)

js 复制代码
this = GetBase(ref)

通过前面的内容,我们说过GetBase(ref)返回的就是base value,即为yc,那么this = ycyc.fn返回云场

cao,终于验证了this是指向了yc的,真费劲~,不过按照这个步骤,接下来的表达式就很好验证了~

示例二:(false || yc.fn)()

以此类推,MemberExpression(false || yc.fn),一个逻辑运算符,我们来看规范11.11 Binary Logical Operators

  1. Let lref be the result of evaluating LogicalORExpression.
  2. Let lval be GetValue(lref).
  3. If ToBoolean(lval ) is true , return lval.
  4. Let rref be the result of evaluating LogicalANDExpression.
  5. Return GetValue(rref).

该运算符最终会返回一个GetValue(ref),根据第二节的第三小节,调用GetValue方法返回具体的值,而不再是一个Reference

根据判断步骤:

如果ref不是一个Reference类型,那么this值为undefined

所以,严格模式下函数调用会报错,否则返回小明

示例三:(0, yc.fn)()

(0, yc.fn)为逗号操作符,查看规范11.14 Comma Operator

  1. Let lref be the result of evaluating Expression.
  2. Call GetValue(lref).
  3. Let rref be the result of evaluating AssignmentExpression.
  4. Return GetValue(rref).

明显,也是跟示例二一样,最后this值为undefined。严格模式下函数调用会报错,否则返回小明

示例四:fn()

最普通、常见的函数调用方式,MemberExpressionfn,即解析标识符(函数名、变量名、属性名等),根据规范10.3.1 Identifier Resolution 。

The result of evaluating an identifier is always a value of type Reference with its referenced name component equal to the Identifier String.

该标识符表达式总是返回Reference类型。

js 复制代码
var fnReference = {
    base: EnvironmentRecord,
    name: 'fn'
};

根据判断步骤2.1:

如果ref是一个Reference类型,并且IsPropertyReference(ref)true,那么this值为GetBase(ref)返回值

那么IsPropertyReference(ref)的结果是什么?

根据前面的内容,如果base value是一个对象,IsPropertyReference(ref)就返回true ,然而fnReferencebase valueEnvironmentRecord,不是一个JavaScript对象类型,所以结果为false

根据判断步骤2.2:

如果ref是一个Reference类型,并且base valueEnvironment Record,那么this值为ImplicitThisValue(ref)返回值

那么thisImplicitThisValue(ref),而ImplicitThisValue(ref)又是什么东西?

根据规范10.2.1.1.6 ImplicitThisValue

Declarative Environment Records always return undefined as their ImplicitThisValue.

这玩意总是返回一个undefined。所以,fn()this的最终值为undefined。-_-

最终结果
js 复制代码
var name = '小明';
var yc = {
  name: '云场',
  fn: function() {
    console.log(this.name)
  }
}
// 示例一
var aa = yc.fn.bind(yc);
aa(); // 云场
​
// 示例二
(false || yc.fn)(); // 小明
​
// 示例三
(0, yc.fn)(); // 小明
​
// 示例四
function fn(){
  console.log(this.name);
}
fn(); // 小明

三、总结

综上,看似简单、普通的 this 指向问题,从ECMAScript实现上去理解却是复杂的,虽然从这个角度去分析比较复杂或者难以理解,一旦理解实现原理后,会给一个全新的视角去看待this指向问题。

小试牛刀

上面示例四中,fn()执行返回全局对象中的name(非严格模式),那改为call进行调用呢?尝试从规范的角度去解释~

js 复制代码
function fn(){
  console.log(this.name);
}
fn.call({name: '蛇皮'});
相关推荐
qiyue773 分钟前
AI编程专栏(三)- 实战无手写代码,Monorepo结构框架开发
前端·ai编程
轻语呢喃6 分钟前
React智能前端:从零开始的识图学单词项目(一)
javascript·react.js·aigc
断竿散人7 分钟前
JavaScript 异常捕获完全指南(下):前端框架与生产监控实战
前端·javascript·前端框架
Danny_FD9 分钟前
Vue2 + Vuex 实现页面跳转时的状态监听与处理
前端
小飞悟9 分钟前
别再只会用 px 了!移动端适配必须掌握的 CSS 单位
前端·css·设计
安思派Anspire10 分钟前
LangGraph + MCP + Ollama:构建强大代理 AI 的关键(一)
前端·深度学习·架构
LRH10 分钟前
JS基础 - 基于 Generator + Promise 实现 async/await 原理
前端·javascript
Jolyne_11 分钟前
可配置永久生效的Table组件的封装过程
前端·react.js
自由逐风11 分钟前
前端小数点精度问题解析
javascript
断竿散人11 分钟前
JavaScript 异常捕获完全指南(上):从同步异步到 Promise 错误处理
前端·javascript·promise