The realization of bind from this mechanism

Keywords: Javascript Attribute

Preface

From this article you can get:

  • Understanding the binding mechanism of this and its priority
  • Learn to use apply/call to implement bind function
  • Learn to implement bind functions without apply/call

Not much nonsense. Let's look at this binding mechanism.

this binding mechanism

At the beginning, it is concluded that this has four binding modes: default binding, implicit binding, explicit binding and new binding.

The priority relationship between them is: new binding > explicit binding > implicit binding > Default binding.

Let's look at some examples to clarify these kinds of bindings:

Example 1: Default binding

// Default binding
var str = 'hello world'
function log() {
    console.log(this.str) 
}

// At this point this defaults to window
log() // hello world

// In strict mode this defaults to undefined
'use strict'
var str2 = 'hello world'
function log() {
    console.log(this.str2) 
}

// At this point this points to undefined
log() // Error type Error because the program gets str2 from undefined

Example 2: Implicit binding

// Implicit binding typically occurs when a function is called as an object property
var bar = 'hello'
function foo() {
    console.log(this.bar)
}

var obj = {
    bar: 'world',
    foo: foo
}

foo() // hello, this points to window, so output hello
obj.foo() // World, this implicitly binds obj, and then this points to obj, so it outputs world

Column 3: Explicit binding

// Explicit binding is the application, call, bind that we often talk about.
var bar = 'hello'
var context = {
    bar: 'world'
}
function foo() {
    console.log(this.bar);
}

foo() // hello
foo.call(context) // The world can see that the direction of this has now become context.

Column 4: new binding

New binding is special. In most cases, new creates a new object, points this to the new object, and finally returns the object.

function Foo(bar) {
    this.bar = bar
}

// Create a new object and point this to it, returning the object to be assigned to foo
var foo = new Foo(3);
foo.bar // 3

After describing the binding type of this, let's consider the output of the following code

var context = {
    bar: 2
}

function Foo() {
    this.bar = 'new bar'
}

var FooWithContext = Foo.bind(context);
var foo = new FooWithContext();

// Consider the output of the following code
console.log(foo.bar) 
console.log(context.bar)

// The result is: new bar 2
/** 
  * We can see that although this will be bound to context using the bind function,
  * But Foo, which is called by new, is not bound to context.
  */

Priority Validation of Four this Binding

From the example 2 above, it can be inferred that the implicit binding priority is higher than the default binding, so we only derive the following three binding priority relationships.

Explicit binding and new binding

Example 4:

// Let's first verify the priority relationship between implicit binding and explicit binding
var context = {
    bar: 1
}

var obj = {
    bar: 2
}

function foo() {
    // Assigning bar
    this.bar = 3;
}

// Explicit binding
var fooWithContext = foo.bind(context);

obj.foo = fooWithContext;
obj.foo()

console.log(obj.bar); // 2
console.log(context.bar); // 3

// So instead of changing the value of obj.bar, foo creates a new object that fits our description of the new binding.

From the above columns, we can conclude that new binding > explicit binding

Several other binding priorities

Based on examples 2 and 3, we can easily deduce that implicit binding and explicit binding have priority over default binding.

Let's verify the priority relationship between implicit binding and explicit binding.

var obj = {
    bar: 2
}

var context = {
    bar: 3
}

function foo () {
    this.bar = 4
}

// Bind this of foo to context
var fooWithContext = foo.bind(context);
// Assign the bound function to obj's attribute foo
obj.foo = fooWithContext;
obj.foo()

console.log(obj.bar); // 2 does not change the value of obj.bar
console.log(context.bar); // The value of 4 context.bar has changed

It can be seen that this priority of explicit binding is higher than that of implicit binding.

Finally, we can conclude that new binding > explicit binding > implicit binding > Default binding.

Implementation of bind with apply

So many of this binding issues have just been discussed, what does this really have to do with our implementation of bind?

Let's look at a simple implementation code for bind:

Function.prototype.bind(context, ...args) {
   const fn = this;
   return (...innerArgs) => {
       return fn.call(context, ...args, ...innerArgs)
   }
}

This bind function works well in most cases, but we consider the following scenarios:

function foo() {
    this.bar = 3
}

var context = {
    bar: 4
}

var fooWithContext = foo.bind(context);
var fooInstance = new fooWithContext();

console.log(context.bar) // 3

As you can see, foo called by new still points to context at run time, which is not in line with the binding priority we just inferred from the native method: new binding > explicit binding

So when we implement bind, we need to consider maintaining new calls.

Let's see how to achieve a real bind:

 Function.prototype.bind(context, ..args) {
     var fToBind = this;
     
     // Declare an empty function first, and use it later.
     var fNop = function() {};
     
     var fBound = function(...innerArgs) {
         // If called by new, this should be an instance of fBound
         if(this instanceof fBound) {
             /**
               * cover Live in the case of new calls
               * So actually we're going to simulate fToBind being called by new and return
               * We replace the context passed in by bind with the object created by new
               */
           return fToBind.call(this, ...args, ...innerArgs)
         } else {
            // Normal return in case of non-new call
            return fToBind.call(context, ...args, ...innerArgs)
         }
     }
     
     // In addition to maintaining the new this binding, we also need to maintain the prototype chain changes caused by new
     // After executing new, the prototype chain of the returned object will point to fToBind
     // But what we actually return after we call bind is fBound, so we need to replace the prototype of fBound here.
     
       fNop.prototype = this.prototype;
       // fBound.prototype.__proto__ = fNop.prototype
       fBound.prototype = new fNop();
       /**
         * So when new calls fBound, the instance can still access the prototype method of fToBind
         * Why not directly fBound.prototype = this.prototype?
         * Consider adding instance methods to fBound after returning fBound
         * That is fBound.prototype.anotherMethod = function() {}
         * If the prototype of fToBind is assigned directly to the prototype of fBound, adding the prototype method will
         * Pollution Source Method, the Prototype of fToBind
         */
     return fBound
 }

So far we have implemented a bind function that matches the native representation, but sometimes people can't help asking how to implement bind without apply and call. Next, we use implicit binding to implement a bind

Implementing bind without using apply and call

Now that we've just analyzed the points that need to be noted in implementing bind, let's not repeat them here. Let's see how to use implicit binding to mimic bind.

// We focus on how to replace the call method
Function.prototype.bind(context, ...args) {
    var fToBind = this;
    var fNop = function() {};
    var fBound = function(...innerArgs) {
        // We assign fToBind to an attribute of context.
        context.__fn = fToBind;
        if(this instanceof fBound) {
            // Simulate the new call, create a new object, and the prototype chain of the new object points to the prototype of fBound
            var instance = Object.create(fBound);
            instance.__fn = fToBind;
            var result = instance.__fn(...args, ...innerArgs);
            delete instance.__fn;
            // When a new call is made, if the constructor returns an object, replace this with the returned object
            if(result) return result;
            return instance;
        } else {
            // When _fn is not explicitly bound, _fn runtime this points to context
            var result = context.__fn(...args, ...innerArgs);
            // Delete the _fn attribute of context after calling
            delete context.__fn;
            return result;
      }
    }
    
    fNop.prototype = this.prototype;
    fBound.prototype = new fNop();
    return fBound;
}

Here, bind without apply is done.

summary

Let me summarize the following points that need to be identified:

  • this has four binding modes: default binding, implicit binding, explicit binding, and new binding.
  • The four priority relationships of this binding: new binding > explicit binding > implicit binding > default binding
  • Implementation of bind requires additional maintenance of new bindings

After seeing so much, some friends may ask, what about the arrow function?

Welcome to another article of mine: this in the arrow function

Reference material:

Posted by karlovac on Thu, 26 Sep 2019 06:16:15 -0700