关于javascript:JavaScript手撕bind方法

36次阅读

共计 6734 个字符,预计需要花费 17 分钟才能阅读完成。

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

1. 挂载到 Function 的原型链上

Function.prototype.mybind =...
// 这样,所有继承自 Function 的函数就可能应用. 操作符来拜访 mybind 了!//PS:因为 JS 原型式继承

而后,让咱们先看看原生 JS 的 bind 办法有哪些行为 –

2. 调用函数时扭转 this 指向

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

咱们能够 借助 apply办法实现

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 的this

    • 此处应用箭头函数
    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 的那个办法

  • 实现柯里化的逻辑很简略,仅仅须要在 mybind 中接管一次参数,而后在绑定函数中接管一次参数,并将二者拼接后一起传给 mybind 的调用办法应用即可

    上面,实现传参 & 柯里化!

  • 若应用的是一般函数,要解决参数,因为 arguments 为类数组,slice 为 Array 办法,故先在原型链上调用而后 call 一下

    • 第一个参数为 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 办法还是能够应用箭头函数实现的,而且比一般函数更加简洁

然而想要持续欠缺它的的行为,就不能用持续用 Arrow Function 了,因为箭头函数不能被 new!,要是尝试去 new 它会报错:

anonymous_bind is not a constructor

笔者也是写到这才想起箭头函数这个机制的。那么上面咱们须要用一般函数重写 mybind
不过也很简略,只须要手动保留一下 this 即可。就不再贴出改变后的代码了。间接看下一步

5. 反对 new 绑定函数

bind 的一个隐式行为:

  • 它返回的绑定函数容许被 new 关键字调用 ,然而, 理论被作为结构器的是调用 bind 的那个函数!!!
  • 且 new 调用时传入的参数照常 被传递给调用函数。

    逻辑

实现这一步的逻辑也较为简单,咱们类比一下和个别调用 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');
  }

最终后果

Function.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;
  return function (...bindArgs) {let all_args = [...args, ...bindArgs];
    if (new.target !== undefined) {var result = self.apply(this, all_args);
      Object.setPrototypeOf(this, self.prototype);
      return result instanceof Object ? result : this;
    }
    let call_fn = self.apply(context, all_args);
    return call_fn;
  };
};

其余知识点:

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

正文完
 0