Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bind 方法的兼容实现 #19

Open
lessfish opened this issue Sep 5, 2016 · 1 comment
Open

bind 方法的兼容实现 #19

lessfish opened this issue Sep 5, 2016 · 1 comment

Comments

@lessfish
Copy link
Owner

lessfish commented Sep 5, 2016

自从进入七月以来,我的 underscore 源码解读系列 更新缓慢,再这样下去,今年更完的目标似乎要落空,赶紧写一篇压压惊。

前文 跟大家简单介绍了下 ES5 中的 bind 方法以及使用场景(没读过的同学建议先看看),毕竟 bind 是 ES5 的东西,低版本 IE 不支持。今天就根据 underscore 的实现,来聊一聊如何实现一个 bind 的 polyfill。

之前在 ECMAScript 5(ES5) 中 bind 方法简介备忘 一文中,给出了一个 "穷人版" 的 polyfill,如下。

Function.prototype.bind = Function.prototype.bind || function(context) {
  var that = this;
  return function() {
    return that.apply(context, arguments);
  }
}

说实话,基本可以满足多数的场景需求了。bind 方法返回的还是一个方法(经典闭包),很巧妙地用 apply 改变(绑定)了 this 指向。但是毫无疑问这样简单的实现是有问题的。

首先,该方法只支持传入一个参数,为方法需要绑定的 this 指向,原生的 bind 方法可以传入多个参数,如果要问这些参数干嘛用,回头翻翻 前文。如何实现传参?非常简单,传入,然后提取,不就 ok 了?

underscore 源码中重点看这几行:

var args = slice.call(arguments, 2);
var bound = function() {
  // args.concat(slice.call(arguments))
  // 最终函数的实际调用参数由两部分组成
  // 一部分是传入 _.bind 的参数
  // 另一部分是传入 bound(_.bind 所返回方法)的参数
  return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
};

return bound;

其实核心实现差不多,都是闭包返回函数。第一行将参数(args)提取保存(这些参数将会在方法中被优先调用),返回的是一个叫做 bound 的方法,bound 也能传参啊,用 args.concat(slice.call(arguments)) 将两个参数合并当做原方法的参数,因为 args 会优先调用,所以合并结果 args 中元素在先。

接着来看 executeBound 函数,为何 "穷人版" 一行的代码,这里却要整个函数出来?原因是 "穷人版" 没有考虑 bind 返回函数被 new 操作的情况。如果不是被 new 操作,那就简单了,和 "穷人版" 是一样一样的,直接看 underscore 源码。

// 非 new 调用 _.bind 返回的方法(即 bound)
// callingContext 不是 boundFunc 的一个实例
if (!(callingContext instanceof boundFunc))
  return sourceFunc.apply(context, args);

如果进行 new 运算操作呢?这里我们还要复习一下 new 运算,有兴趣的可以看下我以前的文章 一道有意思的笔试题引发的对于 new 操作符的思考。概括地讲,如果构造函数有返回值,且返回值是对象(不能是 null),那么对其进行 new 操作返回该对象,否则返回构造实例。所以在方法 executeBound 中,我们需要进一步判断这个构造函数有没有返回值,返回值是不是对象。

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 非 new 调用 _.bind 返回的方法(即 bound)
  // callingContext 不是 boundFunc 的一个实例
  if (!(callingContext instanceof boundFunc))
    return sourceFunc.apply(context, args);

  // 如果是用 new 调用 _.bind 返回的方法

  // self 为 sourceFunc 的实例,继承了它的原型链
  // self 理论上是一个空对象(还没赋值),但是有原型链
  var self = baseCreate(sourceFunc.prototype);

  // 用 new 生成一个构造函数的实例
  // 正常情况下是没有返回值的,即 result 值为 undefined
  // 如果构造函数有返回值
  // 如果返回值是对象(非 null),则 new 的结果返回这个对象
  // 否则返回实例
  // @see http://www.cnblogs.com/zichi/p/4392944.html
  var result = sourceFunc.apply(self, args);

  // 如果构造函数返回了对象
  // 则 new 的结果是这个对象
  // 返回这个对象
  if (_.isObject(result)) return result;

  // 否则返回 self
  // var result = sourceFunc.apply(self, args);
  // self 对象当做参数传入
  // 会直接改变值
  return self;
};

关于这部分的源码,有兴趣的同学可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L698-L719。

关于 Function 这部分,接下去的打算是去抖一篇,节流一篇,然后其他零碎的方法概要一篇,希望能在十月中旬左右结束掉吧。

@shenzhim
Copy link

shenzhim commented Jul 5, 2018

如果是通过new方式 调用bind后的函数时,直接

if (callingContext instanceof boundFunc) { return new sourceFunc(... args) }
这样的方式不是更好点么?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants