JavaScript 原型链完全指南:从懵圈到手撕继承,一篇通透!

104 阅读5分钟

本文将带你从 零基础 出发,一步步揭开 JavaScript 中最核心也最容易被误解的概念——原型(Prototype)原型链(Prototype Chain) 。我们将对比其他语言的继承机制,详细剖析 JavaScript 如何通过原型实现继承,并结合大量示例与“答疑解惑”环节,让你真正掌握这门语言的灵魂所在。


第一部分:JavaScript 原型链的基础知识 🧱

1.1 什么是对象?万物皆对象!

在 JavaScript 中,一切皆对象(除了原始类型:numberstringbooleannullundefinedsymbolbigint)。即使是函数,也是 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 引擎会按以下顺序查找:

  1. 先在对象自身查找;
  2. 如果找不到,就去它的 [[Prototype]](即 __proto__)中查找;
  3. 如果还找不到,就继续沿着原型链向上查找;
  4. 直到 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.createclass 等,但建议团队统一风格。


第三部分:原型链在继承中的实战详解 🔍(超详细!)

现在,我们进入干货核心:如何用原型链实现各种继承模式?

⚠️ 注意:以下所有方法都基于 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 classextends 的真相

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 极大的灵活性——你可以随时修改原型、混入行为、甚至实现多重继承(通过混入多个原型方法)。

🌱 记住

  • 原型链是查找机制,不是复制机制;
  • 继承的本质是委托,不是占有;
  • 理解 thisprototype__proto__ 三者关系,你就掌握了 JS 面向对象的核心。

希望这篇深度解析,能帮你彻底打通原型与继承的任督二脉!🚀


延伸思考

  • 如何用 Object.setPrototypeOf 动态修改原型链?
  • 为什么现代框架(如 React)越来越少用 class,而倾向函数式?
  • 原型链过长会影响性能吗?

欢迎在评论区交流!💬