Skip to main content

☝ this keyword

Useful resources:

What is this ?

this is a binding that is made when a function is invoked, what it references is determined by entirely by the call-site where the function is called. When a function is invoked, an execution context is created which contains the following information:

Execution context

  • Where the function was called from (the call stack)
  • How the function was invoked
  • What parameters were passed
  • this

execution context

One of the most important properties of execution context is the this reference that will be used for the duration of that function call.

How to determine call-site ?

call-site: The location in code where a function is called (not where it's declared).

Because call stack is a stack of functions that have been called to get us to the current moment of execution. The call-site of a function is in the invocation before the currently executing function. In other words, it is the second item from the top of call stack. see the the image below

call-site explanation

Insert a debugger at the code above

call-site

In this example, the call-site of function foo (currently executing function), is in bar.

4 Rules

How to determine where this will point during the execution of a function ?

  1. Find the call-site
  2. Determine which of 4 rules applies.

These 4 rules are applied in this order:

  1. Called with new? Use the newly constructed object.
  2. Called with call, apply (or bind)? Use the specified object.
  3. Called with a context object in front of the call? Use that object. e.g. person.sayName()
  4. Default: strict mode ? undefined : globalThis

Default Binding

If a function is called with a plain, un-decorated function reference, then default binding applies. The default binding make this points at the global object.

standalone function invocation
function foo() {
console.log(this.a);
}

var a = 2; // is declared in the global scope

foo(); // 2 -- call-site at this line

Follow the instruction. The first step is to find the call-site

call-site of default binding example

In this example, foo() is called with a plain, un-decorated function reference, so default binding applies.

Strict Mode Behavior

In strict mode, the global object is not eligible for the default binding. That means this will be set to undefined.

"use strict" in function definition (contents)
function foo() {
"use strict";
console.log(this.a);
}

var a = 2; // is declared in the global scope

foo(); // Uncaught TypeError: Cannot read properties of undefined (reading 'a')

One important detail to remember is that: the global object is only eligible for the default binding if the contents of foo() are not running in strict mode. The strict mode in call-site of foo() is irrelevant.

"use strict" in call-site
function foo() {
console.log(this.a);
}

var a = 2; // is declared in the global scope

(() => {
"use strict";
foo(); // 2
})();

Implicit Binding

When a function preceded by an object is called, the implicit binding rule says: that object should be used as the function call's this binding.

Because person is the this for the sayName() call, therefore, this.name is synonymous with person.name.

Only the last object reference matters to the call-site.

Implicitly Lost

One common issue of implicit binding is that it might lose its this binding. That usually means this falls back to the default binding which is either the global object or undefined, depending on strict mode.

Implicitly lost - 01
function sayAgeFunc() {
console.log(`Hello, I'm ${this.age} years old.`);
}

const person = {
age: 22,
sayAge: sayAgeFunc,
};

var age = "global age!!!";

const sayAgeStandAlone = person.sayAge;

sayAgeStandAlone(); // Hello, I'm global age!!! years old.

In this example, the call-site of sayAgeStandAlone() is a plain, un-decorated call, therefore the default binding applies.

Implicitly lost - 02
function sayNameFunc() {
console.log(`Hello, my name is ${this.Name}.`);
}
function sayAgeFunc() {
console.log(`Hello, I'm ${this.Age} years old!`);
}

function introduce(first, second) {
first();
second();
}

const person = {
Name: "xiaohai",
Age: 22,
sayName: sayNameFunc,
sayAge: sayAgeFunc,
};

var Name = "[[Global Name]]";
var Age = "[[Global Age]]";

person.sayName(); // Hello, my name is xiaohai.
person.sayAge(); // Hello, I'm 22 years old!

introduce(person.sayName, person.sayAge);
// Hello, my name is [[Global Name]].
// Hello, I'm [[Global Age]] years old!
danger

As you can see that, this is changed unexpectedly, we don't have control over how the function will be executed. That means that we have no way of controlling the call-site to make function call's this to stick with the intended binding.

Explicit Binding

func.call(), func.apply(), func.bind()

func.call(), func.apply() takes an object as the first parameter which will be used as this, and invoke the function. Because you are directly defining what you want the this to be, it is known as explicit binding.

Explicit binding - 01
function sayNameFunc() {
console.log(`Hello, my name is ${this.Name}.`);
}

const person = {
Name: "xiaohai",
sayName: sayNameFunc,
};

person.sayName(); // Hello, my name is xiaohai.
sayNameFunc.call(person); // Hello, my name is xiaohai.

Invoking sayNameFunc with explicit binding by sayNameFunc.call(...) enable us to bind its this to be person.

note

If a primitive value (string, boolean, or number) is passed as the first parameter this binding, the primitive value is wrapped in its object-form (new String(...), new Boolean(...), or new Number(...), respectively). That behavior is known as "boxing".

Hard Binding

However, func.call(), func.apply() still doesn't solve the problem (implicit lost) "a function losing its intended this binding".

But a variation pattern around explicit binding can solve the problem.

We can use ES5: Function.prototype.bind to achieve the same result shown above. Below is a simple version of bind.

function bind(func, thisObject) {
return function (...args) {
// Don't forget to return the value computed by the func
return func.call(thisObject, ...args);
};
}

func.bind(...) returns a new function that will call the original function with this set as you specified.

info

In ES6, the function created by bind(...) has a name property that derived from the original target function. newFunc = func.bind(...), newFunc's name property should be bound func

new Binding

When a function is invoked with new in front of it, the following things are done automatically.

  1. a brand new object is created.
  2. the newly constructed object's prototype is set to Func.prototype. see constructor
  3. the newly constructed object is set as the this binding for the function call.
  4. execute the constructor to populate the newly constructed object if necessary.
  5. if there is no return, the function will return the newly constructed object. Otherwise, perform the normal return.
new binding example 1
info

mimic new operator

function mynew(Func, ...args) {
// 1.create a brand new object
const obj = {};
// 2.set the new object's prototype to be the Func.prototype.
obj.__proto__ = Func.prototype;
// 3.set `this` using .apply() and run the constructor code
let result = Func.apply(obj, args);
// 4.check if the constructor returns
return result instanceof Object ? result : obj;
}

Priority of Rules

What happened if the call-site has multiple eligible rules? What order to apply these rules?

  1. new binding
  2. explicit binding
  3. implicit binding
  4. default binding has the lowest priority among the 4.

Explicit vs Implicit

explicit > implicit
function sayNameFunc() {
console.log(`Hello, my name is ${this.Name}.`);
}

const person = {
Name: "xiaohai",
sayName: sayNameFunc,
};

const person2 = {
Name: "dan-dan",
};

person.sayName.call(person2);
// Hello, my name is Hello, my name is dan-dan.

new vs Explicit

new > explicit

Binding Exceptions

If you pass null or undefined as the first argument to call, apply, or bind, the value you passed will be ignored, and instead the default binding rule applies.

pass null to call
function sayNameFunc() {
console.log(`Hello, my name is ${this.Name}.`);
}

const person = {
Name: "xiaohai",
sayName: sayNameFunc,
};

var Name = "[[Global Name]]";

person.sayName.call(null);
// Hello, my name is [[Global Name]].

Lexical this

ES6 introduces arrow-function which doesn't follow the rules specified above.

arrow-functions adopt the this binding from the enclosing (function or global) scope.

arrow-function in object literal

arrow-function in object literal
const person1 = {
Name: "xiaohai",
sayName: () => {
console.log(`Hello, my name is ${this.Name}`);
},
};

const person2 = {};
person2.Name = "xiaohai";
person2.sayName = () => {
console.log(`Hello, my name is ${this.Name}`);
};

// pre-es6
var self = this; // captures this
const person3 = {};
person3.Name = "xiaohai";
person3.sayName = function () {
console.log(`Hello, my name is ${self.Name}`);
};

function sayName() {
console.log(`Hello, my name is ${this.Name}`);
}

var Name = "[[Global Name]]";
person1.sayName(); // Hello, my name is [[Global Name]]
person2.sayName(); // Hello, my name is [[Global Name]]
person3.sayName(); // Hello, my name is [[Global Name]]
sayName(); // Hello, my name is [[Global Name]]
polyfill for arrow-function
function Person(name) {
this.Name = name;
this.sayName = () => {
console.log(`Hello, my name is ${this.Name}`);
};
// pre-es6
var self = this;
this.sayNameFunc = function () {
console.log(`Hello, my name is ${self.Name}`);
};
}

const person = new Person("xiaohai");
// polyfill: these two ways of defining functions have the same effect
person.sayName();
person.sayNameFunc();
arrow-function's this cannot be changed after created

Instance methods in class are just normal function. see below

instance method behavior

Questions

Question 1
window.name = "hello";

function Foo() {
console.log(JSON.stringify(this)); // {}
this.name = "bar";
this.getName = function () {
return this.name;
};
}
const { log } = console;
let foo = new Foo(); // This line logs the newly constructed object {}
let getName = foo.getName;

log(foo.getName()); // new binding
log(getName()); // implicit binding lost - use default binding
log(Foo.bind(foo).getName); // Foo.getName is undefined
Question 2
var obj = {
a: 1,
foo: function (b) {
b = b || this.a;
return function (c) {
console.log(this.a);
console.log(b);
console.log(c);
};
},
};

var a = 2;
var obj2 = { a: 3 };

obj.foo(a).call(obj2, 1);
obj.foo.call(obj2)(1);

obj.foo.call(obj2) create a function close over foo so it can access to the value of b defined in foo. Secondly, it is a standalone function invocation, default binding rule is applied to determine whether this should point to globalThis or undefined .