new & 深入bind实现

58 阅读6分钟

new

MDN概念:new 运算符允许开发人员创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

翻译一下:new的作用实际上就是根据构造函数进行对象创建,将对象的原型和构造函数的原型进行绑定

举个例子:

function Otaku(name, age) {
	this.name = name;
	this.age = age;

	this.habit = 'Games';
}

// 因为缺乏锻炼的缘故,身体强度让人担忧
Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
	console.log('I am ' + this.name);
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName(); // I am Kevin

这是创建对象 配置对象 使用对象的一个示例过程,可以看到其确实也只做了我们上面说的两件事情

  1. 创建对象,执行构造函数赋予对象属性和方法
  2. 绑定原型使得对象能够访问到原型链上的属性和方法(原型不了解的同学可以先去学习一下原型和原型链~)

这也是我们的主要实现点

实现代码

由于我们无法覆盖JS中的操作符,所以我们只能够提供一个myNew(Otaku, ...)方法来绕点远路实现它

const myNew = (constructor, ...rest) => {
	const res = {}
	// 创建一个对象用于执行构造函数 进行初始值设定
	res.fn = constructor
	res.fn(...rest)
	delete res.fn
	// 原型绑定
	res.__proto__ = constructor.prototype
	return res
}

var person = myNew(Otaku, 'Kevin', '18')

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName(); // I am Kevin

代码解释

  1. new的作用是创建一个对象并且返回,所以我们创建了一个res作为初始对象 之后的变更都对res进行
  2. 通过隐式绑定的方式 在res上执行构造函数,使得构造函数的this指向实例对象res,从而能够修改其属性和方法
  3. 修改对象的原型,使之和构造函数的原型一致,组成原型链

new & bind

了解了new的实现后让我们来回头看看上篇文章中myBind留下的问题:为什么myBind后的函数无法在new时正确初始化实例?要如何判断bind返回的函数是否被new操作执行了呢?让我们来举例分析一下~

举例分析

// myBind的实现
Function.prototype.myBind = function (that, ...rest) {
	that = that || globalThis
	const curFn = this
	return function (...args) {
		that.fn = curFn; 
		that.fn(...rest, ...args)
		delete that.fn
	}
}

const myNew = (constructor, ...rest) => {
	const res = {}
	// 创建一个对象用于执行构造函数 进行初始值设定
	res.fn = constructor
	res.fn(...rest)
	delete res.fn
	// 原型绑定
	res.__proto__ = constructor.prototype
	return res
}

var foo = {
	value: "f",
};

function barConstuctor(param, param2) {
	this.value = "bar"
	console.log(this.value + param + param2);
}
const bindFunc = barConstuctor.myBind(foo, "myBind")
const instance = myNew(bindFunc, "param2")
console.log(foo) // { value: 'bar' }
console.log(instance) // {}

上述代码中,我们定义了一个构造函数barConstuctor,并且使用myBind方法获得到一个修改了this指针的函数bindFunc,再以之作为构造函数进行对象构建。其存在的问题是构造函数修改了bind绑定的对象foo而不是new出来的实例对象。

让我们来一步一步分析函数调用过程 来看看为什么会是这样的结果

  1. 首先myBind函数接受参数 this指向的foo,返回了一个匿名函数,将这个匿名函数赋给了变量bindFunc。其实现实际上是

    function (...args) {
            foo.fn = curFn; // curFn通过闭包保存的外界的this指针,即指向barConstuctor
            foo.fn(...rest, ...args)
            delete foo.fn
    }
    
  2. bindFunc作为参数传入myNew方法,并在其中被创建的初始实例对象res调用,即res.bindFunc(...rest)

  3. 因此最终的调用逻辑是:执行res的属性bindFunc函数,函数内部则执行了foo的属性的fn函数,即barConstuctor可以看出即barConstuctor最终是通过foo.barConstuctor执行,所以其this最终指向foo 也修改的是foo。成功解释上面的问题

解决方式

既然如此,我们能够做的就是在myBind返回的函数中判断当前是否是正在进行new操作,如果是就不修改this指向并且将this指向new的实例对象,如果不是则正常执行。

怎么判断呢?

  1. 首先我们需要注意到new操作的本质就是在对象上调用构造函数,即res.bindFunc(...rest),这点很关键,这意味着我们在bindFunc内部可以通过this指针来获取到res
  2. 光获取到res还不够,还得想想res执行和一般执行的区别。一般我们获取到bindFunc后在某个地方直接执行bindFunc(args),这时候其this默认绑定的是全局this,区别就在此处!res是构造函数创建出来的实例,而全局this可不是。因此我们只需要判断bindFunc中的this是否是curFn的实例,即可判断当前是否是正在进行new操作。 总结一下:执行构造函数时,bindFunc内部this绑定的是构造函数的实例;而其他情况并不是,故只需要判断this是否为实例即可判断是否正在执行构造函数
  3. 而实例和构造函数之间的联系在于原型,可以直接用instanceof判断,也可以通过 instance.__proto__ === constructor.prototype 判断

来看代码!

Function.prototype.myBind = function (that, ...rest) {
  that = that || globalThis
  const curFn = this // 不使用箭头函数则需要保存外层的this 使得内层能够访问得到
  const bindFunc = function (...args) {
    // 这边需要判断其是否用new绑定了 new的实现是创建一个对象,将对象原型设置为函数的原型,在对象上执行构造函数。
    // 因此要判断这个方法的调用方是否是new 只需要判断调用这个方法的对象的原型是不是构造函数!(this指向的就是调用该方法的对象)
    if (this.__proto__ === curFn.prototype) {
      curFn.call(this, ...rest, ...args) // 记得通过call把this绑定到实际的对象上 不然成了默认绑定
    } else {
      that.fn = curFn
      that.fn(...rest, ...args)
      delete that.fn
    }
  }
  // 为了new的时候能够获取到正确的原型,我们这边得把返回函数的原型也给配置成初始函数的原型。如果直接返回一个匿名函数而没有配置其原型,那么其原型是{},myNew中实例指向的原型也是{},和curFn.prototype无法匹配
  bindFunc.prototype = curFn.prototype
  return bindFunc
}

const myNew = (constructor, ...rest) => {
  const res = {}
  // 先进行原型绑定,再进行初始化,以便在对bind的函数进行构造时,其能够判断自己是在new从而放弃修改this指向 让this指向为实际的对象
  res.__proto__ = constructor.prototype

  // 创建一个对象用于执行构造函数 进行初始值设定
  res.fn = constructor
  const constructorRes = res.fn(...rest)
  delete res.fn
  return constructorRes instanceof Object ? constructorRes : res // 如果构造函数本身存在返回值,则使用返回值作为新对象
}

关键修改点:

myBind

  1. 使用this.__proto__ === curFn.prototype判断是否执行构造函数
  2. 如果执行构造函数,调用call将构造函数的指针指向this,在实例上执行构造函数(直接执行是默认绑定到全局哦)
  3. 修改返回函数原型,使之指向调用myBind的函数的原型,保证只修改调用函数指针不修改其他部分。反之,如果直接返回一个匿名函数而没有配置其原型,那么其原型是{},传递给myNewconstructor的原型也是{}myNew中实例指向的原型也是{},和curFn.prototype无法匹配

myNew

  1. 先进行原型绑定,再进行初始化,以便在执行res.fn时,fn内部能够通过this(指向res)的原型是否和调用函数的原型一致来判断是否在执行构造函数

测试一下

var foo = {
	value: "f",
};

function barConstuctor(param, param2) {
	this.value = "bar"
	console.log(this.value + param + param2);
}
const bindFunc = barConstuctor.myBind(foo, "myBind")
const instance = myNew(bindFunc, "param2")
console.log(foo) // { value: 'f' }
console.log(instance) // barConstuctor { value: 'bar' }

这下输出就正确了,成功修改了目标实例,避免影响myBind绑定的变量,大功告成!