前言
- 前端工程师应该明白,new 关键字到底做了什么事情。当然这也是一道常见的手撕面试题。
构造函数返回值不同
- 在手撕 new 关键字前,我们先来看下在构造函数返回值不同时,所创建出来的实例对象也会有所不同。
function Person(name, age) {
this.name = name;
this.age = age;
// 情况一:返回对象
return {};
// 情况二: 返回数组
// return []
// 情况三: 返回函数
// return function () {}
// 情况四: 返回 undefined
// return undefined
// 情况五: 返回 null
// return null;
}
Person.prototype.say = function () {
console.log("说话了");
};
let p = new Person("peiqi", 22);
console.log(p); // 此处输出的实例对象 p 会受 Person 构造函数的返回值影响
// p.say();
-
上述代码中,
Person 构造函数返回值及实例对象 p的对应关系如下情况:-
- 返回对象:
实例对象 p为该对象
- 返回对象:
-
- 返回数组:
实例对象 p为该数组
- 返回数组:
-
- 返回函数:
实例对象 p为该函数
- 返回函数:
-
- 返回 undefined:
实例对象 p不变, 也就是跟没有 return 语句的结果一致
- 返回 undefined:
-
- 返回 null:
实例对象 p不变, 也就是跟没有 return 语句的结果一致
- 返回 null:
-
手撕 new 关键字
- 了解了上面的内容后, 下面我们就开始手写实现 new 关键字, 其实 new 关键字做了这些事情:
-
- 首先创建一个空对象, 这个对象将会作为执行构造函数之后返回的对象实例。
-
- 使上面创建的空对象的原型(即
__proto__) 指向构造函数的prototype属性, 这样实例对象就可以调用原型对象上的方法。
- 使上面创建的空对象的原型(即
-
- 调用构造函数,同时使用
apply()修改构造函数中的 this 指向新创建的空对象,这样就可以把属性挂载到空对象上。
- 调用构造函数,同时使用
-
- 最后判断构造函数执行的结果, 如果结果是引用数据类型(如对象、数组或函数), 那么就返回该构造函数的执行结果。否则就返回第一步所创建的空对象。
-
- 由于 new 是 JavaScript 的关键字, 因此我们不能直接将其覆盖, 但可以通过实现一个
MockNew()来进行模拟。我们对MockNew()预期的使用方式如下:
function Person(name, age) {
this.name = name;
this.age = age;
}
const p = MockNew(Person, "peiqi", 22);
console.log(p); // 预期输出: { name: 'peiqi', age: 22 }
方式一
根据预期的使用方式, 我们可以实现 MockNew, 代码如下:
function MockNew() {
// 取出 arguments 的第一个参数, 即目标构造函数
let Constructor = [].shift.call(arguments);
// 当然你也可以使用如下方式, 其实是等效的, 都是把类数组 arguments 转为数组, 同时取出第一个参数
// let Constructor = Array.prototype.shift.call(arguments)
// Object.create() 返回一个空对象 obj, 且会使 obj.__proto__ 指向 Object.create() 的第一个参数
// 即实现了 obj.__proto__ === Constructor.prototype
let obj = Object.create(Constructor.prototype);
// 执行构造函数, 使用 apply() 修改构造函数中 this 指向 obj, 并且得到构造函数返回结果
let res = Constructor.apply(obj, arguments);
// 如果构造函数执行后, 返回结果是复杂数据类型(如对象、数组或函数), 则直接将该结果返回, 否则返回 obj 对象
return res instanceof Object ? res : obj;
}
方式二
除了上面一种实现方式以外, 还有如下的实现方式,但其实本质都是一样:
function MockNew(Constructor, ...args) {
let obj = {};
obj.__proto__ = Constructor.prototype;
let res = Constructor.apply(obj, args);
// 只要是对象、数组或函数类型,那么都是返回 res
// 注意该行代码一定要想明白:(res !== 'null' && typeof res === 'object') || typeof res === 'function' 表示就是引用类型
if (
(res !== "null" && typeof res === "object") ||
typeof res === "function"
) {
return res;
}
return obj;
}
图解
如果你对上面的代码还是想不明白, 你可以参考如下的图解, 相信结合图解, 你会更加清晰:
最后补充
- 最后补充一些小知识点:
-
__proto__是隐式原型,所以当某些方法定义在原型上的时候,我们直接调用,不用写__proto__, 内部会帮我们加上。
-
- 当通过
对象.方法的方式调用对象上的方法时,会先在自身找,找到则直接使用,没有的话则去__proto__上找
- 当通过
-