What is this?

basis

In JavaScript, the value to which this points is closely related to JavaScript’s underlying execution logic (especially the scope logic), and to figure out what this is, you inevitably have to dig deep into the ECMAScript specification. While I don’t want to enumerate the complex and extensive execution logic of the specification, there are a few basic concepts to understand first:

ECMAScript Specification Type

In the ECMAScript specification, data types fall into two broad categories:

  • ECMAScript Language Type
  • ECMAScript Specification Type

Language Type is the data Type that we can actually use in JavaScript, such as number, NULL, string, etc.

The Specification Type is an abstract data Type that exists only in the Specification or kernel to facilitate the description of execution logic and is not exposed to the user (us programmers).

For example: A Language Type is like the API that the back end gives you that you can access through HTTP, and the actual implementation of the API is the code (or the logic defined in the code) on the back end, equivalent to the Specification Type.

Execution Context and Environment Record

An Environment Record is a Specification Type that describes the execution Environment, which can be thought of as holding scope information. Each Environment Record has an OuterEnv member to Record the outer Environment, a HasThisBinding method to determine if there is a bound this value, and a WithBaseObject method to return the bound object. The value returned is undefined except in the context in which the with statement was created.

A simple TypeScript description:

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

There are two subtypes of Environment Record that are closely related to this:

Function Environment Record

Represents the function execution environment, created while the function is running, and records the value and initialization state of this. Function Environment Record’s OuterEnv depends on the context in which the Function is defined, and HasThisBinding returns the value depending on the initialization state of this.

class FunctionEnvironmentRecord extends EnvironmentRecord {
  OuterEnv: EnvironmentRecord;
  ThisValue: any; // The value of this saved
  ThisBindingStatus: 'lexical' | 'uninitialized' | 'initialized'; // The initialization state of this
  HasThisBinding() {
    if (this.ThisBindingStatus === 'lexical') {
      return false;
    } else {
      return true; }}BindThisValue(thisValue: any) {
    if (this.ThisBindingStatus === 'initialized') {
      throw new ReferenceError(a); }this.ThisValue = thisValue;
    this.ThisBindingStatus = 'initialized';
  }
  GetThisBinding() {
    if (this.ThisBindingStatus === 'uninitialized') {
      throw new ReferenceError(a); }return this.ThisValue; }}Copy the code

Global Environment Record

OuterEnv is null for the Global Environment Record. HasThisBinding always returns True.

class GlobalEnvironmentRecord extends EnvironmentRecord {
  OuterEnv: null;
  GlobalThisValue: globalThis; // In the browser is window, node.js is global
  HasThisBinding() {
    return true;
  }
  GetThisBinding() {
    return this.GlobalThisValue; }}Copy the code

An Execution Context is a Specification Type that holds information about the Execution of code, organized together by an Execution Context stack. The element at the top of the stack is called the Running Execution Context.

Execution Context has a member called LexicalEnvironment, which is of the type Environment Record mentioned above.

interface ExecutionContext {
  LexicalEnvironment: EnvironmentRecord;
}
Copy the code

Each time the function runs, a new Environment Record and execution context are created, and the new Environment Record is assigned to the LexicalEnvironment member of the new Execution context. The execution context is then pushed into the Execution Context stack, and the new execution context becomes the Running Execution Context.

Reference Record

A Reference Record is also a Specification Type used to represent references to values.

There are two members:

  • Base
  • ReferencedName

Base is the referenced object, the Value may be ECMAScript Language Value or Environment Record except null, undefined, ReferencedName is the referenced attribute name, the Value may be a string or Symbol type

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

Here’s an example:

// Access foo globally with the variable name
foo;
/** * ReferenceRecord {* Base: GlobalEnvironmentRecord; * ReferencedName: 'foo'; *} * /

// Attribute access
foo.bar.value;
/** * ReferenceRecord { * Base: foo.bar; * ReferencedName: 'value'; *} * /
Copy the code

this

Why bother to explain execution context, Environment Record, Reference Record?

The answer has to do with the reference logic of this:

  1. Get the current running context (running execution context) LexicalEnvironment (called env)
  2. Check if env is boundthisValue (calling the HasThisBinding method)
    1. If so, return the bound value (GetThisBindin)
    2. OuterEnv if no, replace env with env.OuterEnv and go to Step 2

That said, the reference logic for this is pretty straightforward: OuterEnv starts with the lexical environment of the current execution context, and starts with the OuterEnv layer by layer until it finds a suitable lexical environment. The value of this bound to the lexical environment, because when executing the script code, The Global Environment Record is the Global object, and the Global Environment Record is the Global object.

As stated earlier, when a function is executed, an execution context (localEnv for short) is created and made running execution context

Then our question becomes:

  1. What is the OuterEnv of localEnv?
  2. What factors affect localEnv ThisBindingStatus (HasThisBinding return value)?
  3. When will localEnv’s BindThisValue method be called and what arguments will it take?

The LexicalEnvironment that gets running Execution context when a function is defined is stored in an internal member of the function called Environment, There is also an internal member called ThisMode that holds the binding mode for the function this value, which is lexical when the function is arrow function, strict when the function is strict mode, and global if neither.

ThisBindingStatus = ThisMode ThisBindingStatus = ThisMode ThisBindingStatus = ThisMode ThisBindingStatus = ThisMode ThisMode is lexical if it is lexical, and uninitialized otherwise.

This explains why arrow functions don’t have their own this:

The HasThisBinding of the localEnv created by calling the arrow function always returns false, so the arrow function’s this value points to the localEnv’s OuterEnv, which is the running Execution context in which the arrow function was defined

OuterEnv and BindThisStatus will bind localEnv’s this using the following logic:

  1. Gets the value of the called functionReference Record(called ref) and ThisMode of the function
  2. Define a variable:thisValueAnd determine if ref.base is oneLanguage Type
    • If so, the function is called as a method of an object, and ref.Base is assigned tothisValue.
    • If not, an identifier (variable name) is used to access the function, in which case the type of ref.base isEnvironment Record, the return value of ref.base.WithBaseObject (i.eundefinedAssigned tothsiValue.
  3. Check whether ThisMode isstrict
    • If so, use it directlythisValueBind:localEnv.BindThisValue(thisValue))
    • If not, judgethisValueWhether it isundefinedornull
      • If so, global object binding is used 🙁localEnv.BindThisValue(globalThis))
      • If not, usethisValueWrapper object binding 🙁localEnv.BindThisValue(Object(thisValue)))

Step 3 shows the difference when calling methods of primitive types, but has no effect on ordinary objects:

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

' '.typeOfThis(); // 'object'
' '.strictTypeOfThis(); // 'string'
Copy the code

The logic for this from binding to reference is now complete, so we can answer the previous question and summarize:

What is the OuterEnv of localEnv?

OuterEnv is the LexicalEnvironment that runs the execution context when the function is defined. The OuterEnv is stored in the internal Environment member when the function is defined and then assigned to the localEnv when it is called

What factors affect localEnv ThisBindingStatus(HasThisBinding return value)?

ThisBindingStatus depends on the form of the function definition and the initialization process. ThisBindingStatus will be initialized according to the form of the function definition when the function is called: If the arrow function is initialized to lexical, and otherwise uninitialized, uninitialized will be updated to initialized with the binding of this

When will localEnv’s BindThisValue method be called and what arguments will it take?

BindThisValue is called after ThisBindStatus is initialized. The parameters of the call depend on ThisMode and how the function is referenced. The value may be undefined, globalThis, or an object that refers to a function (it may be a primitive value, object, or wrapper object).

Finally, a bit of imaginary code from the ECMAScript specification will help you understand:


// This is our imaginary execution stack
const executionContexts: ExecutionContext[] = [];

// The global lexical environment is created when the script runs
const globalEnvironmentRecord = new GlobalEnvironmentRecord();
/ / pressure stack
executionContexts.push({
  LexicalEnvironment: ExecutionContext
});

// Use it to represent the function object we defined
interface FunctionObject {
  Environment: EnvironmentRecord;
  ThisMode: 'lexical' | 'strict' | 'global';
  Call(thisValue: any) :any;
}

// Define the function
function DeclareFunction(
  fn: () => any,
  isArrowFunction: boolean,
  isStrictMode: boolean
) :FunctionObject {
  // Get the running execution context
  const runningExecutionContext = executionContexts[0];
  const fnObj: FunctionObject = {};

  // Save the definition environment
  fnObj.Environment = runningExecutionContext.LexicalEnvironment;

  / / save thisMode
  if (isArrowFunction) {
    fnObj.ThisMode = 'lexical';
  } else if (isStrictMode) {
    fnObj.ThisMode = 'strict';
  } else {
    fnObj.ThisMode = 'global';
  }

  // Function call logic, here does not consider the function argument passing
  fnObj.Call = function(thisValue: any) {
    / / create localEnv
    const calleeContext = PrepareForFunctionCall(fnObj);
    / / bind this
    BindThis(fnObj, calleeContext, thisValue);
    const result = fn();
    // Execute the function to push the current context off the stack
    executionContexts.pop();
    return result;
  }

  return fnObj;
}

// The execution context used to create the function is merged into the stack
function PrepareForFunctionCall(fnObj: FunctionObject) {
  const localEnv = new FunctionEnvironmentRecord();
  OuterEnv assigns the function definition environment to the OuterEnv
  localEnv.OuterEnv = fnObj.Environment;

  // initialize ThisBindingStatus
  if (fnObj.ThisMode === 'lexical') {
    localEnv.ThisBindingStatus = 'lexical';
  } else {
    localEnv.ThisBindingStatus = 'uninitialized';
  }

  // Pushes the created context, and the current running context becomes the function's context
  const fnExecutionContext: ExecutionContext = {
    LexicalEnvironment: localEnv
  };
  executionContexts.push(fnExecutionContext);
  return fnExecutionContext;
}
// Bind ThisValue to Environment Record
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)); }}// Use this function to simulate a function call
function CallFunction(ref: ReferenceRecord) {
  // Get the value of the reference
  const fnObj = ref.Base[ref.ReferencedName];
  // Get the value of this to bind, which may or may not be the final this
  const thisValue = GetThisValue();
  return fnObj.Call(thisValue);
}
// Get the value of this to bind according to the type of ref
function GetThisValue(ref: ReferenceRecord) {
  if (ref.Base instanceof EnvironmentRecord) {
    // If EnvironmentRecord returns undefined
    return ref.Base.WithBaseObject();
  } else {
    // Otherwise return the corresponding object
    returnref.Base; }}// Finally use this function to simulate this value
function ResolveThisBinding() {
  const runningExecutionContext = executionContext[0];
  let envRec = runningExecutionContext.LexicalEnvironment;
  // There is no possibility of an infinite loop because the outermost Global Environment Record always returns true
  while(envRec.HasThisBinding() === false) {
    envRec = envRec.OuterEnv;
  }
  return envRec.GetThisBinding();
}


// Simulate function definition logic
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;

// simulate the call
CallFunction({
  Base: foo,
  ReferencedName: 'test'
});

/** * equivalent to: * foo.test(); * /

Copy the code

At this point, I’ve covered 70% of the logic associated with this.

We know from the creation of the function binding this to the retrieval of this value, how can only 70%

Since there are also new, super, bind, call/apply, etc., any function call involved can affect the value of this.

With so much left, how can you get 70%

Although many, but the logic is similar, but some branches have some differences, the next common logic supplement.

Function.prototype.call/Function.prototype.apply

Function.prototype.call and function.prototype. apply differ from normal calls in that call and apply bind this with an explicitly specified value (the first parameter).

// This is the object we explicitly specify as this
declare bar;

// Use this function to simulate the call/apply call
function CallFunctionWithThis(ref: ReferenceRecord, thisValue: any) {
  // Get the value of the reference
  const fnObj = ref.Base[ref.ReferencedName];
  // Use the explicitly specified value directly, supplied to the BindThis binding
  fnObj.Call(thisValue);
}

/ / call:
CallFunctionWithThis(
  {
    Base: foo,
    ReferencedName: 'test'
  },
  bar
);
/** * is equivalent to: foo.test.apply(bar) * or foo.test.call(bar) */

Copy the code

That’s all

Function.prototype.bind

Function.prototype.bind returns a Function bound to the specified this value, which is really just a wrapper around the original Function:

interface BoundFunction {
  Call(): any;
  BoundThis: any; // bind this
  BoundTargetFunction: FunctionObject; / / function
}
// Simulate the definition of 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);
// equivalent to: const boundFooTest = foo.test.bind(bar);

// The call is the same as the normal call, but the this value of boundFunction* is not affected by the way it is called

CallFunction({
  Base: globalEnvironmentRecord,
  ReferencedName: 'boundFooTest'
});
// equivalent to boundFooTest();

Copy the code

new

When a function is called as a constructor, the value of this is an object modeled after the function’s prototype object (_proto_).

Expand DeclareFunction and CreateBoundFunction to add constructor simulation:

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() {
+ // Create context
+ const calleeContext = PrepareForFunctionCall(fnObj);
+ // Create new object as 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();
+}

Copy the code

Call:

const foo = ConstructCallFunction({
  Base: foo,
  ReferencedName: 'test'
})
// equivalent to: const foo = new foo.test();
Copy the code

super

In es6-enabled environments, we can call the superclass constructor in a subclass using the super keyword.

Constructors of a class go through much the same process as normal functions, but they are assigned an inner member called ConstructorKind, which is base when defined, or derived if the class is derived from another class or function.

When called, ConstructorKind will choose a different this binding logic based on:

class A {
  constructor() {
    console.log(this.__proto__ === C.prototype); }}class B extends A {
  constructor() {
    super(a);console.log(this.__proto__ === C.prototype);
    return {
      value: 'value b'}}}class C extends B {
  constructor() {
    super(a);console.log(this.__proto__ === C.prototype);
    console.log(this); }}new C();
/** * print * true * true * false * {value: 'value b'} */
Copy the code

When the constructor of the root class is called, it is the same as the normal constructor, except that the value of this is based on the object created by the subclass instead of the parent class.

When a subclass constructor is called, the creation and binding of this are skipped after the new localEnv and bound to the value returned by the superclass constructor during the execution of super.

This explains why super must be called in the constructor of a subclass before this can be accessed:

A ReferenceError will be raised when localEnv’s this value is initialized before super.

conclusion

As one of the great metaphysics of JavaScript, the value referenced by this may be affected by many factors, but as long as we understand the basic principle, we can still sort out a general judgment idea:

Let’s look at the way the function is defined: if the function is an arrow function, then the value this points to within the function is the value of the environment in which it is defined.

If so, the root class’s this is a new object created from the prototype object (__proto__) of the called class, and the subclass’s this is the value returned by its parent class constructor.

Bind is a Bound Function returned by function.prototype. bind. If so, the value of this is determined by the parameter passed in.

One thing to note here: The Bound Function’s this is already fixed when it is defined and there is no way to override it by calling bind repeatedly

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

This is the value we explicitly specify when called by call or apply, this is the object when called by object methods, and undefined when called by variable names.

Methods that specify this explicitly include array.prototype. some, array.prototype. every, etc.

Finally, remember that undefined is replaced by global objects in non-strict mode, and primitive types are converted to their corresponding wrapper objects.

The resources

  • ECMAScript® 2022 Language Specification (Draft ECMA-262 / March 24, 2021)