前端知库

怎样手写 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 一样的结果。

JavaScript
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 不关心对象是怎么创建的,只顺着原型链往上找。

JavaScript
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 处理原型,再自己调用构造函数。