Skip to content

原型与继承

一、核心概念

JavaScript 是基于原型的语言,而不是基于类的语言(虽然 ES6 引入了 class,但本质上是语法糖)。理解原型是掌握 JS 面向对象编程的基础。

1. 三个关键属性

属性宿主指向描述
prototype函数原型对象只有函数才有此属性。它指向该函数创建的实例的原型。
__proto__对象原型对象所有对象(除 null)都有。指向创建该对象的构造函数的 prototype
constructor原型对象构造函数原型对象通过此属性指回其构造函数。

2. 关系图解

javascript
function Foo() {}
const f1 = new Foo();

console.log(f1.__proto__ === Foo.prototype); // true
console.log(Foo.prototype.constructor === Foo); // true
// 实例本身没有 constructor,沿着原型链找到 Foo.prototype.constructor
console.log(f1.constructor === Foo); // true

注意: __proto__ 是非标准属性(虽然浏览器广泛支持)。推荐使用 Object.getPrototypeOf(obj)Object.setPrototypeOf(obj, proto)(后者有性能损耗,建议 Object.create)。

二、原型链

1. 查找机制

当访问对象的一个属性时,JavaScript 引擎会按照以下顺序查找:

  1. 自身属性: 检查对象自身是否有该属性。
  2. 原型链: 如果没有,则沿着 __proto__ 指向的原型对象查找。
  3. 层层向上: 重复步骤 2,直到找到或到达原型链尽头。
  4. 尽头: 原型链的顶端是 Object.prototypeObject.prototype.__proto__null

2. 属性遮蔽 (Shadowing)

如果在实例上设置了与原型同名的属性,实例属性会遮蔽原型属性,而不会修改原型属性。

javascript
function Person() {}
Person.prototype.name = 'Origin';

const p1 = new Person();
console.log(p1.name); // 'Origin' (来自原型)

p1.name = 'Own'; // 在实例上添加属性
console.log(p1.name); // 'Own' (遮蔽原型)
console.log(p1.__proto__.name); // 'Origin' (原型未变)

三、创建对象与原型

1. new 操作符原理

new 关键字背后做了 4 件事:

  1. 创建一个空的简单 JavaScript 对象(即 {})。
  2. 将这个新对象的 __proto__ 链接到构造函数的 prototype
  3. 将构造函数中的 this 绑定到新对象,并执行构造函数。
  4. 如果构造函数返回非空对象,则返回该对象;否则返回新对象。

手写 new:

javascript
function myNew(Constructor, ...args) {
  // 1. 创建新对象,关联原型
  const obj = Object.create(Constructor.prototype);
  // 2. 绑定 this 执行构造函数
  const result = Constructor.apply(obj, args);
  // 3. 处理返回值
  return (result && typeof result === 'object') ? result : obj;
}

四、继承方式详解

1. 原型链继承

将父类的实例作为子类的原型。

javascript
Child.prototype = new Parent();
  • 缺点1: (严重) 引用类型的属性被所有子类实例共享。
  • 缺点2: 创建子类实例时,无法向父类构造函数传参。

2. 构造函数继承 (借助 call)

在子类构造函数中调用父类构造函数。

javascript
function Child() {
  Parent.call(this, ...arguments);
}
  • 优点: 解决了引用属性共享问题;可以传参。
  • 缺点: 只能继承父类实例属性,无法继承父类原型上的方法。每次都要在构造函数中定义方法,无法复用。

3. 组合继承 (常用)

结合上述两者。

javascript
function Child(name) {
  Parent.call(this, name); // 第二次调用 Parent
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child;
  • 优点: 融合了前两者的优点。
  • 缺点: 调用了两次父类构造函数,产生两份实例属性(一份在实例上,一份在原型上)。

4. 寄生组合式继承 (推荐 ES5)

只调用一次父类构造函数,通过 Object.create 继承原型。这是最成熟的 ES5 继承方案。

javascript
function inheritPrototype(subType, superType) {
  // 创建父类原型的一个副本
  const prototype = Object.create(superType.prototype);
  // 修正 constructor 指向
  prototype.constructor = subType;
  // 将副本赋值给子类原型
  subType.prototype = prototype;
}

function Child(name) {
  Parent.call(this, name); // 只用这一种方式继承实例属性
}

inheritPrototype(Child, Parent);

5. ES6 Class 继承 (现代标准)

语法糖,底层逻辑与寄生组合式继承类似。

javascript
class Parent {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log('Hello');
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 必须先调用 super()
    this.age = age;
  }
}
  • 区别: Class 内部定义的方法是不可枚举的;Class 必须使用 new 调用;不存在变量提升。

五、类型检测

1. instanceof 原理

检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

手写 instanceof:

javascript
function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left);
  const prototype = right.prototype;
  
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

2. isPrototypeOf

Object.prototype.isPrototypeOf() 用于测试一个对象是否存在于另一个对象的原型链上。

javascript
Parent.prototype.isPrototypeOf(child); // true

六、面试高频题

Q1: 解释一下原型链?

参考答案: JavaScript 中每个对象都有一个 __proto__ 属性指向其原型对象,原型对象也有自己的 __proto__,层层向上直到 Object.prototype(其 __proto__ 为 null)。当访问对象的属性时,JS 引擎会先在这个对象自身上查找,如果没有找到,就会沿着 __proto__ 链向上查找,直到找到该属性或到达链尾。这条链就叫原型链。

Q2: 箭头函数有原型吗?

参考答案: 箭头函数没有 prototype 属性,也不能作为构造函数(使用 new 会报错)。它也没有自己的 this,而是捕获定义时上下文的 this

Q3: Object.create(null) 创建了什么?

参考答案: 创建了一个没有原型的空对象。它不继承 Object.prototype 上的任何属性或方法(如 toString, hasOwnProperty)。常用于创建纯净的字典对象,防止原型污染。


上一篇: 作用域与闭包 | 下一篇: 异步编程

前端面试知识库