JavaScript手撕bind方法?

首先,它是函数的一个方法,咱们须要将其--

1.挂载到Function的原型链上

Function.prototype.mybind =...
//这样,全部继承自Function的函数就可以使用.操做符来访问mybind了!
//PS:由于JS原型式继承
而后,让咱们先看看原生JS的bind方法有哪些行为--

2.调用函数时改变this指向

让调用该方法的函数的this指向传入的第一个参数

咱们能够借助apply方法实现javascript

Function.prototype.mybind = function (context) {
  this.apply(context);
};
let obj = {
  name: "Crushdada",
};
let fn = function (params) {
  console.log(this.name);
};
fn.mybind(obj); //Crushdada

3.返回一个匿名的绑定函数

注意两点:
  • 因为咱们返回了一个绑定函数(匿名函数),则在调用时须要在调用语句后面再加一个圆括号
  • 与此同时,因为匿名函数中的this指向window/global,咱们须要使用箭头函数或者手动保存一下指向mybind中指向调用者fn的thisjava

    • 此处使用箭头函数
    Function.prototype.mybind = function (context) {
      return () => this.apply(context);
    };
    let obj = {
      name: "Crushdada",
    };
    let fn = function (params) {
      console.log(this.name);
    };
    fn.mybind(obj)(); //Crushdada

    4.支持柯里化传递参数

我的理解:相比“容许传入参数”这种说法,形容为“传递参数”更贴切,bind方法做为一个中间方法,会代收参数后再传递给它返回的匿名绑定函数,其返回一个匿名函数这一点,自然支持柯里化(多是ES6引入它的初衷之一),由于这样就容许咱们在调用bind时传入一部分参数,在调用其绑定函数时再传入剩下的参数。而后它会在接收完第二次传参后再apply执行调用bind的那个方法segmentfault

  • 实现柯里化的逻辑很简单,仅仅须要在mybind中接收一次参数,而后在绑定函数中接收一次参数,并将两者拼接后一块儿传给mybind的调用方法使用便可数组

    下面,实现传参&柯里化!
  • 若使用的是普通函数,要处理参数,因为arguments为类数组,slice为Array方法,故先在原型链上调用而后call一下app

    • 第一个参数为this新的指向,不是属性,故slice掉它
    使用箭头函数能极大简化代码
    下面咱们改亿点点细节!
  • 使用箭头函数(Array Function)没有arguments属性,所以使用rest运算符替代处理
  • 在拼接args和bindArgs时使用扩展运算符替代concat
  • 不得不说ES6引入的rest运算符、扩展运算符在处理参数这一点上提供了极大的便利函数

    Function.prototype.mybind = function (context, ...args) {
      return (...bindArgs) => {
      //拼接柯里化的两次传参
        let all_args = [...args, ...bindArgs]; 
      //执行调用bind方法的那个函数
        let call_fn = this.apply(context, all_args); 
        return call_fn;
      };
    };
    let person = {
      name: "Crushdada",
    };
    let getInfo = function (like, fav) {
      let info = `${this.name} likes ${like},but his favorite is ${fav}`;
      return info;
    };
    //anonymous_bind:mybind返回的那个匿名的绑定函数
    let anonymous_bind = getInfo.mybind(person, "南瓜子豆腐");
    let info = anonymous_bind("皂角仁甜菜"); //执行绑定函数
    console.log(info);
    //Crushdada likes 南瓜子豆腐,but his favorite is 皂角仁甜菜

    箭头函数不能做为构造函数!

须要用普通函数重写mybind

写到支持柯里化这一步,bind方法仍是可使用箭头函数实现的,并且比普通函数更加简洁post

可是想要继续完善它的的行为,就不能用继续用Arrow Function了,由于箭头函数不能被new!,要是尝试去new它会报错:性能

anonymous_bind is not a constructor
笔者也是写到这才想起箭头函数这个机制的。那么下面咱们须要用普通函数重写mybind
不过也很简单,只须要手动保存一下this便可。就再也不贴出改动后的代码了。直接看下一步

5.支持new绑定函数

bind的一个隐式行为:ui

  • 它返回的绑定函数容许被new 关键字调用,可是,实际被做为构造器的是调用bind的那个函数!!!
  • 且new调用时传入的参数照常被传递给调用函数。this

    逻辑

实现这一步的逻辑也较为简单,咱们类比一下和通常调用new时的区别--

  • new一个普通函数:按理来讲生成的实例对象的构造函数是那个普通函数
  • new一个绑定函数:生成的实例对象的构造函数调用bind的那个函数

主要须要咱们写的逻辑有:

  1. 判断是不是new调用
  2. getInfo函数中的this指向--new中建立的实例对象obj

    1. 就是把getInfo函数里的this换成obj,以使obj获取到其中的属性
    2. 能够借助apply方法
  3. 判断getInfo函数是否返回一个对象,如果,则返回该对象,不然返回new生成的obj

    至于为何这么写,就须要你先弄懂new关键字实现的机制了,个人笔记连接附在文末
    下面,实现它!
    Function.prototype.mybind= function (context, ...args) {
      let self = this;
      return function (...bindArgs) {
        //拼接柯里化的两次传参
        let all_args = [...args, ...bindArgs];
        // new.target 用来检测是不是被 new 调用
        if (new.target !== undefined) {
          // 让调用mybind的那个函数的this指向new中建立的空对象
          var result = self.apply(this, all_args);
          // 判断调用mybind方法的那个实际的构造函数是否返回对象,没有返回对象就返回new生成的实例对象obj
          return result instanceof Object ? result : this;
        }
        //若是不是 new 就原来的逻辑
        //执行调用bind方法的那个函数
        let call_fn = self.apply(context, all_args);
        return call_fn;
      };
    };
    let person = {
      name: "Crushdada",
    };
    let getInfo = function (like, fav) {
      this.dear = "Bravetata";
      let info = `${this.name} likes ${like},but his favorite is ${fav}`;
      return info;
    };
    //anonymous_bind:mybind返回的那个匿名的绑定函数
    let anonymous_bind = getInfo.mybind(person, "南瓜子豆腐");
    let obj = new anonymous_bind("皂角仁甜菜"); //执行绑定函数
    console.log(obj);       //{ dear: 'Bravetata' }
    console.log(obj.name);  //undefined
    解释一下以上代码:

第一个逻辑

  • new内部有相似这样一条语句:Con.apply(obj, args)
  • 其中Con是new 的那个构造函数,obj是最后要返回的实例对象
  • 当咱们new上面mybind中return的那个绑定函数时
  • Con就是该绑定函数
  • 当Con.apply(obj, args)执行,
  • 调用绑定函数并将其中的this换成obj
  • 而后程序就进入到了该绑定函数中--
  • 判断确实是new调用的
  • 执行self.apply(this, all_args);
  • 这条语句就至关于getInfo.apply(obj, all_args)
  • 这样就达成咱们的目的了!--让getInfo成为new生成的实例对象的实际构造器

第二个逻辑

  • new关键字会判断构造函数自己会不会返回一个对象
  • 若是会,则直接返回这个对象当作实例,不然正常是返回那个new生成的obj当作实例对象
  • 那么--咱们在第一个逻辑里已经调用了实际构造器--getInfo
  • 接下来咱们直接判断一下调用的结果,即它是否return一个对象,而后return给new作最终的return便可

    此外:能够看到,当new mybind返回的绑定函数时,obj没有获取到person.name属性,为undefined。也就是说--

    此时,bind改变this指向的行为会失效

看个栗子,这样清楚一点
var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的this 失效了,

这是为何呢?

若是你们了解 new 的模拟实现,就会知道了--

new是JS模拟面向对象的一个关键字,它的目的之一是实现继承,它要去继承构造函数(类)之中的属性,那么new关键字是怎样去实现的呢?它在内部应用了相似这样一条语句:

Con.apply(obj, args) //Con是new 的那个构造函数

new 关键字会先声明一个空对象obj,而后将构造函数的this指向这个对象

这样作会发生什么--
  • 若是构造函数中设置了一些属性,如:this.name = xx;
  • 那么就至关于将this换成了obj,变成:obj.name = xx;
  • obj就继承到了构造函数的属性!!
  • obj就是最后会返回的实例对象

详见:《JS中new操做符作了什么?》--Crushdada's Notes

让咱们回到为何this会失效这一问题上
了解完new关键字的相关实现,咱们已经获得答案了--

new完绑定函数后,绑定函数内部的this 已经指向了 obj,而obj中没有value这个属性,固然就返回undefined了

6.支持原型链继承

实际上这一步是对绑定函数内重写new方法的一个补充--

由于new方法原本就支持原型链继承

逻辑

那么咱们只须要--

让new的实例对象obj的原型指向实际构造器getInfo的prototype便可

Object.setPrototypeOf(this, self.prototype);
规范化/严谨性

能够为mybind方法加上一个判断,调用者必须是一个函数,不然抛出TypeError--

if (typeof this !== 'function' || Object.prototype.toString.call(this) !== '[object Function]') {
    throw new TypeError(this + ' must be a function');
  }
一个疑问?

咱们模拟实现bind方法,终归是经过apply实现的。而它源码是如何实现的,对于我来讲就像一个黑盒。也就是说:不用apply,它是如何实现?

7.究极版本--不借助apply实现

百度的各类版本大都借助apply实现的,不过很幸运在思否找到了答案--JS bind方法如何实现?

答者给出的替代apply的方法很简单:

  • 调用bind方法的那个函数--即要改变this指向的那个函数:caller
  • 要让caller的this指向:context

那么咱们只须要--

  • 将caller做为一个对象方法挂载到context上:context.callerFn = caller

    • 上面那句代码中,属性名"callerFn"是自定义的

这样,当执行该句时,至关于context调用了caller函数,那么caller函数中的this天然就指向其调用者context了。

以上,就替代了apply在本例中的核心功能--调用函数同时改变this指向

此外,为提升代码性能,用完callerFn后就删掉它

context.__INTERNAL_SECRETS = func
  try {
  return context.__INTERNAL_SECRETS(...args)
} finally {
  delete context.__INTERNAL_SECRETS
}
将apply替换为以上代码,就获得最终版了

最终版

FFunction.prototype.mybind = function (context, ...args) {
  if (
    typeof this !== "function" ||
    Object.prototype.toString.call(this) !== "[object Function]"
  ) {
    throw new TypeError(this + " must be a function");
  }
  let self = this; //这里的this和self即:调用mybind的方法--fn()
  context.caller2 = self;
  return function (...bindArgs) {
    let all_args = [...args, ...bindArgs];
    //new调用时,this被换成new方法最后要返回的实例对象obj
    if (new.target !== undefined) {
      try {
        this.caller = self;
        var result = this.caller(...all_args);
      } finally {
        delete this.caller;
      }
      Object.setPrototypeOf(this, self.prototype);
      return result instanceof Object ? result : this;
    }
    //当不是new调用时,this指向global/window(由于匿名函数返回后由全局调用)
    try {
      var final_res = context.caller2(...all_args);
    } finally {
      delete context.caller2;
    }
    return final_res; //调用mybind的那个函数[可能]有返回
  };
};

其余知识点:

Array.prototype.slice.call()

  • 接收一个字符串或有length属性对象
  • 该方法可以将有length属性对象或字符串转换为数组

    所以像是arguments对象这样拥有length属性的类数组就可使用该方法转换为真正的数组
    JS中,只有String和Array拥有.slice方法,对象没有。
    let slice = (arrlike) => Array.prototype.slice.call(arrlike);
    var b = "123456";
    let arr = slice(b);
    console.log(arr);
    // ["1", "2", "3", "4", "5", "6"]

    arr.slice()方法

  • 返回一个新的数组对象,这一对象是一个由beginend决定的原数组的浅拷贝包括begin不包括end)。

    • 接收的参数--begin、end是数组index
    • 原始数组不会被改变。
    const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
    console.log(animals.slice(2));
    // expected output: Array ["camel", "duck", "elephant"]
    console.log(animals.slice(2, 4));
    // expected output: Array ["camel", "duck"]

    逻辑或“||”

a || b :

  • 若运算符前面的值为false,则返回后面的值
  • 若true,返回前面的值

    js中逻辑值为false的6种状况

当一个函数拥有形参,但调用时没有传实参时,形参是undefined,会被按false处理
function name(params) {
  console.log(params); //undefined
}
name();
console.log(undefined == false);  //false
console.log(undefined || "undefined was reated as false");
//undefined was reated as false
会被逻辑或运算符当作false处理的总共6个--

0、null、""、false、undefined 或者 NaN

参考:

JavaScript深刻之bind的模拟实现--掘金

js 实现 bind 的这五层,你在第几层?--蓝色的秋风 | 思否

《JS中new操做符作了什么?》--Crushdada's Notes