前言

距离攻克JS的难点又近了一步,因为今天打算把JS的这三个函数翻出来说一说,虽然是老生常谈,但是用自己的话来说明白这三个函数毕竟是第一次,所以很多人都会想说“不用写博客啦,反正百度一搜(鄙视用百度搜索)都可以找到,干嘛费那个劲去总结别人都总结过的东西呢?”。但是我只想说一句“那是别人的东西,不是你自己的”。

除了上面的原因之外主要还是因为我正要写的一篇博客(后续会发表的Eslint背后那些我们应该知道的为什么)涉及的内容把我卡住了,因为这篇文章牵扯的内容都是JS的难点(我自认为的难点),所以完成博客Eslint背后那些我们应该知道的为什么之前我就得好好掌握这些难点,因为后续还会有ES6学习之箭头函数以及ES6学习之class两篇文章。

1、call

call是Function对象的原型方法,在MDN上解释得倒是很轻松,一句话带过:

call()方法会调用一个单独提供给定的**this**值和参数的函数。

刚开始我也是对这句话不甚了解,不过看完examples后,相信你应该就很清楚了:

在例子中我们定义了两个对象(其实是一个函数和纯对象),在函数中打印一句话,可是里面的变量使用了this来求值。如果你对this清楚的话,此时函数animal是属于浏览器windows对象的(假定在浏览器上做测试)。那么this.typethis.name将都是undefined

之后我们就要用到call()方法,根据解释:当我们调用一个已经存在的函数的时候可以指定一个我们想要给定的上下文(也就是this),然后函数在执行的时候就只会在我们指定的上下文环境中执行,也就是你看到后面的语句animal.call(dog);

call方法除了携带指定的this值之外,还可以带一串参数,即:

Function.prototype.call (thisArg , ...args)

那么当我们调用call的时候,将会执行以下操作:

  1. 检查操作,判断函数的合法性
  2. 初始化argList为空列表
  3. 如果这个方法被调用的时候带有不止一个参数,那么从左到右的顺序,从第一个开始,依次追加参数到argList中。
  4. 执行PrepareForTailCall()
  5. 返回Call(func, thisArg, argList)

如此我们使用call的时候,你可以在一个对象中继承另外一个对象的所有信息,就如你所见,animal中的属性都是从dog对象中继承来的。

使用call 有助于解决一个经典的闭包问题,题目来源于MDN

2、apply

ok,call基本上讲完了,那apply呢?几乎和call一模一样,那为什么ECMA还需要多加这么一个方法呢?人家自有道理呀,因为apply的传参形式改为了数组方式,形如:

Function.prototype.apply ( thisArg, argArray )

将之前的第一个例子改用apply为:

MDN中给出了一个例子,刚开始还真是没看懂,之后细细品读,把自己的理解说一下:

首先应该理解的是Object.create(),该方法是E5中提出的一种新的对象创建方式,第一个参数是要继承的原型,如果不是一个子函数,可以传一个null,第二个参数是对象的属性描述符,这个参数是可选的。也就是说使用这个方法,可以一次性地创建一个对象并且指定它的原型对象,类似于这种写法:

function func(){}

func.prototype = {};

return func;

所以我们在原生Function的原型对象上加了一个construct的方法,然后首先在方法内部使用Object.create()创建一个新的对象,该对象的原型对象设置为MyConstructor方法的原型对象,之后让MyConstructor方法执行apply操作,其中this指针指定为新建的对象,参数是我们传递的数组,于是oNew对象便有了三个属性:property0property1property2,然后返回这个新的对象。

这个实现方法其实是我们知道的new操作的一种实现方法,其实现流程的图示如下:

3、bind

bind函数就更有意思了,底层实际上使用的是apply,但是其实现过程却是完全不同。因为该方法是直接创建一个新的函数,该函数学名为bound function(简称BF)。BF是一个外来的(exotic)函数对象(来自ES6的术语:Bound Function Exotic Objects)封装了原始的函数对象。调用一个BF就会导致执行它封装的函数。

其内部实现原理在Function.prototype.bind ( thisArg , ...args)中列举了16个步骤,简单浓缩之后大致是:

Function.prototype.bind = function (scope) {
    var fn = this;
    return function () {
        return fn.apply(scope);
    };
}

给个使用例子:

bind的浏览器兼容性相较于前面两个函数比较差:

4、结论

  1. callapply是可以相互替换的,这仅仅是取决于你传递参数使用数组方便还是逗号分隔的参数列表方便。
  2. callapply很容易混淆掉,有时候会忘掉apply是使用数组还是列表,那么有一个简单的记住办法那就是applyaarraya是一致的,这样就记住了吧?
  3. bind稍微不同,因为它返回的是一个函数,可以在任何你想要执行的时候执行,而前面两个函数都是立马执行的。因此总体来说bind的灵活性会比callapply更好,适用的场景更多

参考

  1. //www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.call
  2. //www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.apply
  3. https://www.smashingmagazine.com/2014/01/understanding-javascript-function-prototype-bind/