原型与继承,什么是原型链?,如何实现继承?

一、原型(Prototype):JS 继承的核心基石

JavaScript 是基于原型(Prototype-Based) 的面向对象语言(而非类基),核心逻辑是:每个对象都有一个「原型对象」,对象可继承原型上的属性 / 方法

1. 原型的核心概念

关键属性 / 方法 作用
[[Prototype]] 对象的隐式原型(内部属性),指向其原型对象(浏览器中可通过 __proto__ 访问)
prototype 函数特有的属性,指向「原型对象」(只有函数有,普通对象无)
constructor 原型对象上的属性,指向创建该原型的构造函数(如 Person.prototype.constructor === Person

2. 核心规则

  • **所有对象(除 Object.create(null))都有 [[Prototype]]**:包括普通对象、数组、函数、实例;
  • 函数的 prototype 是原型对象:构造函数通过 prototype 定义 “共享属性 / 方法”,实例通过 [[Prototype]] 继承;
  • 原型对象也是普通对象:因此原型对象也有自己的 [[Prototype]],这是 “原型链” 的基础。

示例:原型的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 构造函数
function Person(name) {
this.name = name; // 实例私有属性
}

// 原型上定义共享方法(所有实例共享)
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};

// 创建实例
const p1 = new Person("张三");
const p2 = new Person("李四");

// 实例访问原型方法
p1.sayHi(); // Hi, I'm 张三
p2.sayHi(); // Hi, I'm 李四

// 实例的 __proto__ 指向构造函数的 prototype
console.log(p1.__proto__ === Person.prototype); // true
// 原型的 constructor 指向构造函数
console.log(Person.prototype.constructor === Person); // true

二、原型链(Prototype Chain):继承的查找机制

原型链是对象的 [[Prototype]] 依次指向原型对象,直到 null 的链式结构,核心作用是:解析对象的属性 / 方法访问—— 当访问对象的属性 / 方法时,JS 引擎先在对象自身查找,找不到则沿原型链向上查找,直到 null(原型链终点)。

1. 原型链的结构(以 Person 实例为例)

1
2
3
4
p1(实例)
[[Prototype]] → Person.prototype(原型对象)
[[Prototype]] → Object.prototype(顶级原型)
[[Prototype]] → null(原型链终点)

2. 原型链的查找规则

  • 自身优先:先查对象自身的属性 / 方法,找到则直接使用;
  • 向上追溯:自身没有则沿 [[Prototype]] 找原型对象的属性 / 方法;
  • 终点终止:直到 Object.prototype(其 [[Prototype]]null),若仍未找到,返回 undefined(方法则报错)。

示例:原型链查找

1
2
3
4
5
6
7
8
// 接上面的 Person 示例
console.log(p1.name); // 张三(自身属性)
console.log(p1.sayHi); // [Function](Person.prototype 上的方法)
console.log(p1.toString); // [Function: toString](Object.prototype 上的方法)
console.log(p1.xxx); // undefined(原型链终点仍未找到)

// 验证原型链终点
console.log(Object.prototype.__proto__); // null

原型链的核心特性

  • 继承性:实例可继承原型链上所有属性 / 方法(如 p1.toString() 继承自 Object.prototype);
  • 共享性:原型上的方法 / 属性被所有实例共享(节省内存);
  • 单向性:只能从对象向原型链上游查找,原型无法访问实例的属性。

三、JS 实现继承的常用方式(从经典到现代)

JS 没有 class 关键字(ES6 前),通过原型链实现继承,以下是主流方案(按推荐度排序):

1. ES6 class extends(推荐,语法糖)

ES6 新增 class/extends,本质是原型链的语法糖,更贴近类基语言的写法,易读性高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 父类
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi, ${this.name}`);
}
}

// 子类继承父类
class Student extends Person {
constructor(name, grade) {
super(name); // 调用父类构造函数(必须先调用)
this.grade = grade; // 子类私有属性
}
study() {
console.log(`${this.name} is studying in grade ${this.grade}`);
}
}

// 使用
const s1 = new Student("王五", 6);
s1.sayHi(); // Hi, 王五(继承父类方法)
s1.study(); // 王五 is studying in grade 6(子类方法)
console.log(s1.__proto__ === Student.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true(原型链继承)

2. 原型链继承(基础方案,有缺陷)

核心:子类的 prototype 指向父类的实例,让子类实例通过原型链继承父类属性 / 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 父类构造函数
function Person(name) {
this.name = name;
this.hobbies = ["reading"]; // 引用类型属性(缺陷:所有子类实例共享)
}
Person.prototype.sayHi = function() {
console.log(`Hi, ${this.name}`);
};

// 子类构造函数
function Student(name, grade) {
this.grade = grade;
}

// 核心:子类 prototype 指向父类实例
Student.prototype = new Person();
// 修复 constructor 指向(否则 Student.prototype.constructor === Person)
Student.prototype.constructor = Student;

// 缺陷:引用类型属性被共享
const s1 = new Student("张三", 5);
const s2 = new Student("李四", 6);
s1.hobbies.push("swimming");
console.log(s2.hobbies); // ["reading", "swimming"](s2 被影响)

缺陷:父类的引用类型属性会被所有子类实例共享,修改一个实例的属性会影响其他实例。

3. 构造函数继承(解决引用类型共享问题)

核心:在子类构造函数中调用父类构造函数(call/apply),实现实例属性的独立继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
this.name = name;
this.hobbies = ["reading"];
}
Person.prototype.sayHi = function() { /* ... */ };

function Student(name, grade) {
// 调用父类构造函数,绑定子类 this → 实例属性独立
Person.call(this, name);
this.grade = grade;
}

const s1 = new Student("张三", 5);
const s2 = new Student("李四", 6);
s1.hobbies.push("swimming");
console.log(s2.hobbies); // ["reading"](解决共享问题)
console.log(s1.sayHi); // undefined(缺陷:无法继承父类原型上的方法)

缺陷:只能继承父类实例属性,无法继承父类原型上的方法(复用性低)。

4. 组合继承(原型链 + 构造函数,经典方案)

核心:结合原型链继承(继承原型方法)和构造函数继承(继承实例属性),弥补各自缺陷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name) {
this.name = name;
this.hobbies = ["reading"];
}
Person.prototype.sayHi = function() {
console.log(`Hi, ${this.name}`);
};

function Student(name, grade) {
Person.call(this, name); // 构造函数继承:实例属性独立
this.grade = grade;
}
Student.prototype = new Person(); // 原型链继承:继承原型方法
Student.prototype.constructor = Student; // 修复 constructor

// 验证
const s1 = new Student("张三", 5);
s1.hobbies.push("swimming");
console.log(s2.hobbies); // ["reading"](属性独立)
s1.sayHi(); // Hi, 张三(继承原型方法)

缺陷:父类构造函数被调用两次(new Person()Person.call()),存在冗余属性。

5. 寄生组合继承(最优经典方案,ES6 class 底层逻辑)

核心:通过 Object.create() 继承父类原型,避免父类构造函数重复调用,是组合继承的优化版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
this.name = name;
this.hobbies = ["reading"];
}
Person.prototype.sayHi = function() { /* ... */ };

function Student(name, grade) {
Person.call(this, name); // 仅调用一次父类构造函数
this.grade = grade;
}

// 核心:子类 prototype 指向父类 prototype 的副本(而非父类实例)
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

// 新增子类方法
Student.prototype.study = function() { /* ... */ };

优势:父类构造函数仅调用一次,无冗余属性,同时继承实例属性和原型方法,是 ES6 class extends 之前的最优方案。

四、核心总结

  1. 原型:每个对象有 [[Prototype]],函数有 prototype,是继承的基础;

  2. 原型链[[Prototype]] 形成的链式结构,是属性 / 方法查找的核心机制,终点是 null

  3. 继承实现

    • 现代推荐:ES6 class extends(原型链语法糖,简洁易读);
    • 经典方案:寄生组合继承(无冗余,性能优);
    • 基础方案:原型链继承 / 构造函数继承(有缺陷,仅作理解);
  4. 核心逻辑****:JS 继承的本质是 “修改原型链”,让子类实例能沿原型链访问父类的属性 / 方法。****