JavaScript原型链 - 继承的基石与核心机制

2 阅读4分钟

前言:从一道面试题说起

function Person() {}
const person = new Person();

console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

console.log(person.constructor === Person); // true
console.log(Person.constructor === Function); // true
console.log(person.constructor === Person.prototype.constructor); // true (!)
console.log(Function.constructor === Function); // true (!)

如果你能完全理解上面的代码,那么你已经掌握了原型链的核心。如果不能,本篇文章将带我们一步步揭开原型链的神秘面纱。

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

在JavaScript中,每个对象都有一个特殊的内部属性[[Prototype]](可以通过__proto__访问),它指向该对象的原型对象。

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

// 原型对象:所有实例共享的方法和属性
Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};

// 实例:通过new创建的对象
const zhangsan = new Person('zhangsna');

// 三者关系验证
console.log(zhangsan.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

prototype和__proto__的区别与联系

  • prototype 是函数特有的属性。
  • __proto__ 是每个对象都有的属性。
function Foo() {}
const obj = new Foo();

console.log(typeof Foo.prototype); // "object"
console.log(typeof obj.__proto__); // "object"

console.log(Foo.prototype === obj.__proto__); // true
console.log(Foo.__proto__ === Function.prototype); // true

// 一定要注意:函数也是对象,所以函数也有__proto__
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
  1. 每个函数都有一个 prototype 属性,指向该函数的原型对象
  2. 每个对象都有一个 __proto__ 属性,指向创建该对象的构造函数的原型对象
  3. 原型对象的 constructor 属性指向创建该实例对象的构造函数。
  4. Function 是一个特殊的函数,它的 constructor 指向它自己。

完整的原型链结构

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

Animal.prototype.eat = function() {
    console.log(`${this.name} is eating`);
};

function Dog(name, breed) {
    Animal.call(this, name); // 调用父类构造函数
    this.breed = breed;
}

// 设置原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor指向

Dog.prototype.bark = function() {
    console.log(`${this.name} is barking`);
};

const myDog = new Dog('Buddy', 'Golden Retriever');

// 原型链查找路径:
console.log(myDog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

上述代码中,原型链的查找过程:

  1. myDog 本身有 namebreed 属性
  2. myDog.__proto__ (Dog.prototype) 有 bark 方法
  3. Dog.prototype.__proto__ (Animal.prototype) 有 eat 方法
  4. Animal.prototype.__proto__ (Object.prototype) 有 toString 等方法
  5. Object.prototype.__proto__null,查找结束

Object.prototype是所有原型链的终点。

属性屏蔽规则

hasOwnProperty vs in操作符

  • hasOwnProperty: 检查属性是否在对象自身(不在原型链上)
  • in操作符: 检查属性是否在对象自身或原型链上

属性屏蔽的三种情况

function Parent() {
    this.value = 'parent value';
}

Parent.prototype.shared = 'parent shared';

function Child() {
    this.value = 'child value'; 
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.shared = 'child shared'; 

const child = new Child();

情况1:对象自身有该属性(完全屏蔽)

以上述代码为例,Child 本身有自己的 value 属性,当调用 value 属性时,会完全屏蔽原型链上的同名 value 属性:

console.log(child.value); // 'child value'(不是'parent value')
console.log(child.hasOwnProperty('value')); // true

情况2:对象自身本来没有,但添加对象自身属性

console.log(child.shared); // 'child shared'(来自Child.prototype)
console.log(child.hasOwnProperty('shared')); // false

// 添加对象自身属性
child.shared = 'instance shared';
console.log(child.shared); // 'instance shared'(现在自身有了)
console.log(child.hasOwnProperty('shared')); // true

情况3:属性是只读的

Parent.prototype.readOnly = 'cannot change';

// 试图修改只读属性
child.readOnly = 'try to change';
console.log(child.readOnly); // 'cannot change'(修改失败)
console.log(child.hasOwnProperty('readOnly')); // false(没有添加成功)

原型链的基础结构

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

Person.prototype.sayHello = function() {
    console.log(`Hello, ${this.name}`);
};

const zhangsan = new Person('zhangsan');

其原型结构图如下:

zhangsan (实例)
  ├── __proto__: Person.prototype
  │      ├── constructor: Person
  │      ├── sayHello: function()
  │      └── __proto__: Object.prototype
  │             ├── constructor: Object
  │             ├── toString: function()
  │             ├── hasOwnProperty: function()
  │             └── __proto__: null
  ├── name: "zhangsan"
  └── (其他实例属性...)

Person (构造函数)
  ├── prototype: Person.prototype
  └── __proto__: Function.prototype
           ├── constructor: Function
           ├── apply: function()
           ├── call: function()
           └── __proto__: Object.prototype

原型链的实际应用

实现继承的多种方式

原型链继承(经典方式)

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

function Child(age) {
    this.age = age;
}

// 关键:让子类原型指向父类实例
Child.prototype = new Parent();

// 修复constructor指向
Child.prototype.constructor = Child;
  • 问题:引用类型的属性会被所有实例共享

组合继承(最常用)

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

function Child(name, age) {
    // 继承属性
    Parent.call(this, name);  // 第二次调用Parent
    this.age = age;
}

// 继承方法
Child.prototype = new Parent();  // 第一次调用Parent
Child.prototype.constructor = Child;
  • 优点:结合了原型链和构造函数的优点
  • 缺点:父类构造函数被调用了两次

寄生组合式继承(最佳实践)

function inheritPrototype(child, parent) {
    // 创建父类原型的副本
    const prototype = Object.create(parent.prototype);
    // 修复constructor指向
    prototype.constructor = child;
    // 设置子类原型
    child.prototype = prototype;
}

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

function Child(name, age) {
    Parent2.call(this, name);
    this.age = age;
}

inheritPrototype(Child, Parent);
  • 只调用一次父类构造函数
  • 避免在子类原型上创建不必要的属性
  • 原型链保持不变

ES6 class继承

class Parent3 {
    constructor(name) {
        this.name = name;
    }
}

class Child3 extends Parent3 {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
    
    sayAge() {
        console.log(this.age);
    }
}
  • 语法简洁,现代解决方案,但需要ES6+支持

实现混入(Mixin)

const canEat = {
    eat: function(food) {
        console.log(`${this.name} is eating ${food}`);
        this.energy += 10;
    }
};

const canSleep = {
    sleep: function(hours) {
        console.log(`${this.name} is sleeping for ${hours} hours`);
        this.energy += hours * 5;
    }
};

const canWalk = {
    walk: function(distance) {
        console.log(`${this.name} is walking ${distance} km`);
        this.energy -= distance * 2;
    }
};

// 混入函数
function mixin(target, ...sources) {
    Object.assign(target.prototype, ...sources);
}

// 创建动物类
function Animal(name) {
    this.name = name;
    this.energy = 100;
}

mixin(Animal, canEat, canSleep, canWalk);

// 创建鸟类,额外添加飞行能力
const canFly = {
    fly: function(distance) {
        console.log(`${this.name} is flying ${distance} km`);
        this.energy -= distance * 5;
    }
};

function Bird(name) {
    Animal.call(this, name);
    this.wings = 2;
}

// 设置原型链
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;

// 添加飞行能力
Object.assign(Bird.prototype, canFly);

原型链常见陷阱

陷阱1:修改原型会影响所有实例

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

const zhangsan = new Person('zhangsan');
const lisi = new Person('lisi');

// 修改原型
Person.prototype.sayHello = function() {
    console.log(`Hello, ${this.name}`);
};

zhangsan.sayHello(); // Hello, zhangsan (正常)
lisi.sayHello();   // Hello, lisi (正常)

陷阱2:原型上的引用类型属性被所有实例共享

function Problem() {
    this.values = []; // 正确:每个实例有自己的数组
}

Problem.prototype.sharedValues = []; // 错误:所有实例共享同一个数组

const p1 = new Problem();
const p2 = new Problem();

p1.sharedValues.push('from p1');
p2.sharedValues.push('from p2');

console.log(p1.sharedValues); // ['from p1', 'from p2']
console.log(p2.sharedValues); // ['from p1', 'from p2']

陷阱3:for...in会遍历原型链上的可枚举属性

function Parent() {
    this.parentProp = 'parent';
}

Parent.prototype.inheritedProp = 'inherited';

function Child() {
    this.childProp = 'child';
}

Child.prototype = new Parent();

const child = new Child();

for (let key in child) {
    console.log(key); // childProp, parentProp, inheritedProp
}

解决方案:使用hasOwnProperty过滤:

for (let key in child) {
    if (child.hasOwnProperty(key)) {
        console.log(key); // childProp, parentProp
    }
}

原型链的最佳实践

实践1:使用Object.create设置原型链

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

Parent.prototype.sayName = function() {
    console.log(this.name);
};

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 最佳方式
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 添加子类方法
Child.prototype.sayAge = function() {
    console.log(this.age);
};

实践2:使用class语法(ES6+)

class GoodParent {
    constructor(name) {
        this.name = name;
    }
    
    sayName() {
        console.log(this.name);
    }
}

class GoodChild extends GoodParent {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
    
    sayAge() {
        console.log(this.age);
    }
}

实践3:安全地检查属性

const obj = { ownProp: 'value' };

// 不好的做法
if (obj.property) {
    // 如果property值为falsy(0, '', false, null, undefined),会被误判
}

// 好的做法
if (obj.hasOwnProperty('property')) {
    // 明确检查自身属性
}

// 更好的做法(防止hasOwnProperty被覆盖)
if (Object.prototype.hasOwnProperty.call(obj, 'property')) {
    // 最安全的方式
}

实践4:避免修改内置对象的原型

// 非必要情况,不得进行以下操作
if (!Array.prototype.customMethod) {
    Array.prototype.customMethod = function() {
        // 实现
    };
}

思考题:以下代码的输出是什么?为什么?

function Foo() {}
function Bar() {}

Bar.prototype = Object.create(Foo.prototype);

const bar = new Bar();

console.log(bar instanceof Bar); 
console.log(bar instanceof Foo); 
console.log(bar instanceof Object);

console.log(Bar.prototype.isPrototypeOf(bar)); 
console.log(Foo.prototype.isPrototypeOf(bar)); 
console.log(Object.prototype.isPrototypeOf(bar)); 

结语

原型链是 JavaScript 面向对象编程的基石,在 JavaScript 中没有真正的类,只有对象和它们之间的链接(原型链),对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!