都2020年了,你应该知道如何手写Call、Apply、Bind了吧

导读

做为面试中面试官最宠爱的一个问题,在这里进行一个详细的介绍,你们重点要放在理解,而不是背。 写的很差或不对的地方,请你们积极指出,好了,话很少说,咱们“圆规正转”javascript

先说一下三者的区别
共同点就是修改this指向,不一样点就是
1.call()和apply()是马上执行的, 而bind()是返回了一个函数
2.call则能够传递多个参数,第一个参数和apply同样,是用来替换的对象,后边是参数列表。
3.apply最多只能有两个参数——新this对象和一个数组argArray

复制代码

1、手写实现Call

1.call主要都作了些什么。

  • 更改this指向
  • 函数马上执行

2.简单实现

Function.prototype.myCall = function(context) {
  context.fn = this;
  context.fn();
}

const obj = {
  value: 'hdove'
}

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

fn.myCall(obj); // hdove
复制代码

3.出现的问题

  • 没法传值
  • 若是fn()有返回值的话,myCall 以后获取不到
function fn() {
  return this.value;
}

console.log(fn.myCall(obj)); // undefined

复制代码
  • call其实就是更改this指向,指向一个Object,若是用户传的是基本类型又或者干脆就不传呢?
  • myCall执行以后,obj会一直绑着fn()

4.通通解决

Function.prototype.myCall = function(context) {
  // 1.判断有没有传入要绑定的对象,没有默认是window,若是是基本类型的话经过Object()方法进行转换(解决问题3)
  var context = Object(context) || window;
  
  /** 在指向的对象obj上新建一个fn属性,值为this,也就是fn() 至关于obj变成了 { value: 'hdove', fn: function fn() { console.log(this.value); } } */
  context.fn = this;
  
  // 2.保存返回值
  let result = '';
  
  // 3.取出传递的参数 第一个参数是this, 下面是三种截取除第一个参数以外剩余参数的方法(解决问题1)
  const args = [...arguments].slice(1);
  //const args = Array.prototype.slice.call(arguments, 1);
  //const args = Array.from(arguments).slice(1);
  
  // 4.执行这个方法,并传入参数 ...是es6的语法用来展开数组
  result = context.fn(...args);
  
  //5.删除该属性(解决问题4)
  delete context.fn;
  
  //6.返回 (解决问题2)
  return result;
}


const obj = {
  value: 'hdove'
}

function fn(name, age) {
  return  {
      value: this.value,
      name,
      age
  }
}

fn.myCall(obj, 'LJ', 25); // {value: "hdove", name: "LJ", age: 25}

复制代码

2、手动实现Apply

实现了call其实也就间接实现了apply,只不过就是传递的参数不一样

Function.prototype.myApply = function(context, args) {
  var context = Object(context) || window;
  
  context.fn = this;
  
  let result = '';
  
  //4. 判断有没有传入args
  if(!args) {
    result = context.fn();
  }else {
    result = context.fn(...args);
  }
  
  delete context.fn;
  
  return result;
}


const obj = {
  value: 'hdove'
}

function fn(name, age) {
  return  {
      value: this.value,
      name,
      age
  }
}

fn.myApply(obj, ['LJ', 25]); // {value: "hdove", name: "LJ", age: 25}

复制代码

3、实现Bind

bind() 方法建立一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其他参数将做为新函数的参数,供调用时使用(MDN)

复制代码

1.bind特性

  • 指定this
  • 返回一个函数
  • 传递参数并柯里化

2.简单实现

Function.prototype.myBind = function(context) {
    const self = this;
    
    return function() {
        self.apply(context);
    }
}

const obj = {
  value: 'hdove'
}

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

var bindFn = fn.myBind(obj);

bindFn(); // 'hdove;


复制代码

3.优化

相比于call、apply,我我的以为bind的实现逻辑更加复杂,须要考虑的东西不少,在这里分开进行优化。java

3.1 调用bind是个啥玩意?

在这里咱们须要进行一下判断,判断调用bind的是否是一个函数,不是的话就要抛出错误。es6

Function.prototype.myBind = function(context) {

    if (typeof this !== "function") {
        throw new Error("不是一个函数");
    }
    const self = this;
    
    return function() {
        self.apply(context);
    }
}

复制代码

3.2 传递参数

咱们看下面这段代码web

Function.prototype.myBind = function(context) {

    if (typeof this !== "function") {
        throw new Error("不是一个函数");
    }
    const self = this;
    
    return function() {
        self.apply(context);
    }
}

const obj = {
  value: 'hdove'
}

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

var bindFn = fn.myBind(obj, LJ, 25);

bindFn(); // 'hdove' undefined undefined


复制代码

很明显,第一个优化的地方就是传递参数,咱们来改造下面试

Function.prototype.myBind = function(context) {

    if (typeof this !== "function") {
        throw new Error("不是一个函数");
    }
    
    const self = this;
    
    // 第一个参数是this,截取掉
    const args = [...arguments].slice(1);
    
    return function() {
        /**
            这里咱们其实便可以使用apply又可使用call来更改this的指向
            使用apply的目的其实就是由于args是一个数组,更符合apply的条件
        */
        return self.apply(context, args);
    }
}

const obj = {
  value: 'hdove'
}

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

var bindFn = fn.myBind(obj, 'LJ', 25);

bindFn(); // 'hdove' 'LJ' 25
 
复制代码

想在看起来没什么问题,可是咱们这样传一下参数数组

var bindFn = fn.myBind(obj, 'LJ');

bindFn(25); // 'hdove' 'LJ' undefined

复制代码

咱们发现后面传递的参数丢了,这里就须要使用柯里化来解决这个问题bash

Function.prototype.myBind = function(context) {

    if (typeof this !== "function") {
        throw new Error("不是一个函数");
    }
    
    const self = this;
    
    // 第一个参数是this,截取掉
    const args1 = [...arguments].slice(1);
    
    return function() {
        // 获取调用时传入的参数
        const args2 = [...arguments];
        return self.apply(context, args1.concat(args2));
    }
}

const obj = {
  value: 'hdove'
}

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

var bindFn = fn.myBind(obj, 'LJ');

bindFn(25); // 'hdove' 'LJ' 25

复制代码

3.3this丢失

其实bind还具备一个特性就是 做为构造函数使用的绑定函数,意思就是这个绑定函数能够当成构造函数使用,能够调用new操做符去建立一个实例,当咱们使用new操做符以后,this其实不是指向咱们指定的对象,而是指向new出来的这个实例的构造函数,不过提供的参数列表仍然会插入到构造函数调用时的参数列表以前。咱们简单实现一下。app

Function.prototype.myBind = function(context) {
    if (typeof this !== "function") {
        throw new Error("不是一个函数");
    }
    const self = this;
    const args1 = [...arguments].slice(1);
    
    const bindFn = function() {
        const args2 = [...arguments];
        
        /**
            这里咱们经过打印this,咱们能够看出来。
            当这个绑定函数被当作普通函数调用的时候,this实际上是指向window。
            而当作构造函数使用的时候,倒是指向这个实例,因此this instanceof bindFn为true,这个实例能够获取到fn()里面的值。
            
            咱们能够再fn里面添加一个属性test.
            若是按照以前的写法 打印出来的是undefined,正好验证了咱们上面所说的this指向的问题。
            因此解决方法就是添加验证,判断当前this
            若是 this instanceof bindFn 说明这是new出来的实例,指向这个实例, 不然指向context
        */
        console.log(this);
        
        return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
    }
    
    return bindFn;
}

const obj = {
  value: 'hdove'
}

function fn(name, age) {
    this.test = '我是测试数据';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFn = fn.myBind(obj, 'LJ');

var newBind = new bindFn(25);

console.log(newBind.test); // undefined

复制代码

3.4绑定原型

咱们都知道每个构造函数,都会有一个原型对象(prototype),来添加额外的属性。函数

function fn(name, age) {
    this.test = '我是测试数据';
}

fn.prototype.pro = '原型数据';

var bindFn = fn.myBind(obj, 'LJ', 25);

var newBind = new bindFn();

console.log(bindObj.pro); // undefined

复制代码

由于咱们没有绑定原型,因此会出现undefined,咱们简单绑定一下post

Function.prototype.myBind = function(context) {
    if (typeof this !== "function") {
        throw new Error("不是一个函数");
    }
    const self = this;
    const args1 = [...arguments].slice(1);
    
    const bindFn = function() {
        const args2 = [...arguments];
        return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
    }
    
    // 绑定原型
    bindFn.prototype = self.prototype;
    
    return bindFn;
}

function fn(name, age) {
    this.test = '我是测试数据';
}

fn.prototype.pro = '原型数据';

var bindFn = fn.myBind(obj, 'LJ', 25);

var newBind = new bindFn();

console.log(bindObj.pro); // "原型数据"

复制代码

可是这样会出现这样一个问题

function fn(name, age) {
    this.test = '我是测试数据';
}

fn.prototype.pro = '原型数据';

var bindFn = fn.myBind(obj, 'LJ');

var bindObj = new bindFn();

bindObj.__proto__.pro = '篡改原型数据';

console.log(bindObj.__proto__ === fn.prototype); // true

console.log(bindObj.pro); // "篡改原型数据"

console.log(fn.prototype.pro); // "篡改原型数据"

当咱们修改bindObj的原型的时候,fn的原型也一块儿修改了
这实际上是由于 bindObj.__proto__ === fn.prototype
咱们在修改bindObj的同时也间接修改了fn

复制代码

解决方法其实很简单,建立一个新方法proFn(),来进行原型绑定,也就是实现继承的几种方式中的原型式继承,而后咱们把这个新方法的实例对象绑定到咱们的绑定函数的原型中

Function.prototype.myBind = function(context) {
    if (typeof this !== "function") {
        throw new Error("不是一个函数");
    }
    const self = this;
    const args1 = [...arguments].slice(1);
    
    const bindFn = function() {
      const args2 = [...arguments];
       
        return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
    }
    
    // 绑定原型
    
    function proFn() {}  //建立新方法
    proFn.prototype = self.prototype; //继承原型
    bindFn.prototype = new proFn(); //绑定原型
    
    return bindFn;
}

function fn(name, age) {
    this.test = '我是测试数据';
}

fn.prototype.pro = '原型数据';

var bindFn = fn.myBind(obj, 'LJ', 25);

var newBind = new bindFn();

console.log(bindObj.__proto__ === fn.prototype); // false

console.log(bindObj.pro); // "篡改原型数据"

console.log(fn.prototype.pro); // "原型数据"

复制代码

4、面试

这些东西实际上是面试中比较容易考到的问题,你们不要想着去背,背下来实际上是没什么用处的,容易会被问倒,重点仍是在于理解,理解了也就能够垂手可得的写出来了。但愿这篇文章会给你们带来收获,那怕是一点点。在这里,提早给你们伙拜个早年,鼠年幸运,跳槽顺利,涨薪顺利,哈哈。

5.推荐阅读

相关文章
相关标签/搜索