skip to content
月与羽

原型委托

/ 8 min read

“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 去找

  • 这种查找过程称为 “原型链查找”


三、为什么说是“委托关系”,而不是“继承关系”

因为:

  1. 没有拷贝(copy)行为

    在传统继承中,子类会得到父类的属性副本(逻辑上)。

    而在 JS 中,子对象只是在运行时委托给原型对象,没有复制。

  2. 运行时动态性

    你可以在程序运行时修改原型对象,所有委托它的对象都会立刻“感知”到变化:

animal.eat = function() {
console.log("Animal eats");
};
dog.eat(); // 立即生效!
  1. 委托式查找:这说明它们之间不是“继承”,而是 “委托:我找不到,就请我的原型帮我处理。”

  2. 层级关系不同

    在类继承中,父类对子类有定义权;

    在原型委托中,任何对象都可以被另一个对象动态地当作原型。


四、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`); }
};
// 派生对象:委托给 Animal
const 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.prototype
d.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 → animal
dog.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。