手动实现bind函数以及如何实现extend

为什么要自己去实现一个bind函数?

bind()函数在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。

所以,为了理想主义和世界和平(所有浏览器上都能随心所欲调用它),必要的时候需要我们自己去实现一个bind。那么,一个bind函数需要具备什么功能呢?

bind函数的核心作用:绑定this、初始化参数

绑定this、定义初始化参数是它存在的主要意义和价值。MDN对它的定义如下:

语法:fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值(thisArg)。

被调用时,arg1、arg2等参数将置于实参之前传递给被绑定的方法。

它返回由指定的this值和初始化参数改造的原函数拷贝。

鉴于这两个核心作用,我们可以来实现一个简单版看看:

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
      if (typeof this !== 'function') {
      return
    }
  
    let self = this
    let args = Array.prototype.slice.call(arguments, 1)
    return function () {
      return self.apply(oThis, args.concat(Array.prototype.slice.call(arguments))) //这里的arguments是执行绑定函数时的实参
    }
  }
}

由于arguments是类数组对象,不拥有数组的slice方法,所以需要通过call来将slice的this指向arguments。args就是调用bind时传入的初始化参数(剔除了第一个参数oThis)。将args与绑定函数执行时的实参arguments通过concat连起来作为参数传入,就实现了bind函数初始化参数的效果。

bind函数的另外一个也是最主要的作用:绑定this指向,就是通过将调用bind时的this(self)指向指定的oThis来完成。这样当我们要使用bind绑定某个对象时,执行绑定函数,它的this就永远固定为指定的对象了~

遇到new操作符的时候呢

到这里,我们已经可以用上面的版本来使用大部分场景了。但是~

但是,这种方案就像前面说的,它会永远地为绑定函数固定this为指定的对象。如果你仔细看过MDN关于bind的描述,你会发现还有一个情况除外:

thisArg:当使用new 操作符调用绑定函数时,该参数无效。
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

我们可以通过一个示例来试试看原生的bind对于使用new的情况是如何的:

 function animal(name) {
    this.name = name
}
let obj = {}

let cat = animal.bind(obj)
cat('lily')
console.log(obj.name)  //lily

let tom = new cat('tom')
console.log(obj.name)  //lily
console.log(tom.name)  //tom

试验结果发现,obj.name依然是lily而没有变成tom,所以就像MDN描述的那样,如果绑定函数cat是通过new操作符来创建实例对象的话,this会指向创建的新对象tom,而不再固定绑定指定的对象obj。

而上面的简易版却没有这样的能力,它能做到的只是永久地绑定指定的this(有兴趣的朋友可以在控制台使用简易版bind试下这个例子看看结果)。这显然不能很好地替代原生的bind函数~

那么,如何才能区分绑定函数有没有通过new操作符来创建一个实例对象,从而进行分类处理呢?

区分绑定函数是否使用new,分类处理

我们知道检测一个对象是否通过某个构造函数使用new实例化出来的最快的方式是通过 instanceof

A instanceof B //验证A是否为B的实例

那么,我们就可以这样来实现这个bind:

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
      if (typeof this !== 'function') {
      return
    }
    
    let self = this
    let args = Array.prototype.slice.call(arguments, 1)
    let fBound = function() {
        let _this = this instanceof self ? this : oThis //检测是否使用new创建
        return self.apply(_this, args.concat(Array.prototype.slice.call(arguments)))
    }
    
    if (this.prototype) {
      fBound.prototype = this.prototype
    } 
    return fBound
  }
}

假设我们将调用bind的函数称为C,将fBound的prototype原型对象指向C的prototype原型对象(上例中就是self),这样的话如果将fBound作为构造函数(使用new操作符)实例化一个对象,那么这个对象也是C的实例,this instanceof self就会返回true。这时就将self指向新创建的对象的this上就可以达到原生bind的效果了(不再固定指定的this)。否则,才使用oThis,即绑定指定的this。

但是这样做会有什么影响?将fBound的prototype原型对象直接指向self的prototype原型对象,那么当修改fBound的prototype对象时,self(上述C函数)的prototype对象也会被修改!!考虑到这个问题,我们需要另外一个function来帮我们做个中间人来避免这个问题,我们看看MDN是怎么实现bind的。

MDN提供的Polyfill方案 及 YUI库extend函数实现

MDN针对bind没有被广泛支持的兼容性提供了一个实现方案:

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs = Array.prototype.slice.call(arguments, 1),//这里的arguments是跟oThis一起传进来的实参
      fToBind = this,
      fNOP    = function() {},
      fBound  = function() {
        return fToBind.apply(this instanceof fNOP
          ? this
          : oThis,
          // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
          aArgs.concat(Array.prototype.slice.call(arguments)));
      };

    // 维护原型关系
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype;
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
}

发现了吗,和上面经过改造的方案相比,最主要的差异就在于它定义了一个空的function fNOP,通过fNOP来传递原型对象给fBound(通过实例化的方式)。这时,修改fBound的prototype对象,就不会影响到self的prototype对象啦~而且fNOP是空对象,所以几乎不占内存。

其实这个思路也是YUI库如何实现继承的方法。他的extend函数如下:
如何实现 extend:

function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

最后一步是将Child的constructor指回Child。

总结

实现一个原生的函数,最重要的是理清楚它的作用和功能,然后逐一去实现它们包括细节,基本上就不会有问题~

这里用到的一些关于prototypeinstanceof的具体含义,可以参考阮一峰老师的 prototype 对象,相信对你理解JavaScript的原型链和继承会有帮助~

原文链接:https://lvdingjin.github.io/tech/2018/06/05/achieve-bind.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容

  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,564评论 0 5
  • 特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS...
    杀破狼real阅读 691评论 0 1
  • 函数只定义一次,但可能被执行或调用任意次。JS函数是参数化的,函数的定义会包括一个称为形参的标识符列表,这些参数在...
    PySong阅读 853评论 0 0
  • 函数只定义一次,但可能被执行或调用任意次。JS函数是参数化的,函数的定义会包括一个称为形参的标识符列表,这些参数在...
    PySong阅读 526评论 0 0
  • 夲圣阅读 368评论 0 3