JS手写实现call、apply、bind(超详细)

好久不更新了哦,最近又在找工作。复习一下JS中非常重要的一部分内容,关于this指向的问题

常规的this指向可以简单总结为谁引用就指向谁,找不到谁引用就指向window。

this指向中还有非常重要的一部分,就是改变this指向的方法。原生JS提供了call、apply、bind三种方式来修改this指向,三种方式各有特点,各有应用场景。

在如今的前端面试中,会用call、apply、bind已经达不到要求了,要能够知道原理并手写实现。

原生call的使用

先看一下原生call的使用方式。

function fn() {
  console.log(this.name);
}

let obj = {
  name: "微信公众号: Code程序人",
};

fn.call(obj);

我们先写了一个fn函数,输出this.name,在fn函数中并没有绑定过this.name,所以,如果不改变this指向的话,它会输出undefined
所以我们用call方法,改变一下this指向。
JS手写实现call、apply、bind(超详细)_第1张图片

可以看到改变this指向后,在fn函数中输入了它本身不存在的name
还可以添加额外的参数。

function fn(age, sex) {
  console.log(this.name, age, sex);
}
let obj = {
  name: "微信公众号: Code程序人生",
};

fn.call(obj, 22, '男');

我们想让fn函数额外接收两个参数。可以在call方法的第二个参数开始依次填写要传入的参数。

JS手写实现call、apply、bind(超详细)_第2张图片
可以看到没有问题。

手写实现call

我们知道了原生call的使用方法后,着手自己实现一下。

Function.prototype.myCall = function (ctx) {
  ctx = ctx || window;
  let fn = Symbol();
  ctx[fn] = this;
  let args = [...arguments].splice(1);
  let result = ctx[fn](...args);
  delete ctx[fn];
  return result;
};

因为我们在使用的时候,可以直接在函数的后面通过.call的方式来使用,所以我们一定要在Function原型链上绑定自己写的myCall方法。

ctx就是我们要this要指向的对象。如果ctx不存在的话,默认指向window

Symbol如果不了解的话,可以去百度一下,它可以生成一个独一无二的值,一般用于对象的属性上。

ctx[fn] = this,这句话,可以把要执行的函数绑定到ctx的属性上,之前说过this指向的基本原则是谁调用就指向谁。比如我们执行fn.call(),这个时候call函数里的this指向就是fn。这样达到一个切换this指向的作用。

要实现填入参数,要先了解一下arguments对象,这是一个Function中都存在的一个类数组的东西。它存入的是函数中的所有参数。

我们要的是从第二个参数开始的所有参数,都赋值给新函数中。

...arguments可以将一个类数组转为一个数组,...是ES6的扩展运算符,不懂的同学也可以去自行学习一下。

然后通过数组的splice方法,删掉第一个参数,第一个参数是携带this指向的对象,第二个参数开始才是传入函数需要的参数。

fn.call()在改变this指向后会默认执行这函数,所以执行函数的操作我们也要在函数中执行。

let result = ctx[fn](...args);

这段代码是执行函数的操作,并且args就我们需要传入的参数,同样通过扩展运算符...来展开。

执行完之后,其实有用的只有result,我们要把之前辅助用的ctx[fn]删掉,然后返回result即可。有些有返回值,有些没有。都默认返回。

验证一下。

function fn(age, sex) {
  console.log(this.name, age, sex);
}

let obj = {
  name: "微信公众号: Code程序人生",
};

// fn.call(obj, 22, '男');
fn.myCall(obj, 23, '男');

JS手写实现call、apply、bind(超详细)_第3张图片
通过结果来看,我们写的myCall函数与原生的call函数实现了同样的功能。

原生apply的使用

applycall最大的不同就是他们在传入参数时候,call是将多余的函数参数从call方法的第二个参数开始依次传入的。而apply是将多余的函数参数放在一个数组里,从第二个参数中统一传入。

function fn(age, sex) {
  console.log(this.name, age, sex);
}


let obj = {
  name: "微信公众号: Code程序人生",
};

// fn.call(obj, 22, '男');
// fn.myCall(obj, 23, '男');
fn.apply(obj, [24, '男']);

JS手写实现call、apply、bind(超详细)_第4张图片
剩余的功能和call一模一样。

手写实现apply

所以我们来尝试实现一下。

Function.prototype.myApply = function (ctx) {
  ctx = ctx || window;
  let fn = Symbol();
  ctx[fn] = this;
  let result;
  if (arguments[1]) {
    result = ctx[fn](...arguments[1]);
  } else {
    result = ctx[fn]();
  }
  delete ctx[fn];
  return result;
};

实现的过程也和call基本一致。只是在执行函数时,要判断一下,使用者有没有传入第二个参数,如果有的话,执行时传入...arguments[1]就可以。如果没有传入,在执行时就不传入任何东西。

尝试使用一下。

function fn(age, sex) {
  console.log(this.name, age, sex);
}

let obj = {
  name: "微信公众号: Code程序人生",
};

// fn.call(obj, 22, '男');
// fn.myCall(obj, 23, '男');
// fn.apply(obj, [24, '男']);
fn.myApply(obj, [25, '男']);

JS手写实现call、apply、bind(超详细)_第5张图片
通过结果可以看到,我们成功通过自己的方式实现了apply函数。

原生bind的使用

bindcall相比,在传入参数的方式上是一致的,bind也是将需要传入的函数参数从第二个参数开始依次传入。

但是bindcallapply不同的点是,它在执行后,会返回一个修改this指向之后的函数,要手动去执行,而callapply是自动执行。

function fn(age, sex) {
  console.log(this.name, age, sex);
}
let obj = {
  name: "微信公众号: Code程序人生",
};

// fn.call(obj, 22, '男');
// fn.myCall(obj, 23, '男');
// fn.apply(obj, [24, '男']);
// fn.myApply(obj, [25, '男']);
console.log(fn.bind(obj, 26, '男'));

我们输出一下bind之后的值。

JS手写实现call、apply、bind(超详细)_第6张图片
可以看到输出的是一个函数,所以我们需要手动执行一下。

function fn(age, sex) {
  console.log(this.name, age, sex);
}
let obj = {
  name: "微信公众号: Code程序人生",
};

// fn.call(obj, 22, '男');
// fn.myCall(obj, 23, '男');
// fn.apply(obj, [24, '男']);
// fn.myApply(obj, [25, '男']);
fn.bind(obj, 26, '男')();

JS手写实现call、apply、bind(超详细)_第7张图片

手写实现bind

知道原生bind的使用方法之后,我们尝试实现一下。

Function.prototype.myBind = function(ctx) {
    ctx = ctx || window;
    let self = this;
    let args = [...arguments].splice(1);
    let fn = function() {};
    let _fn = function() {
        return self.apply(this instanceof _fn ? this : ctx, args);
    }
    fn.prototype = this.prototype;
    _fn.prototype = new fn();
    return _fn;
}

bind的实现过程就稍微复杂了一些。我们需要在函数中,生成一个待执行且改变了this指向的函数。

因为bindcallapply最主要的区别就是前者是返回改变this指向后的函数,后者是自动执行。所以在函数中改变this指向的操作就没必要重复写了,我这里使用现有的apply来实现。

_fn函数中判断this instanceof _fn的目的是,原生bind其实是可以new那个bind后返回的函数的,不是new的情况下this指向才会是ctx。与callapply不一样的地方是,这里还需要设置一下返回的那个函数的原型,采用继承的方式,不能直接 = ,要采用一个中介函数fn来辅助改变_fn的原型。

总结

手写实现原生JS的各种方法是考察JS基本功的一种方式,我们首先要了解它的基本原理与使用方式,通过使用方式来一步步通过自己的方式尝试实现

你可能感兴趣的:(前端,面试,ES6,javascript,前端,开发语言)