阅读 685

趣谈js的bind牌胶水

前言

今天聊一聊js中的bind方法,主要从三个维度来阐述:why——>what——>how。文章虽经个人多次校验,对语言表述、代码书写等进行了认真审核,但仍免不了有疏漏之处,如若发现,还望指出,鄙人将审而改之,如若有不爽之处,还望轻喷,理性交流,共同进步也。

Why ???——> 为什么会诞生bind?

1. 背景讲解bind诞生的原因:

bind是ECMAscript5新增的一个方法,ECMAscript是js的编程语言实现(详情可阅相关资料),ECMAscript5是当前主流浏览器的通用支持版本,这个版本的出现很大程度上是为了解决js这门语言在诞生到发展过程中出现的大量问题而提出的解决方案版本,在早期,js定位为“网页小助手”语言,只负责做简单的校验表单字段小活,一度还沦为广告弹框专属语言,因为其尴尬的定位,所以js充满各种意想不到的坑,大家一直也不怎么重视它,直到基于Ajax技术的Gmail项目诞生(Gmail项目不是直接原因,这里只是借机聊下js历史),大家才发现利用js可以做出这么多牛逼的交互,一时间,各大公司蜂拥而至,大公司的项目往往预示着项目的复杂和多人协作,当项目一复杂后大家发现js的缺点就暴露出来了,js虽然在其名里面包含了Java,但其命名只属于取巧沾光,Java面向对象编程的特性可没被js吸收,js语言更具函数式编程特性,函数为js语言的一等公民,当函数越写越多之时,管理他们的艺术就被提上了台面,为了复杂项目开发的规范化、统一化,js迫切需要引入面向对象的相关思想,但面向对象属于语言灵魂层次,js作为函数式编程使用了这么多年,不可能想改就改灵魂层次的东西,为了兼顾函数式编程的灵活和面向对象编程的规范,js开发的相关组织做了很多努力,其中一个努力就是创造出了bind、call、apply三个媒婆,这三个媒婆的共同作用就是为js的一等公民Function函数找个门当户对的人家(指明Function函数的this指向)。

2. 代码讲解bind诞生的原因:

我定义了一个类:

  var Xiaoming = {
    name: '小明',
    sayHi() {
      console.log('hello ' + this.name);
    }
  }
复制代码

如果我想使用这个类的sayHi功能,一开始,我想到的是直接拿来就用:

  var Xiaoming = {
    name: '小明',
    sayHi() {
      console.log('hello ' + this.name);
    }
  }

  var sayHi = Xiaoming.sayHi;
  sayHi(); // hello 
复制代码

不出意外,将会输出hello(在严格模式下将会直接报出cannot read property 'name'错误),原因就是如果直接拿来用,这里的this将会隐式指向到全局window对象,而全局对象中并没有name属性。在js中,当没有明确指定this的情况下,置于全局环境下的函数的this将会是window(注:浏览器环境下为window,node环境下为global,其它宿主环境本篇不做解释,本篇文章涉及的宿主环境都是浏览器)。

function func() {
  console.log(this.toString());  // [object Window]
}

func();
复制代码

window是全局环境下this的最终归属(如果你无家可归,你的家就是这片天地),如果我们想给这些无家可归的可怜函数找一个归属,我们需要一个中介来牵线搭桥,bind就是那个中介之一,bind在js中充当粘合剂的作用,他负责把指定的类和Function函数强力的粘贴在一起:

  var Xiaoming = {
    name: '小明',
    sayHi() {
      console.log('hello ' + this.name);
    }
  }

  var Jack = {
    name: '杰克'
  };

  var sayHello = Xiaoming.sayHi.bind(Jack);
  sayHello(); // hello 杰克
复制代码

当我们用bind粘贴剂把sayHi方法和Jack类粘贴在一起时,sayHello函数的this就指向Jack类了,所以输出的结果就是hello 杰克

What ???——> 什么是bind?

1. 汉语释义:

vt. 绑;约束;装订;包扎;凝固 vi. 结合;装订;有约束力;过紧

在汉语释义中,bind的大体意思就是绑定、结合,我个人给其在js中的定义为胶水(注意胶水二字!)。当我想给一个函数换一个新宿主之时,我就取出“bind牌胶水”把想用的函数和它的新宿主粘贴在一起,然后再调用这个用“bind牌胶水”粘贴的拥有新宿主的新函数。

2. MDN释义:
  • English:

The bind( ) method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

  • 中文:

bind( )方法创建了一个新函数,当新函数被调用之时,将其this指向到指定的值,同时会通过bind传入一串预设参数序列供新函数使用。

在这段定义中我抽出了几个细节:

1. 创建了一个新函数 ——> 这句话很关键,这也是我把bind定义为胶水的原因,这句话以下几点须注意:
  • 使用bind,它不会破坏原先的宿主(意即:不是把函数从原先的宿主中删除掉):
  var Xiaoming = {
    name: '小明',
    sayHi() {
      console.log('hello ' + this.name);
    }
  }

  var Jack = {
  name: '杰克'
};

  var sayHello = Xiaoming.sayHi.bind(Jack);
  console.log(xiaoming); // {name: "小明", sayHi: ƒ}
复制代码
  • 使用bind,会创建一个新函数,意即:把指定的函数从原先的宿主中“复制”一份成新函数,然后通过“bind牌胶水”把指定的宿主和新函数粘贴在一起。这个新函数的this将会指向到指定宿主,而且和之前的旧宿主撇清了关系,实现了和指定宿主的结合。注意:1、不是把函数绑定到指定宿主上;2、这里的绑定是“按址绑定”,不是copy了一份指定宿主,所以当这个粘贴的指定宿主发生改变时,使用“bind牌胶水”粘贴的新函数也会受影响:
var Xiaoming = {
  name: '小明',
  sayHi() {
    console.log('hello ' + this.name);
  }
}

var Jack = {
  name: '杰克'
};

var sayHello = Xiaoming.sayHi.bind(Jack);
sayHello(); // hello 杰克
Jack.name = '皆可'; // 改变新宿主的name属性
sayHello(); // hello 皆可    <—— 当新宿主发生改变时,对应的输出也会受影响

/* 并没有把函数绑定到新宿主上 */
Jack.sayHi(); // error: Uncaught TypeError: Jack.sayHi is not a function
Jack.sayHello(); // error: Uncaught TypeError: Jack.sayHello is not a function
复制代码
2. 一串预设参数序列供新函数使用

“bind牌胶水”的主要作用是给指定函数绑定指定this,第一个参数即指定的新宿主,其后的剩余参数为预设参数,既然第一个参数已经达到了目的,为什么还要在其后加一些预设参数呢?这里要注意参数的预设二字,预设表示预先设定给新函数的参数,通过bind预设的参数将会比新函数自己设定的参数预先使用。看代码:

    var obj1 = {
      name: 'han',
      sayHi(word1, word2) {
        console.log('hello' + this.name + ',' + word1 + ',' + word2);
      }
    };
    
    var obj2 = {
      name: '李'
    };
    var func = obj1.sayHi.bind(obj2, '早上好');
    func('good morning'); // hello 李,早上好, good morning
    func(); // hello 李,早上好, undefined
复制代码

通过代码我们发现这个预设参数和默认参数有点类似(但其实完全不是一回事!),因为通过bind预设的参数总是先被调用,而使用新函数时自定义的参数总是等预设参数调用后再被调用,类似的概念(先进先出)。这个预设参数的设计,我个人觉得略显尴尬,可能是因为js的函数之前没有默认参数的设定导致的吧(不甚了解)?这里用默认参数个人觉得会更合适。

How ???——> 怎么使用bind?

1. 在事件绑定中使用:

在改变this指向的方法中,存在着三个方法:bind、call、apply,bind属于“静态绑定”,作为胶水,bind只负责粘贴函数,不负责粘贴之后的函数的运行,但call和apply却不是,他们给函数绑定this后还把绑定后的函数给当场运行了。因为这个特性,我们在给事件绑定函数时只能使用bind来进行this的绑定(因为给事件绑定的函数不需要我们手动执行,它是在事件被触发时由JS 内部自动执行的),看代码:

  <button id="btn">Click Me</button>
复制代码
  var obj = {
    thing: '搞点事情'
  };
  function onBtnClick() {
    console.dir('我被点击了,我想' + this.thing);
  }
  var btn = document.getElementById('btn');
  btn.addEventListener('click', onBtnClick.bind(obj)); // 我想搞点事情 
复制代码

2. 给迷失的函数找回自我:

在js编程中,经常会出现使用var that = this;的hack黑魔法来给函数找回自我,具体场景如下:

  var obj = {
    datas: ['jack'],
    resolveDatas: function() {
      var that = this;
      this.datas.forEach(function(val) {
        that.name = val;
        console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
        console.log(that); // {datas: Array(1), resolveDatas: ƒ, name: "jack"}
      });
      console.log(this.name); // jack
    }
  }
  obj.resolveDatas();
复制代码

在上面的代码中,当在内部函数内部使用匿名函数时,this将会指向到全局window对象,为了避免这个问题,在函数内部通过var that = this声明了一个变量,然后在forEach的匿名函数中调用,为什么要这样使用?这是因为在没出现ES6的箭头函数之前,js存在着一个“任性this”,关于js中this的复杂度,在《你不知道的js(上卷)》中写到:

this 关键字是js 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在 所有函数的作用域中。但是即使是非常有经验的js 开发者也很难说清它到底指向 什么。 任何足够先进的技术都和魔法无异。 ——Arthur C. Clarke

关于js中this的指向黑魔法问题这里只略提,具体可查阅相关权威资料。在上面的代码中,我们发现一个啼笑皆非的现实:好好的一个函数,咋包了一层函数后就找不到设想中的那个this了呢?本以为自己把this指向到了当前的obj对象,一到用的时候就直接“认贼作父”了,把this指向到了window对象,what the hell?,如何避免这种悲剧?让我们有请“bind牌胶水”隆重登场,作为专业的“this硬绑定”方法,bind用起来妥妥的:

  var obj = {
    datas: ['jack'],
    resolveDatas: function() {
      this.datas.forEach((function(val) {
        this.name = val;
        console.log(this); // {datas: Array(1), resolveDatas: ƒ, name: "jack"}
      }).bind(this));
      console.log(this.name); // jack
    }
  }
  obj.resolveDatas();
复制代码

在上面的代码中,我们通过“bind牌胶水”把真正想用的对象粘贴给了匿名函数,从而让匿名函数能够坚持自我,但在这里个人觉得这种硬绑定是一种笨拙的hack方法,因为针对这种诡异问题竟然要用胶水进行“修补”,个人觉得其实很low,所以不予提倡,Es6提出箭头函数才是专业应对该问题的合适方案:

  var obj = {
    datas: ['jack'],
    resolveDatas: function() {
      this.datas.forEach((val) => {
        this.name = val;
        console.log(this); // {datas: Array(1), resolveDatas: ƒ, name: "jack"}
      });
      console.log(this.name); // un
    }
  }
  obj.resolveDatas();
复制代码

关于bind的使用方,我这里只列举出了两个,更多的使用场景还有很多,可以查阅相关资料。

后语

花了好几天时间终于写完了这篇文章,希望相关内容能给大家带来一些启发和感悟。一般讲bind的时候都会把call和apply放在一起聊,我本有此意,但考虑到自己的啰嗦话语,内容过长,所以还是分开讲解,下一篇文章我来聊聊apply和call方法(因为这两个方法就是孪生兄弟,所以一起讲再合适不过了),然后到下篇再对bind、apply、call三者进行对比来阐述,若知后事如何,且听下回分解!

关注下面的标签,发现更多相似文章
评论