从this机制看bind的实现

前言

从本文你可以得到:javascript

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

废话很少说,咱们一块儿来看看this的绑定机制。前端

this绑定机制

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

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

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

例子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:隐式绑定post

// 隐式绑定通常发生在函数做为对象属性调用时
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:显式绑定ui

// 显式绑定就是咱们常谈的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绑定this

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

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绑定

例子5:

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

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

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

console.log(context.bar); // 1
console.log(instance.bar); // 3

// 可见foo并无改变context.bar的值而是建立了一个新对象,符合咱们对new绑定的描述
复制代码

根据上面的列子咱们能够得出结论new绑定 > 显式绑定

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

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

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

例子6:

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绑定的状况

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

欢迎阅读我其余文章:

参考资料:

相关文章
相关标签/搜索