原型、原型链与继承
2025-12-09 16:24
要真正理解 JavaScript 的面向对象体系,必须先理解它的“原型(Prototype)”。原型决定了对象如何共享方法、如何查找属性,也决定了 JavaScript 的继承方式为何与传统面向对象语言完全不同。
与 Java、C++ 等基于类的语言采用“复制式继承”不同,JavaScript 使用的是 基于原型的委托式继承。每个对象都可以通过原型链向上委托,形成灵活而动态的继承结构。
原型与原型链:JavaScript OOP 的基石
每个 JavaScript 对象都有一个内部属性 [[Prototype]](可通过 __proto__ 访问),它指向另一个对象——即该对象的“原型”。当访问一个对象的属性时,若自身没有,引擎会沿着原型链向上查找,直到 null。
JSconst 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 的链条,就是原型链。
这就是为什么我们可以通过实例对象访问原型中的属性和方法。看例子:
JSconst 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]'
明明没有split、push、toString这些方法,但是通过实例对象访问原型中的属和方法,是因为原型链。它们同时被各自的构造函数创建的实例对象,所以可以通过__proto__属性访问原型对象方法。其中的原始值没有原型链,但是实际操作情况 javascript 中的自动装箱和拆箱机制。
每一个 JavaScript 对象(除了 null )都具有的一个属性,叫proto,这个属性会指向该对象的原型。
原型的来源:构造函数与 prototype
既然每个对象都有 proto 指向其原型,那么下一个问题自然是:原型是怎么来的?
答案是: 来自构造函数的 prototype 属性。
JavaScript 中,每当你定义一个函数时,JS 引擎会自动为该函数创建一个 prototype 对象,并附带一个指向构造函数本身的 constructor 属性。
JSfunction Person() {} Person.prototype.name = "Arafat"; console.log(Person.prototype);

prototype 中除了添加了 name 属性外,还添加了一个 constructor 属性,该属性指向了 Person 函数本身和 __proto__ 属性指向了 Object.prototype。什么是构造函数
构造函数是用来创建对象实例的函数,通常通过 new 调用。例如:
JSfunction 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__)向上委托,最终会到达一个终点。而这个终点就是:
JSObject.prototype.__proto__ === null; // true
为什么原型链的重点是 Null ?原因很简单:JavaScript 必须有一个无法继续委托的终点对象,以避免原型链无限上溯。这个对象就是所有对象的根——Object.prototype。
也就是说从语言设计的角度来看:
- 所有普通对象都继承自
Object.prototype - 但为了避免形成循环或无限委托,根对象的原型必须是
null null表示“没有原型”,从此处终止查找
理解 instanceof:基于原型链的类型判断机制
instanceof 是 JavaScript 中用于判断一个对象是否由某个构造函数创建的运算符。但它的判断逻辑不是检查“构造函数”,也不是检查“类型字符串”,而是检查原型链。
instanceof 的判断规则是当执行:
JAVASCRIPTa instanceof B; // 判断构造函数 B 的 prototype 是否出现在 a 的原型链上。
内部逻辑是:
- 取出 B.prototype
- 沿着 a 的
__proto__(即[[Prototype]])向上查找 - 如果某一层原型 === B.prototype,则返回 true
- 查到顶层
null还没找到,则返回 false 我们也可以手动实现:
JAVASCRIPTfunction 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 运算符行为的一个内置符号。当我们执行:
JSx instanceof C // 等价于 C[Symbol.hasInstance](x)
换句话说,instanceof 的最终判定逻辑是由构造函数的 Symbol.hasInstance 方法决定的。只要你重新定义该方法,就可以改变 instanceof 的行为。
也就是说如果不覆盖 Symbol.hasInstance,那么默认逻辑就是按照原型链去比对。ES6 引入 Symbol.hasInstance 就是为了让 instanceof 具有灵活性和可定制性。比如模拟接口检查:
JSconst Interface = { [Symbol.hasInstance](obj) { return obj && typeof obj.run === 'function'; } }; console.log({ run: () => {} } instanceof Interface); // true
创建对象的方法
JavaScript 中,创建对象的方式主要有三种,每种方式适用场景和特点不同:
构造函数 + new
通过构造函数创建对象是 JavaScript 面向对象体系的核心方式。使用 new 调用构造函数时,会执行以下操作:
- 创建一个空对象
{} - 将这个对象的
__proto__指向构造函数的prototype - 执行构造函数,将
this绑定到新对象,并初始化属性 - 返回对象(如果构造函数返回了对象,则返回该对象;否则返回新创建的对象) 示例:
JAVASCRIPTfunction 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 。这种方式通常用于原型式继承,可以直接将已有对象作为新对象的原型。示例:
JAVASCRIPTconst parent = { name: 'Parent' }; const child = Object.create(parent); child.age = 10; console.log(child.name); // "Parent",通过原型链访问 console.log(child.__proto__ === parent); // true
Object.create不会执行构造函数,仅建立原型关系,因此适合单纯的原型继承或对象克隆。
字面量表示法
最常见和最简便的方式是使用字面量:
JAVASCRIPTconst obj = {}; // 对象 Object.prototype const arr = []; // 数组 Array.prototype const func = () => {}; // 函数 Function.prototype
这种方式主要用于快速创建对象或函数,不涉及构造函数和继承逻辑。
继承机制
JavaScript 的继承与传统类继承不同,它依赖原型链。创建子类实例时,子类可以通过原型链访问父类的属性和方法。
原型继承
JAVASCRIPTfunction 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
优点:
- 方法复用:所有实例共享父类原型方法,节省内存
- 继承实现简单
缺点:
- 引用类型属性共享:数组、对象会被所有实例共享,容易出现副作用
- 无法给父类构造函数传参初始化属性
- 实例属性无法继承,需要手动在子类构造函数中定义
适用场景:父类属性是原始类型,方法复用是重点,不需要传参初始化
盗用构造函数继承
JSfunction 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
优点:
- 实例属性独立,每个实例的引用类型不会共享
- 支持传参初始化父类属性
- 简单明了,不依赖原型链
缺点:
- 父类原型方法无法继承,每个子类实例都需要单独定义方法,占用内存
- 不支持多态,无法访问父类原型方法
**适用场景:**父类有引用类型属性,需要独立实例;不需要继承父类方法
组合继承
JSfunction 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
优点:
- 实例属性独立
- 方法可以复用
- 支持传参
缺点:
- 父类构造函数调用两次,增加性能开销
- 原型对象可能存在冗余实例属性
适用场景:需要同时继承属性和方法,实例独立性重要,性能要求一般
寄生式继承
JAVASCRIPTfunction 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 继承方式)
JAVASCRIPTfunction 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 并不是新增的继承机制,只是让语法更直观、可读性更好。
基本使用
JAVASCRIPTclass 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
JSclass 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
继承机制底层分析:
- 实例级原型链:
c.__proto__ → Child.prototype → Parent.prototype → Object.prototype - 构造函数级原型链:
Child.__proto__ → Parent.__proto__ → Function.prototype super()调用父类构造函数,内部等价于Parent.call(this, name)super.method()调用父类原型方法,等价于Parent.prototype.method.call(this)
静态成员与私有成员
JSclass 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(); // 错误
静态方法 log 和 version 是静态方法,它们只能通过类名调用,不能通过实例对象调用。
私有方法 #getName 是私有方法,它只能通过类内部调用,不能通过实例对象调用。
所以可以理解为:
- 静态成员/方法 → 类级别,只能通过类访问
- 私有成员/方法 → 实例级别,但只能在类内部访问
访问器(getter / setter)
JSclass 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 的核心 —— “基于原型的动态委托” —— 表达得更自然、更稳定、更可维护。
如果你看到这里,谢谢你花时间阅读这篇小小的开篇文章。希望未来能与你在技术的旅途中有更多交流。