ToolkitX
知识库工具箱

原型与继承

prototype, __proto__, class 语法糖

25min·高级

01. 原型链——JavaScript 的继承机制

JavaScript 的继承跟 Java/C++ 不一样——它不是基于类的,而是基于原型的。每个对象都有一个隐藏的 [[Prototype]] 属性(通过 __proto__ 或 Object.getPrototypeOf 访问),指向它的原型对象。 当你在对象上访问一个属性,JS 引擎先找对象本身有没有这个属性。没有就顺着 __proto__ 链往上层找,一直找到 null 为止。这条链就是原型链。 Object.prototype 是所有普通对象的最终原型。Function.prototype 是所有函数的原型。Array.prototype 是所有数组的原型,继承自 Object.prototype。 Array.prototype → Object.prototype → null。你定义的数组能用 push、map、filter 是因为它们定义在 Array.prototype 上,通过原型链传递到你数组里。
javascript
const arr = [1, 2, 3];
arr.push(4); // push 来自 Array.prototype
arr.toString(); // toString 来自 Object.prototype

// 验证原型链
arr.__proto__ === Array.prototype;         // true
Array.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null;       // true(链的终点)

02. 构造函数与 new 关键字

在 ES6 class 出来之前,JavaScript 的类是用构造函数模拟的。任何普通函数前面加 new 调用,就变成了构造函数。 new 做了四件事: 1. 创建一个空对象{} 2. 把空对象的原型指向构造函数的 prototype(obj.__proto__ = Fn.prototype) 3. 以这个空对象为 this 调用构造函数(Fn.call(obj)) 4. 如果构造函数没返回对象,就返回这个空对象 所以构造函数的 prototype 属性就是用它创建出来的实例的原型。注意只有函数的 prototype 属性跟对象的 __proto__ 是两个东西——prototype 是函数才有的属性,__proto__ 是所有对象都有的。
javascript
// 构造函数
function Person(name) {
  this.name = name;
}

Person.prototype.sayHi = function() {
  console.log('Hi, I am ' + this.name);
};

const alice = new Person('Alice');
alice.sayHi(); // Hi, I am Alice

// new 的模拟实现
function myNew(Fn, ...args) {
  const obj = Object.create(Fn.prototype);
  const result = Fn.apply(obj, args);
  return result instanceof Object ? result : obj;
}

03. ES6 class——语法糖

ES6 引入了 class 关键字,但它本质上是原型继承的语法糖——底层还是基于原型的。class 让代码更直观,也加了几个只有 class 才有的特性。 constructor 是构造函数。extends 设置原型链(子类的 prototype.__proto__ 指向父类的 prototype)。super 调用父类构造函数。 class 跟构造函数的不同:class 必须用 new 调用(否则报错);class 的方法不可枚举(默认不可用 for...in 遍历出来);class 内部默认是严格模式。 静态方法(static)定义在类本身而不是原型上,相当于 Person.staticMethod。实例访问不到。
javascript
// class 语法
class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHi() {
    console.log('Hi, I am ' + this.name);
  }
  
  static create(name) {
    return new Person(name);
  }
}

const bob = new Person('Bob');
bob.sayHi();

// extends 继承
class Student extends Person {
  constructor(name, grade) {
    super(name);  // 调用父类构造函数
    this.grade = grade;
  }
  
  study() {
    console.log(this.name + ' is studying');
  }
}
虽然有了 class 语法,但理解背后的原型机制仍然重要。debug 时看对象结构、理解为什么方法在所有实例间共享,都跟原型有关。

04. prototype vs __proto__ vs Object.create

这三个东西容易搞混,一张图搞清楚: prototype——只有函数对象有。它是一个普通对象,当你用 new 调用这个函数时,新对象的 __proto__ 指向函数的 prototype。 __proto__——每个对象都有(除了 null),指向它的原型。这是原型链的物理连接。 Object.create(proto)——创建一个新对象,手动指定它的原型。这是最纯粹的原型继承方式,不需要构造函数。 它们的关系:实例.__proto__ === 构造函数.prototype。函数的 __proto__ 指向 Function.prototype。
javascript
// 三种继承方式的对比

// 1. 构造函数 + prototype
function Animal(name) { this.name = name; }
Animal.prototype.eat = function() { console.log('eat'); };
const dog = new Animal('dog');

// 2. Object.create
const animalProto = { eat() { console.log('eat'); } };
const cat = Object.create(animalProto);
cat.name = 'cat';

// 3. class
class AnimalClass {
  constructor(name) { this.name = name; }
  eat() { console.log('eat'); }
}
const bird = new AnimalClass('bird');

05. 原型链的面试陷阱

原型链相关的高频面试题: 1. instanceof——检查构造函数.prototype 是否在某个对象的原型链上。obj instanceof Fn 就是看 Fn.prototype 在不在 obj.__proto__ 链上。 2. hasOwnProperty——检查属性是在对象自身上还是在原型链上。for...in 会遍历原型链上的可枚举属性,配合 hasOwnProperty 过滤。 3. 修改内置原型——给 Array.prototype 加自定义方法(如 Array.prototype.lastItem)。虽然可以做到但不推荐——可能跟未来的 JS 标准冲突或者覆盖别人的代码。 4. 各种方法在哪个 prototype 上——toString 在 Object.prototype 上,所有对象都能用。call/apply/bind 在 Function.prototype 上,只有函数能用。
javascript
// instanceof
function Foo() {}
const f = new Foo();
f instanceof Foo;  // true
f instanceof Object;  // true(Foo.prototype 的最顶端是 Object.prototype)

// hasOwnProperty
const obj = Object.create({ inherited: true });
obj.own = false;
obj.hasOwnProperty('own');  // true
obj.hasOwnProperty('inherited');  // false

// 检查属性在不在自身
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log('own:', key);
  }
}
不要扩展内置原型——Array.prototype.myMethod。你的代码和第三方库、未来的 JS 标准都看上了同一个 Array.prototype。

知识测验

1/5正确 0

JavaScript 的继承是基于什么的?

下一节

事件循环深入

下一节