怎样手写 new 和 instanceof?
new 先创建一个原型指向 Fn.prototype 的空对象,再把它当作 this 调用构造函数,构造函数返回对象就用返回值,否则用新对象。instanceof 沿着左边对象的原型链往上找,看能不能遇到右边的 prototype。
简答
写
new Person('小明')时,引擎做了四件事:创建一个空对象;把这个对象的原型指向Person.prototype;把它当作this调用Person;如果Person返回了一个对象,就用这个返回值,否则返回刚才创建的对象。手写时用Object.create(Fn.prototype)一步完成前两件事,再用apply调用构造函数。
a instanceof B判断的是B.prototype在不在a的原型链上。手写就是一个循环:从Object.getPrototypeOf(a)开始往上走,遇到B.prototype返回true,走到null还没遇到就返回false。这两个手写版本都是教学用的。真正的
new还会检查目标能不能被构造,比如箭头函数和 class 有不同的规则;真正的instanceof会先看右边有没有Symbol.hasInstance。要注意,
instanceof依赖原型链,所以跨 iframe 的数组会判断失败,原型也能被手动改掉。判断数组用Array.isArray更可靠。
手写 new
先看 new 的完整过程,再对照代码。下面的 myNew 接收一个构造函数和参数,返回和 new 一样的结果。
function myNew(Fn, ...args) {
if (typeof Fn !== 'function') throw new TypeError('Fn 必须是函数');
// prototype 不是对象时,新对象的原型退回 Object.prototype,和原生 new 一致
const proto = Fn.prototype !== null && typeof Fn.prototype === 'object'
? Fn.prototype
: Object.prototype;
const instance = Object.create(proto);
const result = Fn.apply(instance, args);
const isObject = result !== null && (typeof result === 'object' || typeof result === 'function');
return isObject ? result : instance;
}
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `你好,我是${this.name}`;
};
function Box() {
this.value = 1;
return { value: 2 };
}
function Num() {
this.value = 1;
return 42;
}
const p = myNew(Person, '小明');
console.log(p.greet()); // 你好,我是小明
console.log(p instanceof Person); // true
console.log(myNew(Box).value); // 2
console.log(myNew(Num).value); // 1
p.greet() 能调用,是因为 Object.create(proto) 创建的对象,原型就是 Person.prototype,greet 定义在那里。Fn.apply(instance, args) 让构造函数里的 this 指向这个新对象,所以 this.name = name 写进的是 p 自己。
为什么 Box 得到的是 2,Num 得到的是 1?因为构造函数返回值的规则只认对象。Box 返回了一个对象,这个对象就替代了新创建的实例;Num 返回的是数字 42,原始值会被忽略,结果还是新创建的那个实例。判断时函数也算对象,所以 typeof result === 'function' 也要算进去。
手写 instanceof
instanceof 不关心对象是怎么创建的,只顺着原型链往上找。
function myInstanceof(value, Ctor) {
if (typeof Ctor !== 'function') throw new TypeError('右侧必须是函数');
// 原始值没有原型链,原生 instanceof 对它们直接返回 false
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
return false;
}
const target = Ctor.prototype;
let proto = Object.getPrototypeOf(value);
while (proto !== null) {
if (proto === target) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(myInstanceof(dog, Dog)); // true
console.log(myInstanceof(dog, Animal)); // true
console.log(myInstanceof(dog, Array)); // false
console.log(myInstanceof(1, Number)); // false
console.log(myInstanceof(Object.create(null), Object)); // false
dog 的原型链是 Dog.prototype → Animal.prototype → Object.prototype → null。找 Animal 时,第一步遇到 Dog.prototype 不是,第二步遇到 Animal.prototype,返回 true。
myInstanceof(1, Number) 为什么是 false?数字 1 是原始值,原生的 1 instanceof Number 也是 false,它不会先把 1 包装成对象。Object.create(null) 创建的对象没有原型,循环一开始就走到 null,所以连 Object 都不是。
手写版本和原生有哪些差距
手写的 myNew 只检查了参数是不是函数。原生 new 检查的是函数能不能被构造:箭头函数、对象方法简写都不能 new,会抛 TypeError,myNew 却会照常调用它们。反过来,class 必须用 new 调用,Fn.apply 调用 class 会直接抛错,所以 myNew 不能处理 class。要完整模拟,可以改用 Reflect.construct(Fn, args),但那样就等于直接调用了原生的构造流程。
手写的 myInstanceof 没有处理 Symbol.hasInstance。原生 instanceof 会先看右边有没有这个方法,有就调用它决定结果,所以类可以自定义 instanceof 的判断逻辑。
常见追问
为什么跨 iframe 的数组 instanceof Array 是 false?
每个 iframe 有自己的一套全局对象,也就有自己的
Array.prototype。从别的 iframe 传过来的数组,原型链上是那个 iframe 的Array.prototype,跟当前页面的不是同一个对象,所以找不到。判断数组用Array.isArray,它不依赖原型链。
构造函数里 return 一个原始值会怎样?
会被忽略,
new还是返回新创建的实例。只有返回对象(包括函数)时,才会替代新实例。
Object.create 和 new 有什么区别?
Object.create(proto)只创建一个原型是proto的空对象,不调用任何构造函数。new Fn()除了设置原型,还会调用Fn初始化这个对象。所以手写new时用Object.create处理原型,再自己调用构造函数。