MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的继承与原型链机制解析

2024-10-233.6k 阅读

JavaScript中的继承与原型链机制解析

一、JavaScript的继承概述

在面向对象编程中,继承是一个重要的概念。它允许一个对象获取另一个对象的属性和方法,从而实现代码的复用和层次化结构。在JavaScript中,由于它是一种基于原型的语言,其继承机制与传统的基于类的语言(如Java、C++)有所不同。

在基于类的语言中,类是对象的蓝图,通过类可以创建多个实例对象,继承是通过类之间的父子关系来实现的。而JavaScript没有类的概念(ES6之前),它是通过原型对象来实现继承的。每个对象都有一个原型对象,对象可以从原型对象继承属性和方法。

二、原型对象

  1. 原型对象的概念
    • 在JavaScript中,每个函数都有一个prototype属性,这个属性指向一个对象,这个对象就是该函数所创建的实例对象的原型对象。例如:
function Person() {}
console.log(Person.prototype);
- 当我们使用`new`关键字调用构造函数`Person`创建实例时,实例对象会与`Person.prototype`建立关联。
function Person() {}
let person = new Person();
console.log(person.__proto__ === Person.prototype); // true
- 这里的`__proto__`是实例对象指向其原型对象的属性(现代JavaScript中,建议使用`Object.getPrototypeOf()`方法来获取对象的原型,但`__proto__`在浏览器环境中仍然广泛支持)。

2. 原型对象的作用 - 原型对象的主要作用是实现属性和方法的共享。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null)。例如:

function Person() {}
Person.prototype.sayHello = function() {
    console.log('Hello!');
};
let person = new Person();
person.sayHello(); // Hello!
- 在上面的例子中,`person`对象本身没有`sayHello`方法,但由于它的原型对象`Person.prototype`有`sayHello`方法,所以可以通过`person.sayHello()`来调用。

三、原型链

  1. 原型链的形成
    • 原型链是由多个对象的原型相互连接形成的链条。每个对象都有一个__proto__属性,指向其原型对象,而原型对象本身也是一个对象,它也有自己的__proto__属性,这样就形成了一条链。例如:
function Animal() {}
Animal.prototype.eat = function() {
    console.log('I eat.');
};

function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.bark = function() {
    console.log('Woof!');
};

let dog = new Dog();
- 在这个例子中,`dog`对象的`__proto__`指向`Dog.prototype`,`Dog.prototype`的`__proto__`指向`Animal.prototype`,`Animal.prototype`的`__proto__`指向`Object.prototype`,而`Object.prototype`的`__proto__`为`null`,这样就形成了一条原型链:`dog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null`。

2. 原型链的查找机制 - 当访问一个对象的属性或方法时,JavaScript首先在对象本身查找,如果找不到,则沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null)。例如:

function Animal() {}
Animal.prototype.eat = function() {
    console.log('I eat.');
};

function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.bark = function() {
    console.log('Woof!');
};

let dog = new Dog();
dog.eat(); // I eat.
- 这里`dog`对象本身没有`eat`方法,所以JavaScript沿着原型链找到`Animal.prototype`上的`eat`方法并执行。

四、JavaScript中的继承方式

  1. 原型链继承
    • 原理:通过将子类型的原型对象设置为父类型的实例,从而实现子类型继承父类型的属性和方法。例如:
function Animal() {
    this.species = 'animal';
}
Animal.prototype.eat = function() {
    console.log('I eat.');
};

function Dog() {}
Dog.prototype = new Animal();
Dog.prototype.bark = function() {
    console.log('Woof!');
};

let dog = new Dog();
console.log(dog.species); // animal
dog.eat(); // I eat.
dog.bark(); // Woof!
- **缺点**:
  - 所有实例共享父类型构造函数中定义的属性。如果父类型构造函数中的属性是引用类型,那么一个实例对该属性的修改会影响其他实例。例如:
function Animal() {
    this.friends = ['cat'];
}
Animal.prototype.eat = function() {
    console.log('I eat.');
};

function Dog() {}
Dog.prototype = new Animal();
Dog.prototype.bark = function() {
    console.log('Woof!');
};

let dog1 = new Dog();
let dog2 = new Dog();
dog1.friends.push('mouse');
console.log(dog2.friends); // ['cat','mouse']
  - 创建子类型实例时,无法向父类型构造函数传递参数。

2. 借用构造函数继承(经典继承) - 原理:在子类型构造函数中通过callapply方法调用父类型构造函数,将父类型的属性和方法复制到子类型实例中。例如:

function Animal(name) {
    this.name = name;
    this.species = 'animal';
}

function Dog(name) {
    Animal.call(this, name);
    this.breed = 'dog';
}

let dog = new Dog('Buddy');
console.log(dog.name); // Buddy
console.log(dog.species); // animal
console.log(dog.breed); // dog
- **优点**:
  - 可以向父类型构造函数传递参数。
  - 每个实例都有自己独立的属性,不会相互影响。
- **缺点**:
  - 只能继承父类型构造函数中的属性和方法,无法继承原型对象上的属性和方法。例如:
function Animal(name) {
    this.name = name;
    this.species = 'animal';
}
Animal.prototype.eat = function() {
    console.log('I eat.');
};

function Dog(name) {
    Animal.call(this, name);
    this.breed = 'dog';
}

let dog = new Dog('Buddy');
// dog.eat(); // TypeError: dog.eat is not a function
  1. 组合继承
    • 原理:将原型链继承和借用构造函数继承结合起来。通过原型链继承原型对象上的属性和方法,通过借用构造函数继承构造函数中的属性和方法。例如:
function Animal(name) {
    this.name = name;
    this.species = 'animal';
}
Animal.prototype.eat = function() {
    console.log('I eat.');
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
    console.log('Woof!');
};

let dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.name); // Buddy
console.log(dog.species); // animal
console.log(dog.breed); // Golden Retriever
dog.eat(); // I eat.
dog.bark(); // Woof!
- **优点**:
  - 融合了原型链继承和借用构造函数继承的优点,既能继承原型对象上的属性和方法,又能保证每个实例有自己独立的属性。
- **缺点**:
  - 父类型构造函数会被调用两次,一次是在`new Animal()`创建子类型原型对象时,另一次是在`Animal.call(this)`创建子类型实例时。这会导致一些不必要的开销。

4. 原型式继承 - 原理:基于已有对象创建新对象,新对象会继承已有对象的属性和方法。使用Object.create()方法实现。例如:

let animal = {
    species: 'animal',
    eat: function() {
        console.log('I eat.');
    }
};

let dog = Object.create(animal);
dog.breed = 'dog';
dog.bark = function() {
    console.log('Woof!');
};

console.log(dog.species); // animal
dog.eat(); // I eat.
console.log(dog.breed); // dog
dog.bark(); // Woof!
- **缺点**:
  - 与原型链继承类似,所有实例共享原型对象上的引用类型属性。例如:
let animal = {
    friends: ['cat'],
    eat: function() {
        console.log('I eat.');
    }
};

let dog1 = Object.create(animal);
let dog2 = Object.create(animal);
dog1.friends.push('mouse');
console.log(dog2.friends); // ['cat','mouse']
  1. 寄生式继承
    • 原理:在原型式继承的基础上,对新创建的对象进行增强。例如:
function createDog(animal) {
    let dog = Object.create(animal);
    dog.breed = 'dog';
    dog.bark = function() {
        console.log('Woof!');
    };
    return dog;
}

let animal = {
    species: 'animal',
    eat: function() {
        console.log('I eat.');
    }
};

let dog = createDog(animal);
console.log(dog.species); // animal
dog.eat(); // I eat.
console.log(dog.breed); // dog
dog.bark(); // Woof!
- **缺点**:
  - 同样存在所有实例共享原型对象上引用类型属性的问题,并且没有解决代码复用的问题,每次创建新对象都需要重新定义增强的方法。

6. 寄生组合式继承 - 原理:这是一种最优的继承方式。它通过创建一个空函数,将其原型设置为父类型的原型,然后创建子类型的原型对象,这样既避免了父类型构造函数的多次调用,又能实现原型链继承。例如:

function inheritPrototype(subType, superType) {
    let prototype = Object.create(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

function Animal(name) {
    this.name = name;
    this.species = 'animal';
}
Animal.prototype.eat = function() {
    console.log('I eat.');
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

inheritPrototype(Dog, Animal);
Dog.prototype.bark = function() {
    console.log('Woof!');
};

let dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.name); // Buddy
console.log(dog.species); // animal
console.log(dog.breed); // Golden Retriever
dog.eat(); // I eat.
dog.bark(); // Woof!
- **优点**:
  - 解决了组合继承中父类型构造函数被调用两次的问题,同时保留了组合继承的优点,是一种高效的继承方式。

五、ES6类与继承

  1. ES6类的基本概念
    • ES6引入了class关键字,使得JavaScript有了更接近传统基于类的语言的语法。class实际上是一个语法糖,它背后仍然是基于原型的继承。例如:
class Animal {
    constructor(name) {
        this.name = name;
        this.species = 'animal';
    }
    eat() {
        console.log('I eat.');
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    bark() {
        console.log('Woof!');
    }
}

let dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.name); // Buddy
console.log(dog.species); // animal
console.log(dog.breed); // Golden Retriever
dog.eat(); // I eat.
dog.bark(); // Woof!
- 在上面的代码中,`class Animal`定义了一个父类,`class Dog extends Animal`定义了一个子类,子类通过`extends`关键字继承父类。`constructor`方法是类的构造函数,`super`关键字用于调用父类的构造函数。

2. ES6类继承的实现原理 - ES6类继承本质上还是基于原型链的继承。当使用extends关键字时,JavaScript会自动设置子类的原型对象,使其__proto__指向父类的原型对象,同时设置子类实例的__proto__指向子类的原型对象。例如:

class Animal {}
class Dog extends Animal {}

let dog = new Dog();
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
- 这种实现方式与传统的寄生组合式继承类似,但语法更加简洁明了。

3. ES6类继承的特点 - 简洁的语法:相比传统的基于原型的继承方式,ES6类继承的语法更加直观和易于理解,符合面向对象编程的习惯。 - 严格的模式:ES6类中的代码默认处于严格模式,这有助于避免一些常见的JavaScript错误。 - 静态方法和属性:ES6类支持静态方法和属性,通过static关键字定义。静态方法和属性可以直接通过类名调用,而不需要创建类的实例。例如:

class Animal {
    static isAnimal(instance) {
        return instance instanceof Animal;
    }
}

class Dog extends Animal {}

let dog = new Dog();
console.log(Animal.isAnimal(dog)); // true

六、继承与原型链的实际应用

  1. 代码复用

    • 通过继承和原型链,可以将一些通用的属性和方法提取到父类或原型对象中,子类或实例对象可以复用这些代码。例如,在一个游戏开发中,可能有一个Character类作为所有游戏角色的基类,包含一些通用的属性(如生命值、攻击力)和方法(如移动、攻击),然后通过继承Character类创建不同类型的角色(如战士、法师),这些子类可以复用Character类的代码,同时添加自己特有的属性和方法。
  2. 插件开发

    • 在JavaScript插件开发中,继承和原型链也经常被用到。例如,开发一个图表插件,可能有一个基础的Chart类,定义了图表的基本结构和通用方法,然后通过继承Chart类创建不同类型的图表(如柱状图、折线图),每个子类可以根据自身需求重写或扩展父类的方法。
  3. 面向对象设计模式

    • 许多面向对象设计模式都依赖于继承和原型链。例如,工厂模式可以通过继承来创建不同类型的对象,策略模式可以通过继承和原型链来实现不同的算法策略。

七、总结

JavaScript的继承与原型链机制是其核心特性之一,虽然与传统基于类的语言有所不同,但通过原型对象和原型链实现了强大的代码复用和层次化结构。从早期的各种继承方式到ES6引入的类继承语法糖,JavaScript的继承机制不断发展和完善。理解继承与原型链的原理和应用,对于编写高质量、可维护的JavaScript代码至关重要。无论是在大型应用开发还是小型脚本编写中,正确运用继承和原型链都能提高代码的效率和可扩展性。同时,随着JavaScript的不断发展,开发者也需要关注新的特性和最佳实践,以更好地利用这一强大的机制。