JavaScript 一面:原型链、继承与 instanceof

原型和继承几乎是每一份前端面试题里都会出现的老面孔,这一篇尽量用“画图 + 几个典型问题”的方式,把原型链相关的考点串起来。

在 JS 里,几乎每个对象背后都有一根“指向它父辈”的指针:

  • 对象的内部属性 [[Prototype]](大多数环境下可以通过 __proto__ 访问)指向它的原型对象;
  • 构造函数的 prototype 属性指向“实例的原型对象”;
  • 原型对象本身也是对象,也有自己的 [[Prototype]],一直往上,最后指向 Object.prototype,再往上就是 null

可以用一个简化的关系图描述:

1function Foo() {}
2
3const foo = new Foo();
4
5foo.__proto__ === Foo.prototype
6Foo.prototype.__proto__ === Object.prototype
7Object.prototype.__proto__ === null

这条一层层向上的链,就是所谓的“原型链”。
当访问 foo.someProp 时,引擎的查找路径是:

  1. foo 自身有没有 someProp
  2. 没有就去 foo.__proto__(也就是 Foo.prototype)上找;
  3. 还没有就继续沿着 __proto__ 往上找,直到 Object.prototypenull

理解了这条查找路径,后面很多原型链相关的题就自然了。

instanceof 是一面常客,很多题都围绕它做文章。
它的语义可以简化成一句话:

foo instanceof Foo 判断的是:Foo.prototype 是否出现在 foo 的原型链上。

用伪代码来描述大致逻辑:

 1function myInstanceOf(obj, Ctor) {
 2  if (obj == null || (typeof obj !== "object" && typeof obj !== "function")) {
 3    return false;
 4  }
 5
 6  let proto = Object.getPrototypeOf(obj);
 7  const target = Ctor.prototype;
 8
 9  while (proto) {
10    if (proto === target) return true;
11    proto = Object.getPrototypeOf(proto);
12  }
13
14  return false;
15}

面试时如果被问到“instanceof 的原理能不能简单说一下”,
只要说明“沿着左边对象的原型链往上找,看是否能遇到右边构造函数的 prototype”,就足够了。

在一面里,有时会要求你“手写一个简单的继承方案”,常见考点包括:

  • 基于原型的继承:
    • 子类型的 prototype 指向父类型的实例或原型;
    • 保证 constructor 指回子类型本身。

例如一个常见的组合继承写法:

 1function Animal(name) {
 2  this.name = name;
 3}
 4Animal.prototype.sayHi = function () {
 5  console.log("Hi, I'm " + this.name);
 6};
 7
 8function Dog(name, color) {
 9  Animal.call(this, name); // 借用构造函数,初始化实例属性
10  this.color = color;
11}
12
13Dog.prototype = Object.create(Animal.prototype); // 原型链继承
14Dog.prototype.constructor = Dog;                 // 修正 constructor
15
16Dog.prototype.bark = function () {
17  console.log("Woof!");
18};

class 语法本质上是对这种模式的语法糖,理解底层这一套之后,看 class 的行为会更自然。

一些典型的考法(题目不一定一模一样,但思路类似):

  • 判断输出与等式真假:
1function Foo() {}
2const foo = new Foo();
3
4console.log(foo.__proto__ === Foo.prototype);      // ?
5console.log(Foo.prototype.__proto__ === Object.prototype); // ?
6console.log(foo.__proto__.__proto__ === Object.prototype); // ?
7console.log(foo instanceof Foo);                   // ?
8console.log(foo instanceof Object);                // ?
  • 判断 instanceof 在边界情况下的表现:
1console.log([] instanceof Array);   // true
2console.log([] instanceof Object);  // true
3console.log(Object.create(null) instanceof Object); // ?
  • 混合 class 与函数构造器的继承题,考察是否理解 extends 背后的原型链形态。

面对这类题,建议不要硬背答案,而是养成“从对象往上沿原型链走一遍”的习惯。

1function Foo() {}
2const foo = new Foo();
3
4console.log(foo.__proto__ === Foo.prototype);             // ?
5console.log(Foo.prototype.__proto__ === Object.prototype); // ?
6console.log(foo.__proto__.__proto__ === Object.prototype); // ?
7console.log(foo instanceof Foo);                          // ?
8console.log(foo instanceof Object);                       // ?

参考答案:

  • foo.__proto__ === Foo.prototypetrue
    • 实例的内部原型指向构造函数的 prototype
  • Foo.prototype.__proto__ === Object.prototypetrue
    • 自定义构造函数的原型对象默认继承自 Object.prototype
  • foo.__proto__.__proto__ === Object.prototypetrue
    • 沿着原型链再往上一层就是 Object.prototype
  • foo instanceof Footrue
    • Foo.prototypefoo 的原型链上。
  • foo instanceof Objecttrue
    • Object.prototype 也在 foo 的原型链上。

这里可以顺势再强调一句:instanceof 本质就是“右侧 prototype 是否出现在左侧对象的原型链上”。

1console.log([] instanceof Array);                // ?
2console.log([] instanceof Object);               // ?
3console.log(Object.create(null) instanceof Object); // ?

参考答案:

  • [] instanceof Arraytrue
    • 数组实例的原型链上包含 Array.prototype
  • [] instanceof Objecttrue
    • Array.prototype.__proto__ === Object.prototype,因此 Object.prototype 也在原型链上。
  • Object.create(null) instanceof Objectfalse
    • 这个对象的原型是 null,原型链上没有 Object.prototype

这道题的目的,是考你对“原型链起点可以不是 Object.prototype”这一点有没有概念。

题目可能会说:用 ES5 写一个 Animal / Dog 的继承关系,让 Dog 继承 AnimalsayHi 方法。

可以给出类似这样的代码并简要说明:

 1function Animal(name) {
 2  this.name = name;
 3}
 4Animal.prototype.sayHi = function () {
 5  console.log("Hi, I'm " + this.name);
 6};
 7
 8function Dog(name, color) {
 9  Animal.call(this, name); // 借用构造函数,复用初始化逻辑
10  this.color = color;
11}
12
13Dog.prototype = Object.create(Animal.prototype); // 建立原型链
14Dog.prototype.constructor = Dog;                 // 修正 constructor 指针
15
16Dog.prototype.bark = function () {
17  console.log("Woof!");
18};

关键解释点:

  • Animal.call(this, name) 继承实例属性;
  • Object.create(Animal.prototype)Dog 实例在原型链上能访问到 Animal.prototype 上的方法;
  • 最后记得把 constructor 指回 Dog 本身。

在日常项目里,很多人很少直接操作 __proto__prototype
但原型相关的概念在这些地方仍然很重要:

  • 理解内建对象的行为:Array.prototypeMap.prototype 等;
  • 看懂一些框架/库里对原型的魔改(如给原型加方法);
  • 排查某些“方法怎么突然没了/被覆盖了”的诡异 bug。

在一面回答原型链问题时,如果能顺带提一下这些工程上的例子,比只讲裸概念更有说服力。

可以用一个简单的结构来组织回答:

  • 先用那条“foo.__proto__ → Foo.prototype → Object.prototype → null”的链,说明什么是原型链;
  • 接着用一句话讲清 instanceof 的原理,并简单说一下手写实现思路;
  • 再补一个简短的继承例子(函数构造器 + Object.create);
  • 最后用一两句带到工程实践里的用处。

这样既覆盖了高频考点,也能展示你不只是“会做题”,而是确实理解了这一块的运行机制。