深入 JS 之造 bind 轮子

发布于 2022-10-02 01:14:19 字数 5714 浏览 98 评论 0

Function.prototype.bind 方法的定义,MDN上如是说:

bind() 方法创建一个新的函数,在调用时设置this关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项

官方文字性的语句,没用过不容易理解,根据我的理解和使用经历,简单提要下:

  1. 创建一个新函数。
  2. 可以传参。
  3. 新函数被调用时,其内的 this 指向 bind() 的第一个参数。
  4. 可以柯里化。
  5. 当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。换句话说,new可以使bind绑定的this失效。

接下来根据这些提要,一步一步仿写出一个bind来。

第一步

现在我们可以利用 call 来绑定 this ,并且 return 一个新函数,所以:

// 第一版代码
Function.prototype.mybind = function (obj) {
  var self = this;
  return function () {
    return self.apply(obj)
  }
}

// 例子
const obj = {
  value: 1
}

function bar () {
  console.log(this.value);
  return this.value;
}

const newBar = bar.bind(obj);
const newBar2 = bar.mybind(obj);

console.log(newBar());
console.log('*******华丽的分界线*******');
console.log(newBar2());

输出是:

1
1
*******华丽的分界线*******
1
1

第一步我们实现了摘要的第1、3点和不完整的第2点。

接下来第二步我们实现 柯里化 和 完整的第2点 。

第二步

利用 模拟 bind 方法的 arguments 与 返回新函数的 arguments 参数拼接成一个数组传入 aplly() 来实现。

// Second Codes
Function.prototype.mybind = function (obj) {
  var self = this;
  var args = Array.prototype.slice.call(arguments, 1); // 切出从第一位开始到最后的函数参数数组args
  return function () {
    var newArgs = Array.prototype.slice.call(arguments);
    return self.apply(obj, args.concat(newArgs));
  }
}

// Example
const obj = {
  value: 1
}

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

const newBar = bar.bind(obj, 'Sadhu');
const newBar2 = bar.mybind(obj, 'Sadhu');

newBar(17);
console.log('*******华丽的分界线*******');
newBar2(17);

输出:

1
Sadhu
17
*******华丽的分界线*******
1
Sadhu
17

第三步

实现摘要的第5点,new 可以使 bind 绑定的 this 失效。具体什么意思呢?

举个例子:

const obj = {
  value: 1
}

const value = 2;

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

bar.prototype.habit = 'coding'; // 划重点

const newBar = bar.bind(obj, 'sadhu');

const newObj = new newBar(17);
console.log('*******华丽的分界线*******');
console.log(newObj.name);
console.log(newObj.age);
console.log(newObj.habit);
// undefined 
//*******华丽的分界线*******
// sadhu
// 17
// coding

此处全局和obj中都有 value 。但是 new 调用 newBar() 执行

console.log(this.value);

时依然返回 undefined。说明bind绑定的this失效了。其实此时这里的 this 已经指向了 newObj 实例,这里可以看造 new 轮子的文章。

还有个需要关注的要实现一点,我上述代码中 划重点 了。根据输出的情况来看,意味着:

意味着绑定函数 bar 的 prototype 属性,等于 newObj.__proto__ 等于 newBar.prototype

好,搞清楚了需求,接下来来实现。

// codes
Function.prototype.mybind = function (obj) {
  var self = this;
  var args = Array.prototype.slice.call(arguments, 1);
  
  var fBound = function () {
    var newArgs = Array.prototype.slice.call(arguments);
    // 当fBound为构造函数时,它的this指向实例。 instanceof判断为true。
    // 当fBound为普通函数时,它的this默认指向winodw。 instanceof判断为false。
    return self.apply(this instanceof fBound ? this : obj, args.concat(newArgs));
  }
  // 这样返回函数fBound的实例就可以访问到绑定函数的prototype属性上的值。
  fBound.prototype = this.prototype;
  return fBound;
}

测试:

// Example
const value = 2;

const obj = {
  value: 1
}

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

bar.prototype.habit = 'coding'; // 划重点

const newBar = bar.bind(obj, 'sadhu');
const newBar2 = bar.mybind(obj, 'sadhu')

const newObj2 = new newBar2(17);
console.log('*******华丽的分界线*******');
console.log(newObj2.name);
console.log(newObj2.age);
console.log(newObj2.habit);
// undefined 
//*******华丽的分界线*******
// sadhu
// 17
// coding

根据输出结果看,目前为止的模拟,输出是正确的。

这样就完了吗?有没有发现代码中哪里有点不对劲?

提示:fBound.prototype = this.prototype;

接下来我们进行优化。

轮子代码优化

在第三步的写法中,我们使用了 fBound.prototype = this.prototype,那么我们直接修改 fBound.prototype 的时候也会直接修改了绑定函数的 prototype 。

此时我们可以通过一个空函数来进行中转。

Function.prototype.mybind = function (obj) {
  var self = this;
  var args = Array.prototype.slice.call(arguments, 1);
  var fNOP = function() {};

  var fBound = function () {
    var newArgs = Array.prototype.slice.call(arguments);
    return self.apply(this instanceof fBound ? this : obj, args.concat(newArgs));
  }
  
  fNOP.prototype = this.prototype; // 他俩指向的是同一原型对象。
  fBound.prototype = new fNOP(); // 之后要找原型链上的属性就是 fBound实例.__proto__ === fBound.prototype, fBound.prototype.__ptoto__ === FNOP.prototype === this.prototype。
  return fBound;
}

此时对 fBound.prototype 的操作就不会同步到绑定函数的 prototype 的修改了。

到此为止代码完成百分之98了,再来最后的细节优化。

最终代码

最后优化内容:

  1. 调用 bind 的不是函数就报错。
  2. 做个兼容。
Function.prototype.mybind = Function.prototype.bind || function (obj) {
  if (typeof this !== "function") {
    throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  }
  var self = this;
  var args = Array.prototype.slice.call(arguments, 1);
  
  var fNOP = function () {};

  var fBound = function () {
    var newArgs = Array.prototype.slice.call(arguments);
    return self.apply(this instanceof fBuond ? this : obj, args.concat(newArgs));
  }
  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
  return fBound;
}

参考

  1. MDN
  2. JavaScript 深入之 bind 的模拟实现

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

相思故

暂无简介

文章
评论
28 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文