不一样的bind的实现,不来了解一下吗?

123 阅读8分钟

前言

bind的实现,涉及到javascript的很多方面的知识,也是前端面试常考的手写基础题,如果能完全掌握,可以帮你你更深的理解这门语言,闲言少叙,我们开始进入正题。(阅读这个章节之前,你需要提前掌握的知识点:原型链,this指向,bind、apply用法。)

1.改变this指向

bind最常用的方式是改变this指向,比如在setTimeout中调用函数默认会指向window(默认绑定),obj.fn this会指向obj(隐式绑定), bind给我们提供了一种方式去实现调用函数时改变默认this的方式,这种方式也被称之为显示绑定。

var name = "de1ck";
var person = {
  name: "juejin",
};
function getName() {
  console.log(this.name);
}
getName(); // de1ck 在全局执行环境中this指向全局对象。
var bindName = getName.bind(person);
bindName(); // juejin

根据这个特点,我们实现一下第一版的bind:

Function.prototype.bind = function (bindThis) {
  var self = this; // 这里的self对应的就是getName(可以考虑一下为什么需要在这里存放当前作用域下的this,而不是在返回函数内部的)
  // bind函数的结果是返回一个待执行的函数
  return function () {
    // 这里的函数在全局环境中执行时,默认指向全局对象
    return self.apply(bindThis); // 这里通过函数原型链上的apply方法对getName函数指定this执行
  };
};

2.偏函数

bind 的另一个最简单的用法是使一个函数拥有预设的初始参数。

var name = "de1ck";
var human = {
  name: "juejin",
};
function person(age, job, gender) {
  console.log(this.name, age, job, gender);
}
person(25, "engineer", "male");
// de1ck 25 engineer male
var bindHuman = person.bind(human, 25, "engineer");
bindHuman("male");
// juejin 25 engineer male

基于此很容易得到一下实现:

Function.prototype.bind = function (bindThis) {
  var self = this; // 这里的self对应的就是getName
  var partArgs = [].slice.call(arguments, 1); // 获取方法的预设参数
  // bind函数的结果是返回一个待执行的函数
  return function () {
    // 参数合并
    var args = [].concat(partArgs, [].slice.call(arguments));
    // 这里的函数在全局环境中执行时,默认指向全局对象
    return self.apply(bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
  };
};

3.作为构造函数使用的绑定函数

当绑定后函数遇到构造函数时,this会指向构造函数实例,我们可以验证一下

var name = "de1ck";
var human = {
  name: "juejin",
};
// 定义一个变量
var bindThis;
function person(age, job, gender) {
  // 把this赋值给这个变量,以便后续验证
  bindThis = this;
  console.log(this.name, age, job, gender);
}
person(25, "engineer", "male"); // de1ck 25 engineer male
var bindHuman = person.bind(human, 25, "engineer");
// new
var newHuman = new bindHuman("male"); // // undefined 25 'engineer' 'male'
console.log(bindThis === human, bindThis === newHuman); // false true

从输出结果可以看出,使用构造函数会覆盖默认bind的this,在第1章节中介绍了隐式绑定, 这里new调用会绑定到新创建的对象。(具体可以查阅 你不知道的JavaScript中关于this的讲解)

所以,这里可以得到第三版的实现

Function.prototype.bind = function (bindThis) {
  var self = this; // 这里的self对应的就是getName
  var partArgs = [].slice.call(arguments, 1); // 获取方法的预设参数
  // bind函数的结果是返回一个待执行的函数
  var boundFunction = function () {
    // 参数合并
    var args = [].concat(partArgs, [].slice.call(arguments));
    // // 通过instanceof 判断该函数是否是通过new调用
    return self.apply(this instanceof boundFunction ? this : bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
  };
  return boundFunction;
};

4.继承函数的原型

作为构造函数调用时,实例还会继承函数的原型。

var name = "de1ck";
var human = {
  name: "juejin",
};
var bindThis;
function person(age, job, gender) {
  bindThis = this;
  this.age = age;
  console.log(this.name, age, job, gender);
}
person.prototype.getAge = function () {
  console.log(this.age);
};
person(25, "engineer", "male"); // de1ck 25 engineer male
var bindHuman = person.bind(human, 25, "engineer");
// new
var newHuman = new bindHuman("male"); // // undefined 25 'engineer' 'male'
console.log(bindThis === human, bindThis === newHuman); // false true
// Expected output 25
newHuman.getAge(); // Uncaught TypeError: newHuman.getAge is not a function

但上一版的实现中并没有做到原型的继承,我们可以通过修改返回函数的原型达到这个目的:

Function.prototype.bind = function (bindThis) {
  var self = this; // 这里的self对应的就是getName(可以考虑一下为什么需要在这里存放当前作用域下的this,而不是在返回函数内部的)
  var partArgs = [].slice.call(arguments, 1); // 获取方法的预设参数
  // bind函数的结果是返回一个待执行的函数
  var boundFunction = function () {
    // 参数合并
    var args = [].concat(partArgs, [].slice.call(arguments));
    // 通过instanceof 判断该函数是否是通过new调用
    return self.apply(this instanceof boundFunction ? this : bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
  };
  // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承函数的原型中的值
  boundFunction.prototype = self.prototype;
  return boundFunction;
};

这个结果就是准确的吗?我们改一下代码,来测试chrome中prototype的原型是怎么继承的,来验证一下最终的结果。

var name = "de1ck";
var human = {
  name: "juejin",
};
var bindThis;
function person(age, job, gender) {
  bindThis = this;
  this.age = age;
  console.log(this.name, age, job, gender);
}
person(25, "engineer", "male"); // de1ck 25 engineer male
var bindHuman = person.bind(human, 25, "engineer");
// new
var newHuman = new bindHuman("male"); // undefined 25 'engineer' 'male'
// 主要下面输出结果 Expected output:true true false
console.log(
  bindHuman.prototype === person.prototype,
  newHuman.__proto__ === person.prototype,
  bindHuman.constructor === Function
); // false true true

在浏览器控制台中并没有输出我们预想中的结果,所以我们可以发现原型链并没有直接传递给bind返回的函数,而是传递给了new之后的对象,那么怎么给对象设置他的原型呢,我们可以使用Object.setPrototypeOf,由此我们可以得到最终的实现:

Function.prototype.bind = function (bindThis) {
  var self = this; // 这里的self对应的就是getName(可以考虑一下为什么需要在这里存放当前作用域下的this,而不是在返回函数内部的)
  var partArgs = [].slice.call(arguments, 1); // 获取方法的预设参数
  // bind函数的结果是返回一个待执行的函数
  var boundFunction = function () {
    // 参数合并
    var args = [].concat(partArgs, [].slice.call(arguments));
    let isInstanceof = false;
    if (this instanceof boundFunction) {
      // 通过setPrototypeOf设置对象的原型
      Object.setPrototypeOf(this, self.prototype);
      // 通过 setPrototypeOf 后,this的构造函数变为self了,所以这里需要一个变量来说明是否是new调用
      isInstanceof = true;
    }
    // 通过 isInstanceof 判断该函数是否是通过new调用 (this instanceof boundFunction 的结果为 false)
    return self.apply(isInstanceof ? this : bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
  };
  return boundFunction;
};

5.快捷调用

在你想要为一个需要特定的 this 值的函数创建一个捷径(shortcut)的时候,bind() 也很好用。

你可以用 Array.prototype.slice 来将一个类似于数组的对象(array-like object)转换成一个真正的数组,就拿它来举例子吧。你可以简单地这样写:

var slice = Array.prototype.slice;


// ...


slice.apply(arguments);

用 bind()可以使这个过程变得简单。在下面这段代码里面,slice 是 Function.prototypeapply() 方法的绑定函数,并且将 Array.prototype 的 slice() 方法作为 this 的值。这意味着我们压根儿用不着上面那个 apply()调用了。

// 与前一段代码的 "slice" 效果相同
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);


// ...


slice(arguments);

core-js中很多工具函数使用上述bind来实现各种炫酷的功能,上述代码还有更优雅的方式实现:

var FunctionPrototype = Function.prototype;
var call = FunctionPrototype.call;
var uncurryThisWithBind = FunctionPrototype.bind.bind(call, call);


var slice = uncurryThisWithBind([].slice);
slice({ length: 3 });

我们怎么理解这种调用方式呢?可以把这种方式看做组合函数,是不是更容易理解了呢。

最后,你可以使用这种方式来优化bind的实现吗?

我们知道bind的调用的时候会验证this是否为函数类型,什么情况下能使用到呢?当使用快捷调用的时候很容易触发错误:

// ...
// Expected output: Uncaught TypeError: Bind must be called on a function
Function.prototype.bind.call({});
//

所以这里我们需要新增参数检测:

var FunctionPrototype = Function.prototype;
var call = FunctionPrototype.call;
var uncurryThisWithBind = FunctionPrototype.bind.bind(call, call);
var slice = uncurryThisWithBind([].slice);
var concat = uncurryThisWithBind([].concat);
Function.prototype.bind = function (bindThis) {
  // 新增参数检测
  if (typeof this !== "function") {
    throw TypeError("Bind must be called on a function");
  }
  var self = this; // 这里的self对应的就是getName(可以考虑一下为什么需要在这里存放当前作用域下的this,而不是在返回函数内部的)
  var partArgs = slice(arguments, 1); // 获取方法的预设参数 // bind函数的结果是返回一个待执行的函数
  var boundFunction = function () {
    // 参数合并
    var args = concat(partArgs, slice(arguments));
    let isInstanceof = false;
    if (this instanceof boundFunction) {
      // 通过setPrototypeOf设置对象的原型
      Object.setPrototypeOf(this, self.prototype); // 通过 setPrototypeOf 后,this的构造函数变为self了,所以这里需要一个变量来说明是否是new调用
      isInstanceof = true;
    } // 通过 isInstanceof 判断该函数是否是通过new调用 (this instanceof boundFunction 的结果为 false)
    return self.apply(isInstanceof ? this : bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
  };
  return boundFunction;
};

结尾

bind的实现就介绍到这里了,希望对你了解bind有帮助,书写不容易,觉得有帮助,可以点赞收藏。其实bind实现原理涉及到很多知识点,需要提前去复习相关知识点,才能理解其中的原理。

比如原型链,this绑定优先级,new构造函数,函数继承等,这些功能的组合才造就bind函数。基于bind,我们可以实现函数柯里化,偏函数等功能。

参考文献:

core-js

你不知道的JavaScript

juejin.cn/post/715800…

bind mdn