深入理解 JavaScript 原型与原型链:从构造函数到继承机制

49 阅读5分钟

一、为什么需要原型?

1.1 传统构造函数的性能问题

// 问题示例:每次实例化都会创建新的方法
function Car(color) {
    this.color = color;
    // ❌ 不推荐:每个实例都会创建新的 drive 方法
    this.drive = function() {
        console.log('driving ' + this.color + ' car');
    };
}

const car1 = new Car('red');
const car2 = new Car('blue');
console.log(car1.drive === car2.drive); // false - 不同内存地址

问题分析:每次实例化都会创建新的函数,造成内存浪费。

1.2 原型的解决方案

// ✅ 推荐:方法定义在原型上,所有实例共享
function Car(color) {
    this.color = color; // 实例特有属性
}

// 共享的属性和方法放在原型上
Car.prototype = {
    constructor: Car, // 修复构造函数指向
    name: 'su7',
    height: 1.4,
    weight: 1.5,
    drive() {
        console.log(this.color + ' ' + this.name + ' is driving');
    }
};

const car1 = new Car('red');
const car2 = new Car('blue');
console.log(car1.drive === car2.drive); // true - 共享同一个方法

二、原型链的核心概念

2.1 构造函数、原型、实例的三者关系

function Person(name, age) {
    // 实例特有属性
    this.name = name;
    this.age = age;
}

// 原型对象 - 存储共享属性和方法
Person.prototype = {
    constructor: Person, // 重要:修复构造函数引用
    species: '人类',
    introduce() {
        return `我叫${this.name},今年${this.age}岁`;
    }
};

const person1 = new Person('张三', 25);

// 🔍 验证原型关系
console.log('1. 实例的原型指向构造函数的原型:', 
    person1.__proto__ === Person.prototype); // true

console.log('2. 原型的构造函数指向构造函数本身:', 
    Person.prototype.constructor === Person); // true

console.log('3. 实例的构造函数:', 
    person1.constructor === Person); // true

console.log('4. 使用 Object.getPrototypeOf 获取原型:', 
    Object.getPrototypeOf(person1) === Person.prototype); // true

2.2 属性查找机制

function Person(name) {
    this.name = name;
}

Person.prototype.species = '人类';

const p1 = new Person('李四');

console.log('=== 属性查找演示 ===');

// 1. 查找实例自身属性
console.log('实例自身属性 name:', p1.name); // '李四'

// 2. 查找原型链上的属性  
console.log('原型链属性 species:', p1.species); // '人类'

// 3. 属性遮蔽(Property Shadowing)
p1.species = '智人'; // 在实例上创建同名属性
console.log('实例属性遮蔽原型属性:', p1.species); // '智人'
console.log('原型属性保持不变:', p1.__proto__.species); // '人类'

// 4. 删除实例属性后,重新访问原型属性
delete p1.species;
console.log('删除实例属性后:', p1.species); // '人类'

查找顺序:实例自身 → 原型对象 → 原型链上层 → ... → null

三、完整的原型链

3.1 原型链的顶端

function Person(name) {
    this.name = name;
}

const person = new Person('王五');

console.log('=== 完整的原型链 ===');

// 1. 实例的原型指向构造函数的原型
console.log('person.__proto__ === Person.prototype:', 
    person.__proto__ === Person.prototype); // true

// 2. Person.prototype 的原型指向 Object.prototype
console.log('Person.prototype.__proto__ === Object.prototype:', 
    Person.prototype.__proto__ === Object.prototype); // true

// 3. Object.prototype 是原型链的顶端
console.log('Object.prototype.__proto__:', 
    Object.prototype.__proto__); // null

// 4. 方法继承演示
console.log('person.toString():', person.toString()); // [object Object]
console.log('toString方法来源:', 
    person.toString === Object.prototype.toString); // true

3.2 对象字面量的原型关系

// 对象字面量实际上是 new Object() 的语法糖
const obj = {
    name: '张三',
    age: 18
};

// 等价于
const obj2 = new Object();
obj2.name = '张三';
obj2.age = 18;

console.log('对象字面量的原型:', obj.__proto__ === Object.prototype); // true
console.log('对象的方法继承:', obj.toString === Object.prototype.toString); // true

四、原型链继承

4.1 实现简单的原型继承

console.log('=== 原型链继承演示 ===');

// 基础对象
const animalProto = {
    species: '动物',
    breathe() {
        console.log(this.name + ' is breathing');
    }
};

// 构造函数
function Animal(name) {
    this.name = name;
}

// 设置原型
Animal.prototype = animalProto;

function Person(name, age) {
    // 绑定this指向将来通过new Person()创建的实例对象
    // 借用父类构造函数来初始化子类实例的属性
    // 把Animal函数运行一遍,并且强制让里面的this等于我传进去的这个this(也就是person实例)
    // 相当于调用父类方法,避免重复书写代码
    Animal.call(this, name);
    this.age = age;
}

// 设置 Person 的原型链:继承 Animal.prototype
// 用Object.create()的目的是让Person.prototype继承Animal.prototype,但又不直接等于它
// 这样Person.prototype的__proto__指向Animal.prototype,形成原型链
// 如果直接写Person.prototype = Animal.prototype,那么修改Person.prototype会污染Animal.prototype!
Person.prototype = Object.create(Animal.prototype);
Person.prototype.constructor = Person; // 修复构造函数

// 添加子类特有方法
Person.prototype.speak = function() {
    console.log(`我是${this.name},今年${this.age}岁`);
};

const person = new Person('赵六', 30);

// 验证继承关系
person.breathe();  // 继承自 Animal.prototype
person.speak();    // Person 自有方法
console.log('物种:', person.species); // 继承自 Animal.prototype

上述代码形成了类似这样的原型链:

person 
    → __proto__ → Person.prototype 
        → __proto__ → Animal.prototype (== animalProto) 
            → __proto__ → Object.prototype

4.2 更清晰的原型链示例

// 清晰的继承层次
function Animal(species) {
    this.species = species || '未知物种';
}

Animal.prototype.getSpecies = function() {
    return this.species;
};

function Mammal(name) {
    Animal.call(this, '哺乳动物');
    this.name = name;
}

// 建立原型链
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;

Mammal.prototype.giveBirth = function() {
    console.log(this.name + ' is giving birth to live young');
};

function Human(name, age) {
    Mammal.call(this, name);
    this.age = age;
}

Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;

Human.prototype.think = function() {
    console.log(this.name + ' is thinking deeply');
};

// 使用示例
const human = new Human('小明', 25);

console.log('=== 原型链方法调用 ===');
human.getSpecies();   // 来自 Animal
human.giveBirth();    // 来自 Mammal  
human.think();        // 来自 Human

console.log('=== 原型链验证 ===');
console.log('human instanceof Human:', human instanceof Human);   // true
console.log('human instanceof Mammal:', human instanceof Mammal); // true
console.log('human instanceof Animal:', human instanceof Animal); // true

上述代码形成了类似这样的原型链:

human 
    → __proto__ → Human.prototype 
        → __proto__ → Mammal.prototype 
            → __proto__ → Animal.prototype 
                → __proto__ → Object.prototype
                    → __proto__ → null

五、重要注意事项

5.1 原型重写的问题

function Person(name) {
    this.name = name;
}

// ❌ 问题:重写 prototype 会丢失 constructor
Person.prototype = {
    sayHi() {
        console.log('Hi, ' + this.name);
    }
};

const p1 = new Person('张三');
console.log('constructor 丢失:', p1.constructor === Person); // false
console.log('constructor 指向:', p1.constructor); // [Function: Object]

// ✅ 解决方案:手动修复 constructor
Person.prototype = {
    constructor: Person, // 重要:修复构造函数引用
    sayHi() {
        console.log('Hi, ' + this.name);
    }
};

const p2 = new Person('李四');
console.log('constructor 修复后:', p2.constructor === Person); // true

5.2 原型方法的 this 指向

function Person(name) {
    this.name = name;
}

Person.prototype = {
    constructor: Person,
    name: '原型上的name', // 会被实例属性遮蔽
    introduce() {
        // this 指向调用该方法的实例
        console.log('我是' + (this.name || '无名氏'));
    }
};

const person = new Person('实例name');
person.introduce(); // '我是实例name'

// 直接调用原型方法时的 this 指向问题
const introFunc = person.introduce;
// introFunc(); // 在严格模式下会报错,非严格模式下 this 指向全局对象

六、现代 JavaScript 中的原型

6.1 ES6 Class 语法糖

// ES6 Class 本质还是原型继承
class Car {
    constructor(color) {
        this.color = color;
    }
    
    // 方法自动添加到原型上
    drive() {
        console.log(this.color + ' car is driving');
    }
    
    // 静态方法
    // 静态方法不能被实例调用,只能通过构造函数(或类)直接调用
    static info() {
        return 'This is a Car class';
    }
}

// 等同于
function OldCar(color) {
    this.color = color;
}

OldCar.prototype.drive = function() {
    console.log(this.color + ' car is driving');
};

OldCar.info = function() {
    return 'This is a Car class';
};

const car1 = new Car('red');
const car2 = new OldCar('blue');

console.log('Class 方法在原型上:', 
    car1.drive === Car.prototype.drive); // true
console.log('传统写法方法在原型上:', 
    car2.drive === OldCar.prototype.drive); // true

总结

核心要点:

  1. 性能优化:将共享方法放在原型上,避免重复创建
  2. 原型三角:构造函数、原型对象、实例之间的引用关系
  3. 查找机制:属性查找沿着原型链自下而上
  4. 继承实现:通过原型链实现对象之间的继承关系
  5. constructor修复:重写 prototype 时需要手动修复 constructor

最佳实践:

  • 实例特有属性定义在构造函数中
  • 共享方法和属性定义在原型上
  • 使用 Object.create() 建立原型链
  • 重写 prototype 时记得修复 constructor
  • 理解 ES6 class 只是语法糖,底层仍是原型机制

掌握原型和原型链是深入理解 JavaScript 面向对象编程的关键,这些概念在现代框架和库开发中仍然发挥着重要作用。