深入理解JavaScript的this

吐槽君 分类:javascript

What is this?

基础

在JavaScript中,this的指向的值与JavaScript底层的执行逻辑(特别是作用域逻辑)密切相关,要想弄清this是什么,就不可避免得要深入ECMAScript的规范,虽然在这里我并不想生硬的列举规范中那些复杂难懂且牵涉广泛的执行逻辑,但是有几个基础概念首先要了解:

ECMAScript Specification Type

在ECMAScript规范中,数据的类型分为两大类:

  • ECMAScript Language Type
  • ECMAScript Specification Type

其中Language Type就是JavaScript中我们实际能使用到的数据类型,比如number, null, string等。

Specification Type是只存在与规范或者内核中,用于方便描述执行逻辑的一种抽象数据类型,这些类型不会暴露给用户(我们程序员)使用。

打个比方:Language Type像是后端给你提供的API,你能通过HTTP访问,而真正实现这些API的是后端的代码(或者说代码中定义的逻辑),相当于Specification Type

Execution Context 与 Environment Record

Environment Record是一种描述执行环境的Specification Type,可以想像成里面保存着作用域信息。每个Environment Record都有个OuterEnv成员用于记录外层的环境,以及HasThisBinding方法用于判断是否有绑定的this值,最后有一个WithBaseObject方法返回绑定的对象,除了with语句创建的环境以外返回的值都是undefined,篇幅有限,这里不深入with的逻辑。

用TypeScript简单描述:

class EnvironmentRecord {
  OuterEnv: EnvironmentRecord | null;
  HasThisBinding(): boolean;
  WithBaseObject() {
    return undefined;
  }
}
 

Environment Record有两个子类型与this的关系密切:

Function Environment Record

表示函数执行环境,在函数运行时创建,记录着this的值和初始化状态。Function Environment RecordOuterEnv取决于函数定义时的上下文,HasThisBinding返回的值取决于this的初始化状态。

class FunctionEnvironmentRecord extends EnvironmentRecord {
  OuterEnv: EnvironmentRecord;
  ThisValue: any; // 所保存的this的值
  ThisBindingStatus: 'lexical' | 'uninitialized' | 'initialized'; // this的初始化状态
  HasThisBinding() {
    if (this.ThisBindingStatus === 'lexical') {
      return false;
    } else {
      return true;
    }
  }
  BindThisValue(thisValue: any) {
    if (this.ThisBindingStatus === 'initialized') {
      throw new ReferenceError();
    }
    this.ThisValue = thisValue;
    this.ThisBindingStatus = 'initialized';
  }
  GetThisBinding() {
    if (this.ThisBindingStatus === 'uninitialized') {
      throw new ReferenceError();
    }
    return this.ThisValue;
  }
}
 

Global Environment Record

表示全局执行环境,保存的this值指向全局对象,在执行脚本时创建,Global Environment RecordOuterEnvnullHasThisBinding始终返回true

class GlobalEnvironmentRecord extends EnvironmentRecord {
  OuterEnv: null;
  GlobalThisValue: globalThis; // 在浏览器是window, node.js则是global
  HasThisBinding() {
    return true;
  }
  GetThisBinding() {
    return this.GlobalThisValue;
  }
}
 

Execution Context(执行上下文)是一种用来保存代码运行信息的Specification Type,通过execution context stack(执行栈)组织在一起,栈顶的元素被称为running execution context

Execution Context的有一个成员叫LexicalEnvironment(词法环境),它的类型就是上面提到的Environment Record

interface ExecutionContext {
  LexicalEnvironment: EnvironmentRecord;
}
 

每当函数运行时,会创建一个新的Environment Recordexecution context,将新的Environment Record赋值给新execution context的LexicalEnvironment成员,随后将execution context压入execution context stack,于是这个新的execution context就成为了running execution context(当前运行上下文)。

Reference Record

Reference Record也是一种Specification Type,它用于表示值的引用关系。

其中有两个成员:

  • Base
  • ReferencedName

Base是被引用的对象,值可能是除nullundefined外的ECMAScript Language Value或者是Environment RecordReferencedName是引用的属性名,值可能是字符串或是Symbol类型

interface ReferenceRecord {
  Base: EnvironmentRecord | ECMAScriptLanguageValue;
  ReferencedName: string;
}
 

举个栗子:

// 在全局用变量名访问foo
foo;
/**
 * 用Reference Record表示成:
 * ReferenceRecord {
 *    Base: GlobalEnvironmentRecord;
 *    ReferencedName: 'foo';
 * }
 */

// 属性访问
foo.bar.value;
/**
 * ReferenceRecord {
 *    Base: foo.bar;
 *    ReferencedName: 'value';
 * }
 */
 

this

为什么费老大的劲去解释execution context, Environment Record, Reference Record

答案跟this的引用逻辑有关:

  1. 获取当前运行上下文(running execution context)的LexicalEnvironment(记作env)
  2. 判断env是否有绑定的this值(调用HasThisBinding方法)
    1. 如果有,则返回绑定的值(GetThisBindin)
    2. 如果没有,用env.OuterEnv替代env继续执行步骤2

也就是说,this的引用逻辑非常简单直白:从当前执行上下文的词法环境开始,顺着OuterEnv一层层往外找,直到找到符合要求的词法环境,然后这个环境绑定的this值,由于执行脚本代码时,首先会创建一个词法环境为Global Environment Record的执行上下文压入执行栈,所以步骤2循环到最后会遇到全局上下文的词法环境,这时this就是全局对象。

前面说过:当执行函数时,会创建函数的execution context(下面简称localEnv)并使其成为running execution context

那么我们的问题就变成了:

  1. localEnv的OuterEnv是什么?
  2. localEnv的ThisBindingStatus(HasThisBinding的返回值)受什么因素影响?
  3. localEnv的BindThisValue方法什么时候会被调用,以及调用的参数是什么?

当一个函数定义时会获取running execution context(函数定义时所处的上下文而不是执行时的上下文)的LexicalEnvironment保存在一个叫Environment的函数内部成员内,另外还有一个内部成员叫ThisMode,用来保存函数this值的绑定模式,当函数是箭头函数时这个值为lexical,当函数是严格模式时这个值为strict,如果都不是则设置为global

而函数被调用时,函数内部的Environment成员会原封不动赋值给localEnv的OuterEnv,localEnv的ThisBindingStatus则根据ThisMode决定:如果ThisMode是lexical则为lexical,否则为uninitialized

这解释了箭头函数为什么没有自己的this:

调用箭头函数所创建localEnv的HasThisBinding始终返回false,于是箭头函数的this值会指向localEnv的OuterEnv,即是箭头函数定义时的running execution context

在设置OuterEnv与BindThisStatus后,会按照下面的逻辑绑定localEnv的this值:

  1. 获取被调用函数的Reference Record(记作ref)与函数的ThisMode
  2. 定义一个变量:thisValue, 并判断ref.Base是不是一个Language Type
    • 如果是,说明函数是被作为对象的方法调用,则将ref.Base赋值给thisValue
    • 如果不是,说明是使用标识符(变量名)访问函数,这时ref.Base的类型是Environment Record,将ref.Base.WithBaseObject的返回值(也就是undefined)赋值给thsiValue
  3. 判断ThisMode是否为strict
    • 如果是,直接使用thisValue进行绑定:(localEnv.BindThisValue(thisValue)
    • 如果不是,判断thisValue是否为undefined或是null
      • 如果是,则使用全局对象绑定:(localEnv.BindThisValue(globalThis)
      • 如果不是,则使用thisValue的包装对象绑定:(localEnv.BindThisValue(Object(thisValue))

步骤3会在调用基本类型的方法时体现出差异,对于普通对象则没有影响:

String.prototype.typeOfThis = function() {
  return typeof this;
}
String.prototype.strictTypeOfThis = function() {
  'use strict';
  return typeof this;
}

''.typeOfThis(); // 'object'
''.strictTypeOfThis(); // 'string'
 

至此this从绑定到引用的逻辑都介绍完了,在这里回答前面的问题,同时做个小结:

localEnv的OuterEnv是什么?

函数localEnv的OuterEnv就是函数定义时running execution context的LexicalEnvironment,函数定义时保存在内部Environment成员中,调用时再赋值给localEnv

localEnv的ThisBindingStatus(HasThisBinding的返回值)受什么因素影响?

ThisBindingStatus的值取决于函数定义的形式与初始化进程,在函数被调用时会根据函数的定义形式初始化ThisBindingStatus的值:如果是箭头函数初始化成lexical,否则是uninitialized,随着this的绑定uninitialized会更新为initialized

localEnv的BindThisValue方法什么时候会被调用,以及调用的参数是什么?

BindThisValue在完成ThisBindStatus初始化后调用,调用的参数决定于ThisMode和函数的引用方式。值可能是undefinedglobalThis,引用函数的对象(可能是原始值,对象或包装对象)。

最后用一段根据ECMAScript规范虚构的代码加深理解:


// 这是我们虚构的执行栈
const executionContexts: ExecutionContext[] = [];
// 脚本运行时会创建全局的词法环境
const globalEnvironmentRecord = new GlobalEnvironmentRecord();
// 压栈
executionContexts.push({
LexicalEnvironment: ExecutionContext
});
// 用它表示我们定义的函数对象
interface FunctionObject {
Environment: EnvironmentRecord;
ThisMode: 'lexical' | 'strict' | 'global';
Call(thisValue: any): any;
}
// 定义函数
function DeclareFunction(
fn: () => any,
isArrowFunction: boolean,
isStrictMode: boolean
): FunctionObject {
// 获取running execution context
const runningExecutionContext = executionContexts[0];
const fnObj: FunctionObject = {};
// 保存定义环境
fnObj.Environment = runningExecutionContext.LexicalEnvironment;
// 保存thisMode
if (isArrowFunction) {
fnObj.ThisMode = 'lexical';
} else if (isStrictMode) {
fnObj.ThisMode = 'strict';
} else {
fnObj.ThisMode = 'global';
}
// 函数的调用逻辑,这里不考虑函数参数的传递
fnObj.Call = function(thisValue: any) {
// 创建localEnv
const calleeContext = PrepareForFunctionCall(fnObj);
// 绑定this
BindThis(fnObj, calleeContext, thisValue);
const result = fn();
// 执行完函数把当前上下文出栈
executionContexts.pop();
return result;
}
return fnObj;
}
// 用于创建函数的执行上下文并入栈
function PrepareForFunctionCall(fnObj: FunctionObject) {
const localEnv = new FunctionEnvironmentRecord();
// 这里把函数的定义环境赋值给OuterEnv
localEnv.OuterEnv = fnObj.Environment;
// 初始化ThisBindingStatus
if (fnObj.ThisMode === 'lexical') {
localEnv.ThisBindingStatus = 'lexical';
} else {
localEnv.ThisBindingStatus = 'uninitialized';
}
// 将创建的上下文入栈,这时当前运行上下文就变成了函数的上下文
const fnExecutionContext: ExecutionContext = {
LexicalEnvironment: localEnv
};
executionContexts.push(fnExecutionContext);
return fnExecutionContext;
}
// 绑定 Environment Record 的 ThisValue
function BindThis(fnObj: FunctionObject, context: ExecutionContext, thisValue: any) {
const lexicalEnvironment = context.LexicalEnvironment;
if (fnObj.ThisMode === 'strict') {
lexicalEnvironment.BindThisValue(thisValue);
} else if (thisValue === undefined || thisValue === null) {
lexicalEnvironment.BindThisValue(globalEnvironmentRecord.GlobalThisValue);
} else {
lexicalEnvironment.BindThisValue(Object(thisValue));
}
}
// 用这个函数模拟函数调用
function CallFunction(ref: ReferenceRecord) {
// 获取引用对应的值
const fnObj = ref.Base[ref.ReferencedName];
// 获取待绑定的this值,这个值不一定是最终的this
const thisValue = GetThisValue();
return fnObj.Call(thisValue);
}
// 根据ref的类型获取待绑定的this值
function GetThisValue(ref: ReferenceRecord) {
if (ref.Base instanceof EnvironmentRecord) {
// 如果是EnvironmentRecord返回undefined
return ref.Base.WithBaseObject();
} else {
// 否则返回对应的对象
return ref.Base;
}
}
// 最后用这个函数模拟this值的获取
function ResolveThisBinding() {
const runningExecutionContext = executionContext[0];
let envRec = runningExecutionContext.LexicalEnvironment;
// 这里不可能会有死循环,因为最外层的Global Environment Record始终会返回true
while(envRec.HasThisBinding() === false) {
envRec = envRec.OuterEnv;
}
return envRec.GetThisBinding();
}
// 模拟函数定义逻辑
const foo = {value: 'foo value'};
const test = DeclareFunction(
function() {
const that = ResolveThisBinding();
console.log(that.value);
},
false,
true
);
/**
*  相当于:
*  const test = function() {
*    'use strict';
*    const that = this;
*    console.log(that.value);
*  }
*/
foo.test = test;
// 模拟调用
CallFunction({
Base: foo,
ReferencedName: 'test'
});
/**
*  相当于:
*  foo.test();
*/

到这里,this相关的逻辑已经介绍了70%了。

从函数创建绑定thisthis值的获取都知道了,怎么才70%

因为还有newsuperbindcall/apply等,只要涉及到函数调用,就有可能影响this的值。

还剩这么多,怎么能够70%

虽然多,但调用的逻辑都是相似的,无非是某些分支有一些区别,接下来把常用的逻辑补充下。

Function.prototype.call/Function.prototype.apply

Function.prototype.callFunction.prototype.apply与常规调用的区别在于callapply会使用显式指定的值(第一个参数)绑定this,沿用前面的虚拟代码解释:

// 这个是我们要显式指定为this的对象
declare bar;
// 用这个函数模拟call/apply调用
function CallFunctionWithThis(ref: ReferenceRecord, thisValue: any) {
// 获取引用对应的值
const fnObj = ref.Base[ref.ReferencedName];
// 直接使用显式指定的值,提供给BindThis绑定
fnObj.Call(thisValue);
}
// 调用:
CallFunctionWithThis(
{
Base: foo,
ReferencedName: 'test'
},
bar
);
/**
* 相当于:
* foo.test.apply(bar)
* 或
* foo.test.call(bar)
*/

仅此而已

Function.prototype.bind

Function.prototype.bind返回一个绑定指定this值的函数,事实上这个函数只是对原函数的一个包装:

interface BoundFunction {
Call(): any;
BoundThis: any; // 绑定的this
BoundTargetFunction: FunctionObject; // 原函数
}
// 模拟bind的定义
function CreateBoundFunction(fnObj: FunctionObject, boundThis: any): BoundFunction {
return {
BoundThis: boundThis,
BoundTargetFunction: fnObj,
Call() {
return this.BoundTargetFunction.Call(this.BoundThis);
}
};
}
const boundFooTest = CreateBoundFunction(foo.test, bar);
// 相当于:const boundFooTest = foo.test.bind(bar);
// 调用过程与常规调用一致,但*boundFunction*的this值不会受到调用方式的影响
CallFunction({
Base: globalEnvironmentRecord,
ReferencedName: 'boundFooTest'
});
// 相当于 boundFooTest();

new

当函数以构造函数调用时,this的值是一个以函数原型对象(prototype)为原型(_proto_)的对象。

拓展DeclareFunctionCreateBoundFunction,增加构造函数的模拟:

interface FunctionObject {
Environment: EnvironmentRecord;
ThisMode: 'lexical' | 'strict' | 'global';
Call(thisValue: any): any;
+ Construct(): object;
}
function DeclareFunction(
fn: () => any,
isArrowFunction: boolean,
isStrictMode: boolean
): FunctionObject {
const runningExecutionContext = executionContexts[0];
const fnObj: FunctionObject = {};
fnObj.Environment = runningExecutionContext.LexicalEnvironment;
if (isArrowFunction) {
fnObj.ThisMode = 'lexical';
} else if (isStrictMode) {
fnObj.ThisMode = 'strict';
} else {
fnObj.ThisMode = 'global';
}
fnObj.Call = function(thisValue: any) {
const calleeContext = PrepareForFunctionCall(fnObj);
BindThis(fnObj, calleeContext, thisValue);
const result = fn();
executionContexts.pop();
return result;
}
+ fnObj.Construct = function() {
+   // 创建上下文
+   const calleeContext = PrepareForFunctionCall(fnObj);
+   // 创建新对象作为this
+   const thisValue = Object.create(fn.prototype);
+   BindThis(fnObj, calleeContext, thisValue);
+   fn();
+   executionContexts.pop();
+   return thisValue;
+ }
return fnObj;
}
function CreateBoundFunction(fnObj: FunctionObject, boundThis: any): BoundFunction {
return {
BoundThis: boundThis,
BoundTargetFunction: fnObj,
Call() {
return this.BoundTargetFunction.Call(this.BoundThis);
}
+   Construct() {
+     return this.BoundTargetFunction.Construct();
+   }
};
}
+ function ConstructCallFunction(ref: ReferenceRecord) {
+   const fnObj = ref.Base[ref.ReferencedName];
+   return fnObj.Construct();
+ }

调用:

const foo = ConstructCallFunction({
Base: foo,
ReferencedName: 'test'
})
// 相当于: const foo = new foo.test();

super

在支持ES6的环境,我们可以在子类用super关键字在子类中调用父类的构造函数。

类的构造函数与普通函数定义的过程大致相同,但是会在定义时给构造函数分配一个名字叫ConstructorKind的内部成员,当一个类或者函数定义时值为base,如果这个类是继承于其他类或者函数则为derived

函数被调用时,基于ConstructorKind会选择不同的this绑定逻辑:

class A {
constructor() {
console.log(this.__proto__ === C.prototype);
}
}
class B extends A {
constructor() {
super();
console.log(this.__proto__ === C.prototype);
return {
value: 'value b'
}
}
}
class C extends B {
constructor() {
super();
console.log(this.__proto__ === C.prototype);
console.log(this);
}
}
new C();
/**
* 依次输出
* true
* true
* false
* {value: 'value b'}
*/

根类的构造函数被调用时,则与普通构造函数的调用过程一致,只不过这时this的值是基于子类而不是父类创建的对象。

而当子类的构造函数被调用时,在新建localEnv后会跳过this的创建和绑定,转而在super的执行过程中根据父类构造函数返回的值绑定。

这解释了为什么在子类的构造函数中必须先执行super后才能访问this

super执行前localEnvthis值还没完成初始化(ThisBindingStatus为uninitialized),这时引用会抛出一个ReferenceError异常。

总结

this作为JavaScript中的一大玄学,它引用的值可能会受到很多因素的影响,但只要摸清了基本的原理,还是能整理出一条大致的判断思路:

首先看函数的定义方式:如果这个函数是箭头函数,那么不用多考虑,函数内this指向的值就是它被定义的环境的this值。

然后判断是不是以构造函数的形式调用:如果是,根类的this是以被直接调用的类的原型对象(prototype)为原型(__proto__)创建的新对象,子类的this是它父类构造函数返回的值。

接着看这个函数是不是通过Function.prototype.bind返回的Bound Function,如果是,那this的值由传入的参数决定。

这里需要注意一点:Bound Functionthis在它被定义时就已经固定了,没有办法通过重复调用bind来覆盖

function test() {
return this.value;
}
const boundFoo = test
.bind({value: 'value1'})
.bind({value: 'value2'});
boundFoo(); // 返回 value1

最后看函数的调用方式:通过callapply调用时this就是我们显式指定的值,通过对象方法调用时this就是所在的对象,通过变量名调用则是undefined

可以显式指定this的方法除了callapply外还包括Array.prototype.some, Array.prototype.every等方法。

最后的最后,别忘了以上this的值在非严格模式时undefined会被全局对象取代,基本类型则会转变成它们对应的包装对象。

参考资料

  • ECMAScript® 2022 Language Specification(参考版本:Draft ECMA-262 / March 24, 2021)

回复

我来回复
  • 暂无回复内容