this指向初探,实现new bind apply call

597 阅读6分钟

前言

this指向分为默认绑定,隐式绑定,显式绑定,new操作,箭头函数这几类,经常使用的改变this指向的方法就是call,apply, bind,这些我们经常用到的方法你是否对其实现方式好奇呢?其实搞懂这些并不难,我认为首先必须知道函数内部做了什么事情,他解决的是什么问题,然后我们才能针对性地去探索。

new

首先是最常见的new,new并不是一个函数,是一个运算符,用于创建一个对象的实例,这应该是大家熟知的。用法,new后面跟一个构造函数:

new constructor[([arguments])]

看一下MDN上的解释 new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(设置该对象的constructor)到另一个对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。 哟西,既然如此我们就围绕上面四点,动手肝一个myNew函数
function myNew() {
  // 创建一个空的简单JavaScript对象(即{});
  let obj = new Object()
  // 链接该对象(设置该对象的constructor)到另一个对象 --> 就是改变obj原型的指向
  // arguments是一个类数组,所以要这么调shift
  let con = [].shift.call(arguments)
  obj.__proto__ = con.prototype
  // 将步骤1新创建的对象作为this的上下文 ;
  // 如果该函数没有返回对象,则返回this。
  let res = con.apply(obj, arguments)
  // 如果构造函数person return了一个对象类型,就取return的值
  return typeof res == 'object' ? res : obj
}

function person(name) {
  this.name = name
}

let p = myNew(person, 'jack')

插播:为何箭头函数不能用作构造函数?

  1. 没有单独的this
  2. 不绑定arguments
  3. 箭头函数没有prototype属性 结合上述的代码实现,是不是清晰了一些呢?

我们还可以对代码进行优化,使用这种方式来改变和继承属性是对性能影响非常严重的,还会影响到所有继承来自该[[Prototype]]的对象,更优的方案是: Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

let obj = Object.create(con.prototype) //

instanceOf

引用自mdn:

instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上

instanceof的判断逻辑是: 从当前引用的proto一层一层顺着原型链往上找,能否找到对应的prototype。找到了就返回true。挺简单就可以实现一个简易的instanceof

/*obj 实例  con 构造函数*/
function _instanceOf(obj,con) {
    let _obj = obj.__proto__
    let _con = con.prototype
    while(true) {
        if(_obj === null) {
            return false
        }

        if(_obj === _con) {
            return true
        }

        _obj = _obj.__proto__
    }
}

缺点: 不能完全精确的判断复杂类型的具体数据类型

  • [] instanceof Array; //true
  • [] instanceof Object; //true

Bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 先看看官方的例子:

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

由上述例子我们可以知道,使用bind作用有二:

  1. 返回一个新的函数,给函数添加运行时参数,能使一个函数拥有预设的初始参数,倒是有点像es6的默认参数的作用。
  2. 显示绑定this值 ,这解决了绑定隐式丢失问题, 即函数中的 this 丢失绑定对象。例如:使用setTimeout,常见情况时执行时this指向 window。当使用对象的方法时,需要 this 引用对象,你可能需要显式地把 this 绑定到回调函数以便继续使用对象。例如: setInterval(obj.fn.bind(obj), 1000); 基此,我们可以尝试实现一个简单的bind:
function _bind() {
    let fn = this //需要绑定的函数
    let args = Array.from(arguments) //类数组 -> 数组
    let obj = args.shift() //绑定的对象

    return function () {
        fn.apply(obj, Array.from(args).concat(Array.from(arguments)))
    }
}
Function.prototype._bind = _bind
function fn(a, b) {
    console.log(this);  // obj
    console.log(a + b); // 3
}
let obj = {
    name: 'violetrosez'
}
let _fn = fn._bind(obj, 1)
_fn(2)

上述代码,看似满足了需求,但是当返回的函数被作为构造函数时,原函数调用的this指向了bind显示指定的对象,不能根据new的调用而绑定到new创建的对象。当被作为构造函数调用时,我们需要将绑定函数的作用域赋给新对象,并设置绑定函数继承目标函数的原型。修改后如下:

function _bind() {
    let fn = this //函数
    if (typeof fn !== 'function') {
        throw new TypeError('what is trying to be bound is not callable');
    }
    let args = Array.from(arguments)
    let obj = args.shift()

    let fNOP = Object.create(fn.prototype)
    let fBound = function () {
        //如果没有判断,构造函数test在执行时也指向obj,而不会指向新建的实例p,此时p.name == undefined
        fn.apply(this instanceof fn ? this : obj, Array.from(args).concat(Array.from(arguments)))
    }
    //使fBound.prototype是fN的实例,返回的fBound若作为new的构造函数,新对象的__proto__就是fN的实例
    fBound.prototype = fNOP
    return fBound
}

//测试
function test(name) {
    this.name = name
}
let obj = {}
let _fn = test._bind(obj)
_fn('violetrosez')
console.log(obj.name);  // violetrosez
let p = new _fn('zzzzz') 
console.log(obj.name); // violetrosez
console.log(p.name);  // zzzzz

参考:bind方法的实现 MDN-BIND

call

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数.call() 允许为不同的对象分配和调用属于一个对象的函数/方法。 已经有了上面的基础,我们再写这个有点得心应手了,过程都是一个思路,对参数进行处理

function _call(ctx, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('what is trying to be bound is not callable');
    }
    ctx = ctx || window
    ctx.fn = this

    let res = ctx.fn(...args)
    delete ctx.fn //删除掉引用
    return res
}

Function.prototype._call = _call

上面的做法其实是将 this 的默认绑定改为隐式绑定,ctx不存在的时候,我们使ctx指向全局对象,然后将函数作为要绑定的对象的一个方法执行,用完后删掉。这次我们使用es6剩余操作符处理参数,写法比上文更简洁了一些。

apply

apply和call唯一不同的就是他的剩余参数接收的是一个数组,所以把上面的代码改造一下即可。之前一直记混哪个是接受数组,后面干脆把apply的首字母a当成array去记忆了..

function _apply(ctx, args = []) {
    if (typeof this !== 'function') {
        throw new TypeError('what is trying to be bound is not callable');
    }
    if(args && !Array.isArray(args)) {
        throw new TypeError('apply need accept array object');
    }
    ctx = ctx || window
    ctx.fn = this

    let res = ctx.fn(...args)
    delete ctx.fn //删除掉引用
    return res
}

Function.prototype._apply = _apply

后记

至此,我们完成了实现call、apply 和 bind的过程,其实这些本质上都是要改变 this 的指向,在实现过程中一定要时刻搞清楚this的指向,写代码的过程中,什么时候可以用箭头函数,什么时候需要显示绑定this,一定要心中有数。理解了原理之后,就能明白 this 的绑定顺序为什么是 new > 显示绑定 > 隐式绑定 > 默认绑定。 如有疑问或者错误,请各位批评指正,共同进步。求点赞三连QAQ