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 Record的OuterEnv取决于函数定义时的上下文,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 Record的OuterEnv是null
,HasThisBinding始终返回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 Record和execution 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是被引用的对象,值可能是除null
、undefined
外的ECMAScript Language Value或者是Environment Record,ReferencedName是引用的属性名,值可能是字符串或是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
的引用逻辑有关:
- 获取当前运行上下文(running execution context)的LexicalEnvironment(记作env)
- 判断env是否有绑定的
this
值(调用HasThisBinding方法)- 如果有,则返回绑定的值(GetThisBindin)
- 如果没有,用env.OuterEnv替代env继续执行步骤2
也就是说,this
的引用逻辑非常简单直白:从当前执行上下文的词法环境开始,顺着OuterEnv一层层往外找,直到找到符合要求的词法环境,然后这个环境绑定的this
值,由于执行脚本代码时,首先会创建一个词法环境为Global Environment Record的执行上下文压入执行栈,所以步骤2循环到最后会遇到全局上下文的词法环境,这时this
就是全局对象。
前面说过:当执行函数时,会创建函数的execution context(下面简称localEnv)并使其成为running execution context
那么我们的问题就变成了:
- localEnv的OuterEnv是什么?
- localEnv的ThisBindingStatus(HasThisBinding的返回值)受什么因素影响?
- localEnv的BindThisValue方法什么时候会被调用,以及调用的参数是什么?
当一个函数定义时会获取running execution context(函数定义时所处的上下文而不是执行时的上下文)的LexicalEnvironment保存在一个叫Environment的函数内部成员内,另外还有一个内部成员叫ThisMode,用来保存函数this
值的绑定模式,当函数是箭头函数时这个值为lexical
,当函数是严格模式时这个值为strict
,如果都不是则设置为global
。
而函数被调用时,函数内部的Environment成员会原封不动赋值给localEnv的OuterEnv,localEnv的ThisBindingStatus则根据ThisMode决定:如果ThisMode是lexical
则为lexical
,否则为uninitialized
。
在设置OuterEnv与BindThisStatus后,会按照下面的逻辑绑定localEnv的this值:
- 获取被调用函数的Reference Record(记作ref)与函数的ThisMode
- 定义一个变量:
thisValue
, 并判断ref.Base是不是一个Language Type- 如果是,说明函数是被作为对象的方法调用,则将ref.Base赋值给
thisValue
。 - 如果不是,说明是使用标识符(变量名)访问函数,这时ref.Base的类型是Environment Record,将ref.Base.WithBaseObject的返回值(也就是
undefined
)赋值给thsiValue
。
- 如果是,说明函数是被作为对象的方法调用,则将ref.Base赋值给
- 判断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就是函数定义时running execution context的LexicalEnvironment,函数定义时保存在内部Environment成员中,调用时再赋值给localEnv
ThisBindingStatus的值取决于函数定义的形式与初始化进程,在函数被调用时会根据函数的定义形式初始化ThisBindingStatus的值:如果是箭头函数初始化成lexical
,否则是uninitialized
,随着this
的绑定uninitialized
会更新为initialized
BindThisValue在完成ThisBindStatus初始化后调用,调用的参数决定于ThisMode和函数的引用方式。值可能是undefined
,globalThis
,引用函数的对象(可能是原始值,对象或包装对象)。
最后用一段根据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%了。
因为还有new
,super
,bind
,call
/apply
等,只要涉及到函数调用,就有可能影响this
的值。
虽然多,但调用的逻辑都是相似的,无非是某些分支有一些区别,接下来把常用的逻辑补充下。
Function.prototype.call/Function.prototype.apply
Function.prototype.call
与Function.prototype.apply
与常规调用的区别在于call
与apply
会使用显式指定的值(第一个参数)绑定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_)的对象。
拓展DeclareFunction
和CreateBoundFunction
,增加构造函数的模拟:
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
的执行过程中根据父类构造函数返回的值绑定。
总结
this
作为JavaScript中的一大玄学,它引用的值可能会受到很多因素的影响,但只要摸清了基本的原理,还是能整理出一条大致的判断思路:
首先看函数的定义方式:如果这个函数是箭头函数,那么不用多考虑,函数内this
指向的值就是它被定义的环境的this
值。
然后判断是不是以构造函数的形式调用:如果是,根类的this
是以被直接调用的类的原型对象(prototype
)为原型(__proto__
)创建的新对象,子类的this
是它父类构造函数返回的值。
接着看这个函数是不是通过Function.prototype.bind
返回的Bound Function,如果是,那this
的值由传入的参数决定。
function test() {
return this.value;
}
const boundFoo = test
.bind({value: 'value1'})
.bind({value: 'value2'});
boundFoo(); // 返回 value1
最后看函数的调用方式:通过call
、apply
调用时this
就是我们显式指定的值,通过对象方法调用时this
就是所在的对象,通过变量名调用则是undefined
。
可以显式指定this
的方法除了call
、apply
外还包括Array.prototype.some
, Array.prototype.every
等方法。
最后的最后,别忘了以上this
的值在非严格模式时undefined
会被全局对象取代,基本类型则会转变成它们对应的包装对象。
参考资料
- ECMAScript® 2022 Language Specification(参考版本:Draft ECMA-262 / March 24, 2021)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!