“JavaScript 中没有继承,只有对象之间的委托关系”其实反映了 JavaScript 和传统面向对象语言(如 Java、C++、Python)的根本区别。我们可以分几个层面来理解:
一、传统继承(Classical Inheritance)
在传统的 类(class-based)语言 中,比如 Java 或 C++,继承 是通过“类”这个抽象模板完成的:
-
类定义了对象的结构和行为;
-
新类可以 继承 旧类的属性与方法;
-
继承关系是 静态的,即在定义时就确定。
例如在 Java 中:
class Animal { void speak() { System.out.println("Animal speaks"); }}
class Dog extends Animal { void bark() { System.out.println("Woof!"); }}在这里:
-
Dog 是从 Animal 派生出来的;
-
Dog 继承了 Animal 的方法;
-
继承是编译期确定的“类的层级结构”。
二、JavaScript 的原型机制(Prototype System)
而 JavaScript 的对象模型不同:
-
JavaScript 没有类(在 ES6 之前);
-
它的对象是通过 原型(prototype)链 来实现“行为共享”的;
-
所谓“继承”,其实是 一个对象把另一个对象作为自己的原型进行委托。
举个例子:
const animal = { speak() { console.log("Animal speaks"); }};
const dog = Object.create(animal);dog.bark = function() { console.log("Woof!");};
dog.speak(); // 输出 "Animal speaks"在这里:
-
dog 并不是“继承”了 animal;
-
而是 当访问 dog.speak() 时,JavaScript 在 dog 上找不到 speak,于是委托给它的原型 animal 去找;
-
这种查找过程称为 “原型链查找”。
三、为什么说是“委托关系”,而不是“继承关系”
因为:
-
没有拷贝(copy)行为
在传统继承中,子类会得到父类的属性副本(逻辑上)。
而在 JS 中,子对象只是在运行时委托给原型对象,没有复制。
-
运行时动态性
你可以在程序运行时修改原型对象,所有委托它的对象都会立刻“感知”到变化:
animal.eat = function() { console.log("Animal eats");};
dog.eat(); // 立即生效!-
委托式查找:这说明它们之间不是“继承”,而是 “委托:我找不到,就请我的原型帮我处理。”
-
层级关系不同
在类继承中,父类对子类有定义权;
在原型委托中,任何对象都可以被另一个对象动态地当作原型。
四、ES6 class只是语法糖
虽然 ES6 引入了 class 关键字:
class Animal { speak() { console.log("Animal speaks"); }}class Dog extends Animal { bark() { console.log("Woof!"); }}但底层机制仍是基于原型:
typeof Animal; // 'function'Dog.prototype.__proto__ === Animal.prototype; // true也就是说,class 只是对原型委托的语法封装,让面向类的程序员更容易上手而已。
五、委托的实现方法
下面给出几种在实际工程中常用的“原型委托”实现方式与适用场景。
1. 使用 Object.create(推荐的 OLOO 模式)
用一个对象直接作为另一个对象的原型,让后者在查找失败时委托给前者。
// 基对象:被委托者const Animal = { init(name) { this.name = name; return this; }, speak() { console.log(`${this.name} makes a noise`); }};
// 派生对象:委托给 Animalconst Dog = Object.create(Animal);Dog.bark = function () { console.log(`${this.name} woof`); };
// 实例:再委托给 Dog(链长为 2)const d = Object.create(Dog).init('Fido');d.speak(); // Fido makes a noise (委托到 Animal)d.bark(); // Fido woof (在 Dog 上找到)
// 可按需覆写,实现“就近查找覆盖”Dog.speak = function () { console.log(`${this.name} barks`); };d.speak(); // Fido barks要点:
- 首选
Object.create(proto)在创建时确定原型; - 写入属性总是写到“接收者对象”(即点左侧的对象)上,可能遮蔽原型属性;
- 原型方法请使用普通函数,不要用箭头函数(箭头函数不绑定动态
this)。
2. 构造函数 + prototype(与 class 等价的底层形式)
当你需要 new 语义、配合私有字段/装饰器或类工具链时,可使用构造函数或 class,本质仍是原型委托。
function Animal(name) { this.name = name; }Animal.prototype.speak = function () { console.log(`${this.name} makes a noise`);};
function Dog(name) { Animal.call(this, name); // 复用构造逻辑}Dog.prototype = Object.create(Animal.prototype, { constructor: { value: Dog, enumerable: false, writable: true }});Dog.prototype.bark = function () { console.log(`${this.name} woof`); };
const d = new Dog('Fido');d.speak(); // 委托到 Animal.prototyped.bark();等价的 class 写法(语法糖):
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise`); }}class Dog extends Animal { bark() { console.log(`${this.name} woof`); } // 覆盖并可用 super 继续委托 speak() { super.speak(); console.log('(overridden in Dog)'); }}3. Object.setPrototypeOf / __proto__(修改既有对象的原型)
仅在初始化阶段偶尔需要把两个既有对象连成委托关系;频繁修改原型会影响性能,不建议在热路径上使用。
const animal = { speak() { console.log('animal speaks'); } };const dog = { bark() { console.log('woof'); } };
Object.setPrototypeOf(dog, animal); // 建立委托:dog → animaldog.speak(); // animal speaks
// 对象字面量也可用 __proto__(不推荐用于库/核心路径)const cat = { __proto__: animal, meow() { console.log('meow'); } };cat.speak(); // animal speaks注意:
Object.setPrototypeOf会触发引擎的形状变更,影响优化;尽量在对象创建时一次性确定原型;__proto__是历史遗留访问器,规范化但不推荐在生产代码中广泛使用。
4. 小工具:创建并赋值的便捷工厂
function createWith(proto, props) { const obj = Object.create(proto); return Object.assign(obj, props);}
const d = createWith(Dog, { name: 'Buddy' });d.bark();5. 常见陷阱与实践建议
- 查找 vs. 写入:属性查找沿原型链向上,但写入始终落在接收者自身,易造成“遮蔽”。用
Object.hasOwn(d, 'x')区分自有属性; this绑定:原型方法应使用普通函数,箭头函数会捕获外层this;- 链过深:过长的原型链会带来查找成本;
- 性能:在热路径上避免
setPrototypeOf/动态改原型; - 何时选
class:需要 TS/装饰器/私有字段/继承层级时更方便;纯对象协作更适合 OLOO。