本文将带你从 零基础 出发,一步步揭开 JavaScript 中最核心也最容易被误解的概念——原型(Prototype) 和 原型链(Prototype Chain) 。我们将对比其他语言的继承机制,详细剖析 JavaScript 如何通过原型实现继承,并结合大量示例与“答疑解惑”环节,让你真正掌握这门语言的灵魂所在。
第一部分:JavaScript 原型链的基础知识 🧱
1.1 什么是对象?万物皆对象!
在 JavaScript 中,一切皆对象(除了原始类型:number、string、boolean、null、undefined、symbol、bigint)。即使是函数,也是 Function 的实例;数组是 Array 的实例。
每个对象都有一个内部属性 [[Prototype]](在大多数浏览器中可通过 __proto__ 访问),它指向另一个对象,这个被指向的对象就是它的 原型(prototype) 。
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
这里,obj 是一个普通对象,它的原型是 Object.prototype。
1.2 构造函数与 prototype 属性
当你使用 function 定义一个函数时,它会自动拥有一个名为 prototype 的属性:
function Person(name) {
this.name = name;
}
console.log(Person.prototype); // { constructor: Person }
这个 prototype 是一个 普通对象,所有通过 new Person() 创建的实例,都会将 [[Prototype]] 指向 Person.prototype。
const alice = new Person("Alice");
console.log(alice.__proto__ === Person.prototype); // true
1.3 原型链是如何工作的?
当你访问一个对象的属性时,JavaScript 引擎会按以下顺序查找:
- 先在对象自身查找;
- 如果找不到,就去它的
[[Prototype]](即__proto__)中查找; - 如果还找不到,就继续沿着原型链向上查找;
- 直到
Object.prototype,如果仍然没有,就返回undefined。
这就是 原型链(Prototype Chain) 。
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
alice.sayHello(); // "Hello, I'm Alice"
// alice 自身没有 sayHello,但在 Person.prototype 中找到了
而 Object.prototype 的 [[Prototype]] 是 null,所以原型链在此终止。
💡 答疑解惑 #1:常见误区澄清
Q1:__proto__ 和 prototype 是一回事吗?
❌ 不是!
prototype是 函数 才有的属性,用于指定其 实例 的原型。__proto__是 所有对象 都有的(非标准但广泛支持),指向其原型对象。- 实例的
__proto__=== 构造函数的prototype。
Q2:为什么 Object.prototype.__proto__ 是 null?
因为它是原型链的终点。如果它还有原型,就会无限循环,所以设计为 null 表示“无父”。
Q3:原始类型有原型链吗?
有!当你对 "hello".toUpperCase() 这样调用方法时,JS 会临时包装成 String 对象,然后沿 String.prototype 查找方法,用完就销毁。
第二部分:JavaScript 继承 vs 其他语言的继承
2.1 传统面向对象语言的“类继承”
在 Java、C++ 等语言中,继承是基于 类(Class) 的:
class Animal {
void speak() { System.out.println("Animal sound"); }
}
class Dog extends Animal {
void bark() { System.out.println("Woof!"); }
}
- 类是模板,对象是类的实例。
- 继承是“复制”或“扩展”类的结构。
- 编译时确定关系,静态强类型。
2.2 JavaScript 的“原型继承”:动态委托
JavaScript 没有类(ES6 之前) ,只有对象。继承的本质是 对象委托(delegation) :
“我不懂这个方法?那我去问问我的原型会不会。”
这种机制更灵活、动态。你可以在运行时修改原型,所有实例立即生效:
Person.prototype.walk = function() {
console.log(`${this.name} is walking.`);
};
alice.walk(); // "Alice is walking."
// 即使 alice 已创建,也能立刻使用新方法!
2.3 ES6 的 class 只是语法糖 🍬
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
这段代码等价于:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
所以,class 并没有改变 JavaScript 基于原型的本质,只是让写法更接近传统 OOP。
💡 答疑解惑 2:继承方式的本质区别
Q1:原型继承比类继承好吗?
各有优劣:
- 原型继承更灵活、动态,适合行为组合;
- 类继承结构清晰,适合大型工程和类型安全。
Q2:为什么说 JS 是“基于对象”而不是“面向对象”?
因为 JS 没有类(早期),对象直接从其他对象继承,而非通过抽象模板。这是 原型式编程(Prototypal Programming) 的核心。
Q3:我能同时用多种继承方式吗?
可以!JS 的灵活性允许你混合使用构造函数、原型、Object.create、class 等,但建议团队统一风格。
第三部分:原型链在继承中的实战详解 🔍(超详细!)
现在,我们进入干货核心:如何用原型链实现各种继承模式?
⚠️ 注意:以下所有方法都基于 ES5 思维,但理解它们对掌握 ES6+ 至关重要。
3.1 原型链继承(Prototype Chain Inheritance)
思路:让子类的原型等于父类的实例。
function Animal() {
this.species = "animal";
}
Animal.prototype.getSpecies = function() {
return this.species;
};
function Dog() {
this.breed = "dog";
}
// 关键:继承 Animal
Dog.prototype = new Animal();
const myDog = new Dog();
console.log(myDog.getSpecies()); // "animal"
✅ 优点:方法复用,子类可访问父类方法。
❌ 缺点:
- 所有子实例共享父类实例的引用属性(如数组、对象);
- 无法向父类构造函数传参。
Animal.prototype.hobbies = [];
myDog.hobbies.push("fetch");
const anotherDog = new Dog();
console.log(anotherDog.hobbies); // ["fetch"] ❌ 共享了!
3.2 借用构造函数继承(Constructor Stealing)
思路:在子类构造函数中用 call/apply 调用父类构造函数。
function Animal(name) {
this.name = name;
this.hobbies = [];
}
function Dog(name, breed) {
Animal.call(this, name); // 借用父构造函数
this.breed = breed;
}
const dog1 = new Dog("Buddy", "Golden");
dog1.hobbies.push("swim");
const dog2 = new Dog("Max", "Lab");
console.log(dog2.hobbies); // [] ✅ 不共享!
✅ 优点:解决引用属性共享问题,可传参。
❌ 缺点:无法继承父类原型上的方法!
Animal.prototype.sayName = function() { console.log(this.name); };
dog1.sayName(); // ❌ TypeError: dog1.sayName is not a function
3.3 组合继承(Combination Inheritance)🔥
思路:结合上述两种方式!
function Animal(name) {
this.name = name;
this.hobbies = [];
}
Animal.prototype.sayName = function() {
console.log(this.name);
};
function Dog(name, breed) {
Animal.call(this, name); // 借用构造函数(解决属性共享)
this.breed = breed;
}
Dog.prototype = new Animal(); // 原型链继承(继承方法)
Dog.prototype.constructor = Dog; // 修复 constructor 指向
const dog = new Dog("Rex", "Husky");
dog.sayName(); // "Rex" ✅
dog.hobbies.push("howl");
✅ 优点:属性不共享 + 方法可继承,是最常用的 ES5 继承模式。
❌ 缺点:父类构造函数被调用了两次!一次在 new Animal(),一次在 Animal.call()。
3.4 寄生组合继承(Parasitic Combination Inheritance)💎
终极优化方案:避免两次调用父构造函数。
核心技巧:用一个空函数作为中介,只继承原型,不执行父构造函数。
function inheritPrototype(Child, Parent) {
const F = function() {}; // 空函数作中介
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log(this.name);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
inheritPrototype(Dog, Animal); // 关键!
const dog = new Dog("Luna", "Shiba");
dog.sayName(); // "Luna" ✅
✅ 优点:
- 只调用一次父构造函数;
- 属性不共享;
- 方法完整继承;
- 原型链干净。
这也是 ES6 class extends 的底层实现原理!
3.5 ES6 class 与 extends 的真相
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 相当于 Animal.call(this, name)
this.breed = breed;
}
}
Babel 转译后,你会发现它本质上就是 寄生组合继承!
💡 答疑解惑 3:继承实战高频问题
Q1:为什么 constructor 要手动修复?
因为 Child.prototype = new Parent() 会让 Child.prototype.constructor 指向 Parent,破坏了“实例.constructor 应该指向其构造函数”的约定。
Q2:Object.create(null) 和 Object.create(proto) 有什么区别?
Object.create(proto):创建一个[[Prototype]]指向proto的新对象;Object.create(null):创建一个 无原型 的对象(常用于哈希表,避免原型污染)。
Q3:实例和原型对象真的没有“血缘关系”吗?
严格来说,有关系,但不是“父子”而是“委托” 。实例通过 __proto__ 委托给原型,原型不是它的“父亲”,而是“能力提供者”。
Q4:如何判断一个属性是自身的还是继承的?
用 hasOwnProperty:
console.log(dog.hasOwnProperty('name')); // true
console.log(dog.hasOwnProperty('sayName')); // false
结语:拥抱原型,理解 JavaScript 的灵魂 ❤️
JavaScript 的原型机制初看复杂,实则优雅。它不靠“类”的枷锁,而是通过 对象之间的动态链接 实现继承。这种设计赋予了 JS 极大的灵活性——你可以随时修改原型、混入行为、甚至实现多重继承(通过混入多个原型方法)。
🌱 记住:
- 原型链是查找机制,不是复制机制;
- 继承的本质是委托,不是占有;
- 理解
this、prototype、__proto__三者关系,你就掌握了 JS 面向对象的核心。
希望这篇深度解析,能帮你彻底打通原型与继承的任督二脉!🚀
延伸思考:
- 如何用
Object.setPrototypeOf动态修改原型链? - 为什么现代框架(如 React)越来越少用 class,而倾向函数式?
- 原型链过长会影响性能吗?
欢迎在评论区交流!💬