前面我们已经说完了关于 call 和 apply 作用与代码实现,不难发现他们的作用就是使函数立即被调用,同时修改函数执行时的 this 作用域的指向。这里我们再说一个比较类似的方法 —— bind。可以用来自 MDN 的一句话描述 bind 方法的作用:

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

    bind 语法:

    1. function.bind(thisArg[, arg1[, arg2[, ...]]])

    参数详解:

    • thisArg:调用绑定函数时作为 this 参数传递给目标函数的值。如果使用 new 运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,执行作用域的 this 将被视为新函数的 thisArg

    • arg1, arg2, …:当目标函数被调用时,预先添加到绑定函数的参数列表中的参数

    • 返回值:返回一个原函数的拷贝,并拥有指定的this值和初始参数

    来自 MDN 对 bind 方法进行解释的那句话怎么理解呢,我们先来看两段示例代码:

    1. var obj = {
    2. name: 'littleLane'
    3. }
    4. // 下面我们会称之为未绑定函数,即需要绑定的函数
    5. function sayName() {
    6. console.log(this.name)
    7. }
    8. // bind 方法会返回一个修改了 sayName 函数 this 指向的函数,我们称之为绑定函数
    9. var boundFunc = sayName.bind(obj)
    10. boundFunc() // littleLane

    根据我们之前说过的利用 call 和 apply 修改函数 this 指向的方法可以实现一个初版的 bind 方法:

    1. Function.prototype.bind2 = function(ctx) {
    2. var self = this
    3. return function() {
    4. return self.apply(ctx)
    5. }
    6. }

    上面的代码块会在 Function 构造函数的原型上(为了让所有函数都具有该方法)添加一个 bind2 方法,bind2 方法会接受一个上下文对象作为参数,在函数体里面首先通过变量 self 保存当前调用 bind2 方法的函数,然后返回一个函数,返回的函数体里面再返回 self.apply(ctx)。

    我们前面说过调用 bind 方法后得到的是一个被修改了 this 指向的绑定函数,所以会返回 function(){},当调用绑定函数的执行就会执行返回的 function(){},进而执行 self.apply(ctx),就是调用函数本身,并传入上下文对象以达到 bind 函数的目的。

    这里为什么是 return self.apply(ctx) 呢?那是因为未绑定函数可能会有返回值。

    1. var obj = {
    2. name: 'littleLane'
    3. }
    4. // 下面我们会称之为未绑定函数,即需要绑定的函数
    5. function sayName() {
    6. console.log(this.name)
    7. return this.name
    8. }
    9. // bind 方法会返回一个修改了 sayName 函数 this 指向的函数,我们称之为绑定函数
    10. var boundFunc = sayName.bind(obj)
    11. // result 的值为 littleLane
    12. var result = boundFunc() // littleLane

    上面说了函数未传参数的情况,下面就来看看如果有传参需要怎么处理,还是一段示例代码:

    1. var obj = {
    2. name: 'littleLane'
    3. }
    4. // 下面我们会称之为未绑定函数,即需要绑定的函数
    5. function sayName(age, name) {
    6. console.log('arg1', age)
    7. console.log('arg2', name)
    8. console.log(this.name)
    9. }
    10. // bind 方法会返回一个修改了 sayName 函数 this 指向的函数,我们称之为绑定函数
    11. var boundFunc = sayName.bind(obj, 25)
    12. boundFunc('strong')
    13. // output:
    14. // arg1 25
    15. // arg2 strong
    16. // littleLane

    看到示例代码是不是感觉有点复杂了,怎么回事嘛,小老弟?莫慌,我们来强行分析一波:我们这里定义的未绑定函数 sayName 会接受两个参数,并会将他们的值打印出来,当 sayName 函数执行 bind 方法进行上下文绑定时,会额外的接受一个参数作为 sayName 执行时的第一个参数,执行完 bind 方法后会得到一个已经绑定新上下文的绑定函数 boundFunc,当我们调用 boundFunc 函数时,传递的参数就是未绑定函数 sayName 接受的第二个参数了,也就是两次接受的参数会合并起来,作为未绑定函数 sayName 调用时的传参。

    其中上面示例代码中的参数分开传递和下面的参数一起传递是一样的效果:

    1. var boundFunc = sayName.bind(obj, 25, 'strong')
    2. boundFunc()
    3. // output:
    4. // arg1 25
    5. // arg2 strong
    6. // littleLane
    7. // 等同于
    8. var boundFunc = sayName.bind(obj)
    9. boundFunc(25, 'strong')
    10. // output:
    11. // arg1 25
    12. // arg2 strong
    13. // littleLane

    上面的初版,我们只是模拟了极简的一版,下面我们就在初版的基础上加上参数的处理吧:

    1. Function.prototype.bind2 = function(ctx) {
    2. var self = this
    3. // 获取调用 bind 函数时传递的参数
    4. var args = Array.prototype.slice.call(arguments, 1)
    5. return function() {
    6. var bindArgs = Array.prototype.slice.call(arguments)
    7. return self.apply(ctx, args.concat(bindArgs))
    8. }
    9. }

    你以为到这里就已经大功告成吗,我只能说你有点年轻了,其实 bind 还有一个特性:一个绑定函数也能使用 new 操作符创建对象,这种行为就像把原函数当成构造器,提供的 this 值会被忽略,同时调用时的参数被提供给模拟函数。

    上面的特性翻译成大白话就是说:当 bind 返回的函数作为构造函数调用时,调用 bind 方法时指定的 this 值会失效,但是传入的参数依然生效。举个例子:

    1. var obj = {
    2. name: 'littleLane'
    3. }
    4. // 下面我们会称之为未绑定函数,即需要绑定的函数
    5. function sayName(age, name) {
    6. this.flag = 'sayName'
    7. console.log('arg1', age)
    8. console.log('arg2', name)
    9. console.log(this.name)
    10. }
    11. sayName.prototype.hello = 'Hello'
    12. var boundFunc = sayName.bind(obj, 25)
    13. var boundObj = new boundFunc('strong')
    14. // output:
    15. // arg1 25
    16. // arg2 strong
    17. // undefined
    18. boundObj.hello
    19. // Hello
    20. boundObj.flag
    21. //sayName

    其实这一段示例代码和之前的代码片段差不多,不同之处就是最后使用 new 操作符以构造函数的形式调用了经过 bind 方法获取到的绑定函数 boundFunc。

    我们来看一下,如果最后不是使用 new 操作符调用绑定函数,打印结果会是怎样

    1. var boundFunc = sayName.bind(obj, 25)
    2. var boundObj = boundFunc('strong')
    3. // output:
    4. // arg1 25
    5. // arg2 strong
    6. // littleLane

    当不使用 new 操作符调用绑定函数时,打印结果和我们上面的示例代码片段一致,这里需要注意的是:由于原始函数 sayName 没有显式的 return 值,导致 boundObj 的值为 undefined,所以就没有 boundObj.hello 和 boundObj.flag 的打印结果了。

    当使用 new 操作符调用绑定函数 boundFunc 时,根据我们上面提到的 bind 方法的这一特性:调用 bind 方法时传递的 this 值会被忽略,再根据我们之前关于 new 操作符的分析,boundObj 值就是原函数 sayName 的实例对象了,于是 boundObj.hello 和 boundObj.flag 会分别打印 Hello 和 sayName。而在原函数 sayName 内部,this 指向的 sayName 函数作用域的,this.name 并没有赋值,所以会打印 undefined。

    bind 方法作为构造函数调用绑定函数这一特性就说的差不多了,接下来我们就将剩下的实现补充完整:

    1. Function.prototype.bind2 = function(ctx) {
    2. if (typeof this !== 'function') {
    3. throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
    4. }
    5. var self = this
    6. // 获取调用 bind 函数时传递的参数
    7. var args = Array.prototype.slice.call(arguments, 1)
    8. var fBound = function() {
    9. var bindArgs = Array.prototype.slice.call(arguments)
    10. // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
    11. // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
    12. return self.apply(this instanceof fBound ? this : ctx, args.concat(bindArgs))
    13. }
    14. fBound.prototype = this.prototype
    15. return fBound
    16. }

    上面的实现代码还有可以优化的地方。仔细观察上面的代码,最后我们是直接将原函数内部 this 指向的 prototype 属性赋值给了最终通过调用 bind 函数生成的绑定函数的 prototype,也就是他们的 prototype 属性值指向了同一个引用,这样其中一个修改了,另外一个也会跟着修改,这是一个很大的问题。为了很好的解决这个问题,我们可以使用一个空函数进行中间转换,所以继续优化:

    1. Function.prototype.bind2 = function(ctx) {
    2. if (typeof this !== 'function') {
    3. throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
    4. }
    5. var self = this
    6. // 获取调用 bind 函数时传递的参数
    7. var args = Array.prototype.slice.call(arguments, 1)
    8. var fNOP = function () {};
    9. var fBound = function() {
    10. var bindArgs = Array.prototype.slice.call(arguments)
    11. // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
    12. // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
    13. return self.apply(this instanceof fNOP ? this : ctx, args.concat(bindArgs))
    14. }
    15. fNOP.prototype = this.prototype
    16. fBound.prototype = new fNOP()
    17. return fBound
    18. }

    不知道大家有没有注意到,最后两个版本的实现代码片段中多了一个对 this 类型值的判断,这里的逻辑是为了处理当调用 bind 的不是函数,就抛出错误提示。

    好了,到这里 bind 相关的知识点就说完了,罗里吧嗦说了很多,还有上面每一版本的测试代码我就没有贴出来,大家可以自己动手测试一下!