乐趣区

从this机制看bind的实现

前言

从本文你能够获得:

  • 理解 this 的绑定机制及其优先级
  • 学会使用 apply/call 实现 bind 函数
  • 学会不使用 apply/call 实现 bind 函数

废话不多说,我们一起来看看 this 的绑定机制。

this 绑定机制

开局上结论this 有四种绑定模式分别是默认绑定、隐式绑定、显式绑定、new 绑定。

他们之间的优先级关系为:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

让我们来看一些例子分清这几种绑定:

例子 1:默认绑定

// 默认绑定
var str = 'hello world'
function log() {console.log(this.str) 
}

// 此时 this 默认指向 window
log() // hello world

// 在严格模式下 this 默认指向 undefined 的
'use strict'
var str2 = 'hello world'
function log() {console.log(this.str2) 
}

// 此时 this 指向 undefined
log() // 报错 TypeError,因为程序从 undefined 中获取 str2

例子 2:隐式绑定

// 隐式绑定一般发生在函数作为对象属性调用时
var bar = 'hello'
function foo() {console.log(this.bar)
}

var obj = {
    bar: 'world',
    foo: foo
}

foo() // hello,this 指向 window 所以输出 hello
obj.foo() // world,this 隐式绑定了 obj,这时候 this 指向 obj 所以输出 world

列子 3:显式绑定

// 显式绑定就是我们常谈的 apply,call,bind
var bar = 'hello'
var context = {bar: 'world'}
function foo() {console.log(this.bar);
}

foo() // hello
foo.call(context) // world 可见此时 this 的指向已经变成了 context

列子 4:new 绑定

new 绑定比较特殊,new 大部分情况下是创建一个新的对象,并将 this 指向这个新对象, 最后返回这个对象。

function Foo(bar) {this.bar = bar}

// 创建一个新的对象,并将 this 指向这个对象,将这个对象返回赋值给 foo
var foo = new Foo(3);
foo.bar // 3

说完 this 的绑定类型,我们考虑下下面的代码的输出

var context = {bar: 2}

function Foo() {this.bar = 'new bar'}

var FooWithContext = Foo.bind(context);
var foo = new FooWithContext();

// 考虑下面代码的输出
console.log(foo.bar) 
console.log(context.bar)

// 结果是:new bar 2
/** 
  * 我们可以发现虽然将使用 bind 函数将 this 绑定到 context 上,* 但被 new 调用的 Foo,他的 this 并没有绑定到 context 上。*/

四种 this 绑定的优先级验证

从上述例子 2 可以推断隐式绑定优先级是高于默认绑定的,所以这里我们只推导后续三种的绑定的优先级关系。

显式绑定和 new 绑定

例子 4:

// 我们先验证隐式绑定和显式绑定的优先级关系
var context = {bar: 1}

var obj = {bar: 2}

function foo() {
    // 对 bar 进行赋值
    this.bar = 3;
}

// 进行显式绑定
var fooWithContext = foo.bind(context);

obj.foo = fooWithContext;
obj.foo()

console.log(obj.bar); // 2
console.log(context.bar); // 3

// 可见 foo 并没有改变 obj.bar 的值而是创建了一个新对象,符合我们对 new 绑定的描述

根据上面的列子我们可以得出结论 new 绑定 > 显式绑定

另外几种绑定的优先级情况

根据例子 2 和例子 3,我们可以轻松推导出隐式绑定和显式绑定要优先级高于默认绑定

我们验证一下隐式绑定和显式绑定的优先级关系。

var obj = {bar: 2}

var context = {bar: 3}

function foo () {this.bar = 4}

// 将 foo 的 this 绑定到 context 上
var fooWithContext = foo.bind(context);
// 将绑定后的函数,赋值给 obj 的属性 foo
obj.foo = fooWithContext;
obj.foo()

console.log(obj.bar); // 2 并没有改变 obj.bar 的值
console.log(context.bar); // 4 context.bar 的值发生了改变

可见显式绑定的 this 优先级要高于隐式绑定

最后我们便可以得出结论 new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

用 apply 实现 bind

刚刚说了那么多 this 的绑定问题,这到底和我们实现 bind 有什么关系?

我们来看看一段简单的 bind 的实现代码:

Function.prototype.bind(context, ...args) {
   const fn = this;
   return (...innerArgs) => {return fn.call(context, ...args, ...innerArgs)
   }
}

这个 bind 函数,在大部分情况都是能正常工作的,但是我们考虑如下场景:

function foo() {this.bar = 3}

var context = {bar: 4}

var fooWithContext = foo.bind(context);
var fooInstance = new fooWithContext();

console.log(context.bar) // 3

可以看到,被 new 调用后的 foo,在运行时 this 依然指向 context,这不符合我们刚刚根据原生方法推断的绑定优先级:new 绑定 > 显式绑定

所以我们在 实现 bind 的时候,需要考虑维护 new 调用的情况

我们来看看如何实现一个真正的 bind:

 Function.prototype.bind(context, ..args) {
     var fToBind = this;
     
     // 先声明一个空函数,用途后面介绍
     var fNop = function() {};
     
     var fBound = function(...innerArgs) {
         // 如果被 new 调用,this 应该是 fBound 的实例
         if(this instanceof fBound) {
             /**
               * cover 住 new 调用的情况
               * 所以其实我们这里要模拟 fToBind 被 new 调用的情况,并返回
               * 我们使用 new 创建的对象替换掉 bind 传进来的 context
               */
           return fToBind.call(this, ...args, ...innerArgs)
         } else {
            // 非 new 调用情况下的正常返回
            return fToBind.call(context, ...args, ...innerArgs)
         }
     }
     
     // 除了维护 new 的 this 绑定,我们还需要维护 new 导致的原型链变化
     // 执行 new 后返回的对象的原型链会指向 fToBind
     // 但是我们调用 bind 后实际返回的是 fBound,所以我们这里需要替换掉 fBound 的原型
     
       fNop.prototype = this.prototype;
       // fBound.prototype.__proto__ = fNop.prototype
       fBound.prototype = new fNop();
       /**
         * 这样当 new 调用 fBound 后,实例依然能访问 fToBind 的原型方法
         * 为什么不直接 fBound.prototype = this.prototype 呢
         * 考虑下将 fBound 返回后,给 fBound 添加实例方法的情况
         * 即 fBound.prototype.anotherMethod = function() {}
         * 如果将 fToBind 的原型直接赋值给 fBound 的原型,添加原型方法就会
         * 污染源方法即 fToBind 的原型
         */
     return fBound
 }

到这里我们就实现了一个符合原生表现的 bind 函数,但是有时候架不住有人问那不用 apply 和 call 如何实现 bind 呢?接下来我们使用隐式绑定来实现一个 bind

不使用 apply 和 call 实现 bind

我们刚刚分析完实现 bind 的实现需要注意的点,这里就不重复说明了,我们看看如何使用隐式绑定来模仿 bind。

// 我们把关注点放在如何替换 call 方法上
Function.prototype.bind(context, ...args) {
    var fToBind = this;
    var fNop = function() {};
    var fBound = function(...innerArgs) {
        // 我们将 fToBind 赋值给 context 一个属性上。context.__fn = fToBind;
        if(this instanceof fBound) {
            // 模拟 new 调用,创建一个新对象,新对象的原型链指向 fBound 的原型
            var instance = Object.create(fBound);
            instance.__fn = fToBind;
            var result = instance.__fn(...args, ...innerArgs);
            delete instance.__fn;
            // new 调用时,如果构造函数返回了对象,使用返回的对象替换 this
            if(result) return result;
            return instance;
        } else {
            // 在__fn 没有显式绑定的情况下,__fn 运行时 this 指向 context
            var result = context.__fn(...args, ...innerArgs);
            // 调用完后将 context 的__fn 属性删除
            delete context.__fn;
            return result;
      }
    }
    
    fNop.prototype = this.prototype;
    fBound.prototype = new fNop();
    return fBound;
}

到这里不使用 apply 实现的 bind 就大功告成了

总结

我来总结下一共有哪些点需要认清楚:

  • this 有四种绑定模式,默认绑定、隐式绑定、显式绑定、new 绑定
  • this 四种绑定的优先级关系:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
  • 实现 bind 需要额外维护 new 绑定的情况

看了这么多可能会有朋友问,箭头函数呢?

欢迎阅读我的另外一篇文章:箭头函数中的 this

参考资料:

  • MDN: Function.prototype.bind
  • MDN: new 操作符
退出移动版