A

原型、原型链与继承

2025-12-09 16:24

要真正理解 JavaScript 的面向对象体系,必须先理解它的“原型(Prototype)”。原型决定了对象如何共享方法、如何查找属性,也决定了 JavaScript 的继承方式为何与传统面向对象语言完全不同。

与 Java、C++ 等基于类的语言采用“复制式继承”不同,JavaScript 使用的是 基于原型的委托式继承。每个对象都可以通过原型链向上委托,形成灵活而动态的继承结构。

原型与原型链:JavaScript OOP 的基石

每个 JavaScript 对象都有一个内部属性 [[Prototype]](可通过 __proto__ 访问),它指向另一个对象——即该对象的“原型”。当访问一个对象的属性时,若自身没有,引擎会沿着原型链向上查找,直到 null

JS
const arr = []; console.log(arr.__proto__ === Array.prototype); // true console.log(arr.__proto__.__proto__ === Object.prototype); // true console.log(arr.__proto__.__proto__.__proto__ === null); // true

这条从实例 → 构造函数的 prototype → 更上层原型 → null 的链条,就是原型链。

这就是为什么我们可以通过实例对象访问原型中的属性和方法。看例子:

JS
const str = "hello world"; const arr = []; const obj = {}; console.log(str.split(" ")); // ['hello', 'world'] console.log(arr.push(1)); // [1] console.log(obj.toString()); // '[object Object]'

明明没有splitpushtoString这些方法,但是通过实例对象访问原型中的属和方法,是因为原型链。它们同时被各自的构造函数创建的实例对象,所以可以通过__proto__属性访问原型对象方法。其中的原始值没有原型链,但是实际操作情况 javascript 中的自动装箱和拆箱机制。

每一个 JavaScript 对象(除了 null )都具有的一个属性,叫proto,这个属性会指向该对象的原型。

原型的来源:构造函数与 prototype

既然每个对象都有 proto 指向其原型,那么下一个问题自然是:原型是怎么来的?

答案是: 来自构造函数的 prototype 属性。

JavaScript 中,每当你定义一个函数时,JS 引擎会自动为该函数创建一个 prototype 对象,并附带一个指向构造函数本身的 constructor 属性。

JS
function Person() {} Person.prototype.name = "Arafat"; console.log(Person.prototype);
图片加载中...
图表示意图
可以看到 prototype 中除了添加了 name 属性外,还添加了一个 constructor 属性,该属性指向了 Person 函数本身和 __proto__ 属性指向了 Object.prototype

什么是构造函数

构造函数是用来创建对象实例的函数,通常通过 new 调用。例如:

JS
function Person(name) { this.name = name; } const p1 = new Person("Arafat"); console.log(p1.name); // Arafat

与普通函数的区别就在于是否使用 new 调用:

  • new 会创建一个新对象,并把 this 指向它
  • 普通函数调用不会创建新对象,也不会设置 this 指向实例

原型链的顶层:为什么 Object.prototype.proto 是 null

在介绍原型链时,我们知道所有对象在查找属性时,都沿着 [[Prototype]]__proto__)向上委托,最终会到达一个终点。而这个终点就是:

JS
Object.prototype.__proto__ === null; // true

为什么原型链的重点是 Null ?原因很简单:JavaScript 必须有一个无法继续委托的终点对象,以避免原型链无限上溯。这个对象就是所有对象的根——Object.prototype。 也就是说从语言设计的角度来看:

  • 所有普通对象都继承自 Object.prototype
  • 但为了避免形成循环或无限委托,根对象的原型必须是 null
  • null 表示“没有原型”,从此处终止查找

理解 instanceof:基于原型链的类型判断机制

instanceof 是 JavaScript 中用于判断一个对象是否由某个构造函数创建的运算符。但它的判断逻辑不是检查“构造函数”,也不是检查“类型字符串”,而是检查原型链

instanceof 的判断规则是当执行:

JAVASCRIPT
a instanceof B; // 判断构造函数 B 的 prototype 是否出现在 a 的原型链上。

内部逻辑是:

  • 取出 B.prototype
  • 沿着 a 的 __proto__(即 [[Prototype]])向上查找
  • 如果某一层原型 === B.prototype,则返回 true
  • 查到顶层 null 还没找到,则返回 false 我们也可以手动实现:
JAVASCRIPT
function myInstanceof(left, right) { let proto = left.__proto__; while (proto) { if (proto === right.prototype) { return true; } proto = proto.__proto__; // 向上遍历原型链 } return false; } function Animal() {} function Dog() {} Dog.prototype = new Animal(); const dog = new Dog(); console.log(myInstanceof(dog, Dog)); // true console.log(myInstanceof(dog, Animal)); // true(因为 Dog.prototype = new Animal()) console.log(myInstanceof(dog, Object)); // true(所有对象最终继承自 Object)

这就说明 instanceof 是通过原型链来判断的。

什么是 Symbol.hasInstance

Symbol.hasInstance 是 ECMAScript 规范中用于定义 instanceof 运算符行为的一个内置符号。当我们执行:

JS
x instanceof C // 等价于 C[Symbol.hasInstance](x)

换句话说,instanceof 的最终判定逻辑是由构造函数的 Symbol.hasInstance 方法决定的。只要你重新定义该方法,就可以改变 instanceof 的行为。

也就是说如果不覆盖 Symbol.hasInstance,那么默认逻辑就是按照原型链去比对。ES6 引入 Symbol.hasInstance 就是为了让 instanceof 具有灵活性和可定制性。比如模拟接口检查:

JS
const Interface = { [Symbol.hasInstance](obj) { return obj && typeof obj.run === 'function'; } }; console.log({ run: () => {} } instanceof Interface); // true

创建对象的方法

JavaScript 中,创建对象的方式主要有三种,每种方式适用场景和特点不同:

构造函数 + new

通过构造函数创建对象是 JavaScript 面向对象体系的核心方式。使用 new 调用构造函数时,会执行以下操作:

  1. 创建一个空对象 {}
  2. 将这个对象的 __proto__ 指向构造函数的 prototype
  3. 执行构造函数,将 this 绑定到新对象,并初始化属性
  4. 返回对象(如果构造函数返回了对象,则返回该对象;否则返回新创建的对象) 示例:
JAVASCRIPT
function myNew(constructor, ...args) { // 1. 创建一个新对象,原型指向 constructor.prototype const obj = Object.create(constructor.prototype); // 2. 将构造函数作用域绑定到 obj 并执行 const result = constructor.apply(obj, args); // 3. 如果构造函数返回对象,则返回它,否则返回新创建的 obj return result instanceof Object ? result : obj }

通过这种方式创建的对象,会自动形成原型链,是实现继承和共享方法的基础。

Object.create(proto)

Object.create 可以创建一个新的对象,并指定其原型为 proto 。这种方式通常用于原型式继承,可以直接将已有对象作为新对象的原型。示例:

JAVASCRIPT
const parent = { name: 'Parent' }; const child = Object.create(parent); child.age = 10; console.log(child.name); // "Parent",通过原型链访问 console.log(child.__proto__ === parent); // true

Object.create 不会执行构造函数,仅建立原型关系,因此适合单纯的原型继承或对象克隆。

字面量表示法

最常见和最简便的方式是使用字面量:

JAVASCRIPT
const obj = {}; // 对象 Object.prototype const arr = []; // 数组 Array.prototype const func = () => {}; // 函数 Function.prototype

这种方式主要用于快速创建对象或函数,不涉及构造函数和继承逻辑。

继承机制

JavaScript 的继承与传统类继承不同,它依赖原型链。创建子类实例时,子类可以通过原型链访问父类的属性和方法。

原型继承

JAVASCRIPT
function Parent() { this.name = 'Parent'; } Parent.prototype.sayHello = function() { console.log('Hello, ' + this.name); }; function Child() {} Child.prototype = new Parent(); // 原型继承 Child.prototype.constructor = Child; const child = new Child(); child.sayHello(); // Hello, Parent

优点:

  • 方法复用:所有实例共享父类原型方法,节省内存
  • 继承实现简单

缺点:

  • 引用类型属性共享:数组、对象会被所有实例共享,容易出现副作用
  • 无法给父类构造函数传参初始化属性
  • 实例属性无法继承,需要手动在子类构造函数中定义

适用场景:父类属性是原始类型,方法复用是重点,不需要传参初始化

盗用构造函数继承

JS
function Parent(name) { this.name = name; this.items = []; } function Child(name) { Parent.call(this, name); // 盗用构造函数 } const c1 = new Child('c1'); const c2 = new Child('c2'); c1.items.push('item1'); console.log(c2.items); // [] —— 实例独立 console.log(c1.name, c2.name); // c1 c2

优点:

  • 实例属性独立,每个实例的引用类型不会共享
  • 支持传参初始化父类属性
  • 简单明了,不依赖原型链

缺点:

  • 父类原型方法无法继承,每个子类实例都需要单独定义方法,占用内存
  • 不支持多态,无法访问父类原型方法

**适用场景:**父类有引用类型属性,需要独立实例;不需要继承父类方法

组合继承

JS
function Parent(name) { this.name = name; this.items = []; } Parent.prototype.say = function() { console.log('Hello'); }; function Child(name) { Parent.call(this, name); // 继承实例属性 } Child.prototype = new Parent(); // 继承方法 Child.prototype.constructor = Child; const c1 = new Child('c1'); const c2 = new Child('c2'); c1.items.push('item1'); console.log(c2.items); // [] —— 实例独立 c1.say(); // Hello

优点:

  • 实例属性独立
  • 方法可以复用
  • 支持传参

缺点:

  • 父类构造函数调用两次,增加性能开销
  • 原型对象可能存在冗余实例属性

适用场景:需要同时继承属性和方法,实例独立性重要,性能要求一般

寄生式继承

JAVASCRIPT
function createChild(original) { const clone = Object.create(original); clone.sayHi = function() { console.log('Hi'); }; return clone; } const parent = { name: 'Parent' }; const child = createChild(parent); child.sayHi(); // Hi console.log(child.name); // Parent

优点:

  • 可以增强对象功能
  • 不调用构造函数,无副作用

缺点:

  • 方法不能复用,每个实例都重新创建
  • 引用类型属性共享

适用场景:对象功能增强、临时继承,不在乎方法复用和内存占用

寄生组合继承(最优 ES5 继承方式)

JAVASCRIPT
function Parent(name) { this.name = name; this.items = []; } Parent.prototype.say = function() { console.log('Hello'); }; function Child(name) { Parent.call(this, name); // 继承实例属性(不共享) } // 核心:用 Object.create 继承原型,而不是 new Parent() Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; const c1 = new Child('c1'); const c2 = new Child('c2'); c1.items.push('item1'); console.log(c2.items); // [] —— 实例完全独立 c1.say(); // Hello

优点:

  • 实例属性完全独立(不会共享),这是原型链继承的最大痛点,而寄生组合继承完全解决。
  • 原型方法只创建一份(节省内存),这是盗用构造函数继承无法做到的。
  • 父类构造函数只调用一次(性能最佳),组合继承的原型链部分会多调用一次父构造函数,导致性能浪费、生成无意义属性。
  • 继承关系完全正确(保持原型链结构)
  • 支持向父类传参(灵活性强)
  • 可以轻松扩展子类方法,不影响父类

缺点

  • 实现相对复杂,代码冗长,但是是最优解所有这点缺点不算什么

Class:基于原型的语法糖

ES6 引入的 class 是 JavaScript 面向对象的语法糖,它提供了更接近传统面向对象语言的书写方式。但需要注意:

  • class 本质上仍然是构造函数
  • 方法定义在 prototype 上
  • 继承机制仍然依赖原型链
  • super 调用通过原型链和内部绑定实现 换句话说,class 并不是新增的继承机制,只是让语法更直观、可读性更好。

基本使用

JAVASCRIPT
class Person { constructor(name) { this.name = name; // 实例属性 } sayHello() { // 定义在 Person.prototype 上 console.log(`Hello, ${this.name}`); } } const p = new Person("Arafat"); p.sayHello(); // Hello, Arafat console.log(p.__proto__ === Person.prototype); // true

实例对象 p 的原型指向 Person.prototype; 方法 sayHello 并不在实例本身,而是在原型上,节省内存;本质与 ES5 构造函数 + 原型方法等价。

继承与 super

JS
class Parent { constructor(name) { this.name = name; } greet() { console.log(`Hello from Parent`); } } class Child extends Parent { constructor(name, age) { super(name); // 调用父类构造函数 this.age = age; } info() { console.log(`${this.name}, ${this.age}`); } } const c = new Child("Arafat", 20); c.greet(); // Hello from Parent c.info(); // Arafat, 20

继承机制底层分析:

  1. 实例级原型链:c.__proto__ → Child.prototype → Parent.prototype → Object.prototype
  2. 构造函数级原型链:Child.__proto__ → Parent.__proto__ → Function.prototype
  3. super() 调用父类构造函数,内部等价于 Parent.call(this, name)
  4. super.method() 调用父类原型方法,等价于 Parent.prototype.method.call(this)

静态成员与私有成员

JS
class Util { #_name // 私有成员 static version = "1.0"; // 静态成员 constructor(name) { this.#_name = name; } static log(msg) { // 静态方法 console.log(`[LOG]: ${msg}`); } #getName() { // 私有方法 return this.#_name; } getName() { // 公有方法 return this.#getName(); } } const t = new Util("arafat"); console.log(Util.version); // 1.0 Util.log("hello"); // [LOG]: hello t.getName(); // arafat t.#getName(); // 错误

静态方法 logversion 是静态方法,它们只能通过类名调用,不能通过实例对象调用。 私有方法 #getName 是私有方法,它只能通过类内部调用,不能通过实例对象调用。 所以可以理解为:

  • 静态成员/方法 → 类级别,只能通过类访问
  • 私有成员/方法 → 实例级别,但只能在类内部访问

访问器(getter / setter)

JS
class Rectangle { constructor(width, height) { this.width = width; this.height = height; } get area() { return this.width * this.height; } set area(value) { this.width = Math.sqrt(value); this.height = Math.sqrt(value); } } const r = new Rectangle(4, 9); console.log(r.area); // 36 r.area = 100; console.log(r.width, r.height); // 10, 10

访问器可以是实例级、私有或静态成员。

class 本质仍基于原型链,提供了更现代、易读的语法,同时支持静态、私有、访问器等高级特性,使 JavaScript 面向对象能力更加完整。

最后

JavaScript 的对象系统不是传统意义上的“类”式继承,而是以“原型”为核心的动态委托模型。理解原型链,实际上就是在理解 JavaScript 如何查找属性、如何复用方法、如何实现继承乃至如何塑造其整个面向对象体系。

无论是对象字面量、构造函数,还是 class,它们本质上都在构建同一套机制:

对象通过 [[Prototype]] 指向另一个对象,从而形成链式委托。

在早期的 ES5,我们通过构造函数、原型对象和各种继承技巧(原型链、盗用构造函数、组合继承、寄生组合继承)去拼装一个可用的继承体系。虽然灵活,但也带来了冗余和复杂性。

ES6 引入了 class,但它并没有改变语言底层,只是用更符合直觉的写法把原型体系包装成“类”的形式,同时补上了多年来开发者一直期望的能力:静态成员、原型方法、私有成员、访问器、super 调用……

从根本上说:class 是原型的语法糖,而原型才是 JavaScript 面向对象的灵魂。

理解这一点,我们会更容易掌握:

  • 为什么实例方法放在原型上更节省内存
  • 为什么 constructor 不是必须的但 prototype 是
  • 为什么 instanceof 能被改变
  • 为什么修改原型会影响已存在实例
  • 为什么要用寄生组合继承 在现代 JavaScript 中,面向对象的发展路径可以这样理解:

对象 → 原型 → 构造函数 → 原型链继承 → 寄生组合继承 → class(语法糖) 每一步并不是替代,而是将 JavaScript 的核心 —— “基于原型的动态委托” —— 表达得更自然、更稳定、更可维护。


如果你看到这里,谢谢你花时间阅读这篇小小的开篇文章。希望未来能与你在技术的旅途中有更多交流。