Write apply, call and bind~ again based on cat vs. Dog ultraman

I’ll never see you rip my head off again!

Today, I saw a strange description about call and apply when I was brushing the question. I felt quite interesting, so I wrote the logic of call and apply again by hand, and learned something new by reviewing the past

It goes something like this:

  • Cats eat fish, dogs eat meat, ultraman fights little monsters
  • Dog eats fish: cat eats fish. Call (dog, fish)
  • Cat fighting little monster: Ultraman. Call a little monster.

Apply and bind apply and bind apply and bind

The premise

First prepare three objects: cat, dog, ultraman:

let cat = {
  name: "Cat".eatFish() {
    console.log(`The ${this.name}Eat fish! `); }};let dog = {
  name: "Dog".eatMeat() {
    console.log(`The ${this.name}Eat meat! `); }};let ultraman = {
  name: "Tiga".fight() {
    console.log(`The ${this.name}Fight the little monster! `); }};Copy the code

Once we’re ready, let’s implement call.

call

Dogs eating fish need to be used like this: cat. Call (dog, fish); call(cat, fish); call(dog, fish);

cat.eatFish.call(dog, "Dog");
Copy the code

For the call method, the general logic looks like this:

  1. The first parameter passed in is treated as the context, in this case the dog
  2. Dog adds a fish-eating method that points to cat’s fish-eating method, which is cat’s this
  3. Dogs can eat all kinds of fish, too
  4. After eating, the dog deletes this method of eating fish, because this does not belong to it, but just borrows it

Following the above logic, we can write:

Function.prototype.defineCall = function (context, ... args) {
  // Do not pass dog, default is window
  var context = context || window;
  // Dog adds a method that points to the cat's fish-eating method, i.e., this
  context.fn = this;
  // Dogs can eat all kinds of fish, which can have multiple parameters
  letresult = context.fn(... args);// delete dog eating fish
  delete context.fn;
  return result;
};
Copy the code

Now, a custom call is almost complete! Now put it to the test:

cat.eatFish.defineCall(dog, "Dog");
ultraman.fight.defineCall(cat, "Cat");
// output:
// The dog eats the fish!
// The cat fights the little monster!
Copy the code

Now the dog can eat the fish, and the cat can fight the little monster!

Now let’s get the dog to eat more fish. Let’s change the cat’s fish diet briefly:

let cat = {
  name: "Cat".eatFish(. args) {
    console.log(`The ${this.name}Eat fish! To eat is:${args}`); }};Copy the code

Then we call it like this:

cat.eatFish.defineCall(dog, "Salmon".Tuna fish."Shark");

/ / the output:
// The dog eats the fish! Food: salmon, tuna, shark
Copy the code

Arguments can be used as arguments.

apply

The use of apply and call is similar except that the second argument is an array. We can write it like this:

Function.prototype.defineApply = function (context, arr) {
  var context = context || window;
  let result;
  context.fn = this;
  if(! arr) {// If no argument is passed, execute directly
    result = context.fn();
  } else {
    // Execute if there are argumentsresult = context.fn(... arr); }delete context.fn;
  return result;
};
Copy the code

Now call it again to see if it is correct:

cat.eatFish.apply(dog, ["Dog"]);
ultraman.fight.apply(cat, ["Cat"]);

/ / the output:
// The dog eats the fish!
// The cat fights the little monster!
Copy the code

Success! 🎉

bind

Now that call and apply are implemented, we can use bind, which is a little bit more difficult.

Let’s take a look at what bind has:

  1. bindIt’s also used to convertthisPointing to.
  2. bindInstead of executing immediately like these two, a binding is returnedthisTo execute, you need to call it again.
  3. bindSupport for currification of functions.
  4. bindOf the new functionthisIt cannot be changed,call å’Œ applyYou can’t.

Let’s do it step by step. Let’s start with the simplest one:

Function.prototype.defineBind = function (obj) {
  // If this is not stored, this may refer to window during execution
  let fn = this;
  return function () {
    fn.apply(obj);
  };
};
Copy the code

Then add the parameter pass function to it and it looks like this:

Function.prototype.defineBind = function (obj) {
  // Bit 0 is this, so you have to crop from the first
  let args = Array.prototype.slice.call(arguments.1);
  // If this is not stored, this may refer to window during execution
  let fn = this;
  return function () {
    fn.apply(obj, args);
  };
};
Copy the code

And then we add cremation to it:

Function.prototype.defineBind = function (obj) {
  // Bit 0 is this, so you have to crop from the first
  let args = Array.prototype.slice.call(arguments.1);
  let fn = this;
  return function () {
    // Second call we also grab arguments objects
    let params = Array.prototype.slice.call(arguments);
    // Note the order of concat
    fn.apply(obj, args.concat(params));
  };
};
Copy the code

DefineBind is pretty much starting to look like a bind now, so to upgrade it to a bind, there is one more detail:

The returned callback function can also be constructed as new, but its this is ignored during construction, and the returned instance still inherits the constructor and stereotype attributes of the constructor, and can receive attributes as normal (i.e., only this is missing, otherwise normal).

The constructor of an instance of a constructor refers to the constructor itself:

function Fn(){};
let o = new Fn();
console.log(o.constructor === Fn);
//true
Copy the code

And when the constructor is run, the inner this points to the instance (whoever calls it points to this), so this.constructor refers to the constructor:

function Fn() {
  console.log(this.constructor === Fn); 
  //true
};
let o = new Fn();
console.log(o.constructor === Fn); 
//true
Copy the code

Is it possible to change archetypal inheritance by changing the orientation of this.contructor?

Of course the answer is right! When returning a function as a constructor, this should refer to the instance; when returning a function as a normal function, this should refer to the current context:

Function.prototype.defineBind = function (obj) {
  let args = Array.prototype.slice.call(arguments.1);
  let fn = this;
  let bound = function () {
    let params = Array.prototype.slice.call(arguments);
    // Call from constructor: true this points to the instance, otherwise obj
    fn.apply(this.constructor === fn ? this : obj, args.concat(params));
  };
  // Prototype chain inheritance
  bound.prototype = fn.prototype;
  return bound;
};
Copy the code

This way, a bind is basically done, and the constructor is not affected by the instances generated by the returned constructor.

But! Modifying the instance stereotype directly affects the constructor!

What about this? If only there were nothing in the constructor prototype, then there would be no interaction… Blablabla…

Write a small example of using a mediation so that the prototype of a constructor affects only the instance and nothing else:

function Fn() {
  this.name = "123";
  this.sayAge = function () {
    console.log(this.age);
  };
}
Fn.prototype.age = 26;
// create a blank function Fn1, just copy Fn's prototype
let Fn1 = function () {};
Fn1.prototype = Fn.prototype;

let Fn2 = function () {};
Fn2.prototype = new Fn1();
Copy the code

– Added a __proto__ layer to Fn2 so that the prototype of Fn2 points to an instance of Fn, so that Fn2 changes don’t affect Fn (__proto__.__proto__ can still be modified)!

Function.prototype.defineBind = function (obj) {
  let args = Array.prototype.slice.call(arguments.1);
  let fn = this;
  // Create a mediation function
  let fn_ = function () {};
  // Fn2 is bound here
  let bound = function () {
    let params = Array.prototype.slice.call(arguments);
    // Call from constructor: true this points to the instance, otherwise obj
    fn.apply(this.constructor === fn ? this : obj, args.concat(params));
  };
  fn_.prototype = fn.prototype;
  bound.prototype = new fn_();
  return bound;
};
Copy the code

Add a final touch with an error:

Function.prototype.defineBind = function (obj) {
  if (typeof this! = ="function") {
    throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  };
  let args = Array.prototype.slice.call(arguments.1);
  let fn = this;
  // Create a mediation function
  let fn_ = function () {};
  // Fn2 is bound here
  let bound = function () {
    let params = Array.prototype.slice.call(arguments);
    // Call from constructor: true this points to the instance, otherwise obj
    fn.apply(this.constructor === fn ? this : obj, args.concat(params));
  };
  fn_.prototype = fn.prototype;
  bound.prototype = new fn_();
  return bound;
};
Copy the code

Handwritten bind finished!

Finally, let’s test it with a dog eating a fish:

let cat = {
  name: "Cat".eatFish(. args) {
    console.log(`The ${this.name}Eat fish! To eat is:${args}`); }};let dog = {
  name: "Dog"
};
cat.eatFish.defineBind(dog, "Salmon".Tuna fish) ("Shark");

// output:
// The dog eats the fish! Food: salmon, tuna, shark
Copy the code

Bind (es6) : Bind (es6)

Function.prototype.defineBind = function (context, ... rest) {
  if (typeof this! = ="function") {
   throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  }
  var self = this;
  return function F(. args) {
    if (this instanceof F) {
      return newself(... rest, ... args); }return self.apply(context, rest.concat(args));
  };
};
Copy the code

References:

  • Js manual implementation of bind method, ultra detailed thinking analysis!

My public number: the front end stack in the road, share the front end knowledge, chewing the feeling is wonderful ~ for attention