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

理解并使用JavaScript原型链

2023-08-245.4k 阅读

一、JavaScript 中的对象

在 JavaScript 里,对象是一种复合数据类型,它可以包含多个键值对。这些键值对中的值可以是各种数据类型,包括基本数据类型(如字符串、数字、布尔值等),也可以是函数、数组甚至其他对象。

1.1 创建对象的方式

  1. 对象字面量方式

    const person = {
        name: 'John',
        age: 30,
        sayHello: function() {
            console.log(`Hello, my name is ${this.name}`);
        }
    };
    person.sayHello(); 
    

    在这个例子中,我们使用对象字面量创建了一个 person 对象。它有两个属性 nameage,以及一个方法 sayHellothis 在方法 sayHello 中指向调用该方法的对象,也就是 person

  2. 使用 new 关键字和构造函数

    function Animal(name, species) {
        this.name = name;
        this.species = species;
        this.introduce = function() {
            console.log(`I'm ${this.name}, a ${this.species}`);
        };
    }
    const dog = new Animal('Buddy', 'Dog');
    dog.introduce(); 
    

    这里定义了一个 Animal 构造函数,通过 new 关键字创建了 dog 对象。构造函数内部的 this 指向新创建的对象实例。

二、原型(Prototype)的概念

每个 JavaScript 对象(除了 null)都有一个与之关联的原型对象。原型对象本身也是一个对象,它为其他对象提供了共享的属性和方法。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 就会在其原型对象中查找。

2.1 原型与构造函数的关系

每个构造函数都有一个 prototype 属性,这个属性指向一个对象,也就是该构造函数创建的实例对象的原型对象。

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

在上述代码中,Person.prototype 是一个对象,当我们使用 new Person('Alice') 创建实例时,这个实例的原型就指向 Person.prototype

2.2 实例对象与原型的联系

实例对象通过 __proto__ 属性(现代 JavaScript 推荐使用 Object.getPrototypeOf() 方法获取)来访问其原型对象。

function Car(make, model) {
    this.make = make;
    this.model = model;
}
const myCar = new Car('Toyota', 'Corolla');
console.log(myCar.__proto__ === Car.prototype); 

这段代码验证了 myCar 实例的 __proto__ 属性确实指向 Car 构造函数的 prototype

三、原型链的形成

原型链是 JavaScript 实现继承的一种机制。当一个对象的属性或方法在自身找不到时,JavaScript 会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null)。

3.1 原型链的构建过程

假设我们有一个简单的继承结构,Animal 是父类,Dog 是子类。

function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound`);
};
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); 

在这个例子中,Dog.prototype 通过 Object.create(Animal.prototype) 创建,使得 Dog 的原型链继承自 Animal。这样,myDog 实例在查找 speak 方法时,会先在自身查找,找不到就会沿着原型链到 Dog.prototype 查找,再找不到就到 Animal.prototype 查找,最终找到 speak 方法并执行。

3.2 原型链的顶端

原型链的顶端是 Object.prototype,所有对象(除了 null)最终都继承自 Object.prototype。例如:

const num = 5;
console.log(num.toString()); 

这里,num 是一个基本数据类型的包装对象(JavaScript 内部会将基本数据类型临时包装成对象来调用方法),它的原型链最终会指向 Object.prototype,从而可以调用 toString 方法。

四、原型链与属性查找

当我们访问一个对象的属性时,JavaScript 遵循特定的顺序在原型链上查找该属性。

4.1 查找顺序

  1. 首先在对象自身查找属性。
  2. 如果在对象自身没有找到,就沿着原型链在原型对象中查找。
  3. 这个过程会一直持续,直到找到属性或者到达原型链顶端(Object.prototype,如果 Object.prototype 也没有该属性,则返回 undefined)。
function Shape() {
    this.color = 'black';
}
Shape.prototype.getColor = function() {
    return this.color;
};
function Rectangle(width, height) {
    Shape.call(this);
    this.width = width;
    this.height = height;
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
const rect = new Rectangle(5, 10);
console.log(rect.getColor()); 

rect 对象查找 getColor 方法时,先在 rect 自身找不到,然后在 Rectangle.prototype 中也找不到,最后在 Shape.prototype 中找到并调用。

4.2 遮蔽(Shadowing)

如果对象自身定义了与原型链上同名的属性,那么对象自身的属性会遮蔽原型链上的属性。

function Person(name) {
    this.name = name;
}
Person.prototype.name = 'Default Name';
const alice = new Person('Alice');
console.log(alice.name); 

这里 alice 对象自身的 name 属性遮蔽了 Person.prototype 上的 name 属性,所以输出 Alice

五、原型链与方法继承

方法继承是原型链的一个重要应用,通过原型链,子类可以继承父类的方法。

5.1 继承父类方法

function Vehicle(brand) {
    this.brand = brand;
}
Vehicle.prototype.drive = function() {
    console.log(`Driving a ${this.brand} vehicle`);
};
function Car(brand, model) {
    Vehicle.call(this, brand);
    this.model = model;
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
const myCar = new Car('Ford', 'Mustang');
myCar.drive(); 

在这个例子中,Car 类继承自 Vehicle 类,myCar 实例可以调用从 Vehicle.prototype 继承的 drive 方法。

5.2 重写父类方法

子类可以重写从父类继承的方法。

function Animal() {}
Animal.prototype.speak = function() {
    console.log('The animal makes a sound');
};
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
    console.log('The dog barks');
};
const myDog = new Dog();
myDog.speak(); 

这里 Dog 类重写了 Animal 类的 speak 方法,所以 myDog.speak() 输出 The dog barks

六、原型链与性能

虽然原型链是 JavaScript 实现继承的强大机制,但在使用时需要注意性能问题。

6.1 查找性能

随着原型链的增长,属性和方法的查找时间会增加。因为每次查找都需要沿着原型链一级一级往上找,直到找到目标或者到达顶端。例如,如果一个对象的原型链很长,访问一个深层原型上的属性可能会比较耗时。

function A() {}
function B() {}
function C() {}
function D() {}
B.prototype = Object.create(A.prototype);
C.prototype = Object.create(B.prototype);
D.prototype = Object.create(C.prototype);
const d = new D();
d.someProperty; 

在这个例子中,d 对象查找 someProperty 时,如果自身没有,就需要沿着 D -> C -> B -> A 的原型链查找,这比短原型链的查找要慢。

6.2 内存消耗

原型链也可能导致内存消耗问题。因为原型链上的属性和方法是共享的,如果有大量的对象实例,并且这些实例频繁访问原型链上的属性和方法,可能会占用较多的内存。特别是当原型链上有一些较大的对象或者函数时,这种情况更加明显。

七、在现代 JavaScript 中使用原型链

在现代 JavaScript 中,虽然有类(class)语法糖来实现继承,但底层依然是基于原型链的。理解原型链对于深入掌握 JavaScript 的运行机制非常重要。

7.1 类语法与原型链

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound`);
    }
}
class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
}
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); 

这里使用 class 语法定义了 AnimalDog 类,Dog 继承自 Animal。实际上,class 只是对原型链继承的一种封装,Dog.prototype 依然是基于 Animal.prototype 构建的原型链。

7.2 最佳实践

  1. 避免过长的原型链:尽量保持原型链简短,以提高属性查找性能。
  2. 合理使用原型属性和方法:对于一些共享的属性和方法,可以放在原型链上,但要注意内存消耗。
  3. 理解原型链的动态性:JavaScript 中原型链是动态的,可以在运行时修改,要谨慎使用这种特性,避免造成难以调试的问题。例如:
function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};
const alice = new Person('Alice');
Person.prototype.sayGoodbye = function() {
    console.log(`Goodbye, I'm ${this.name}`);
};
alice.sayGoodbye(); 

这里在创建 alice 实例后,又给 Person.prototype 添加了 sayGoodbye 方法,alice 实例也可以调用这个方法,这体现了原型链的动态性。

通过深入理解和合理使用 JavaScript 原型链,开发者可以更好地掌握 JavaScript 的对象系统和继承机制,编写出高效、可维护的代码。无论是简单的对象创建,还是复杂的继承体系构建,原型链都在其中发挥着核心作用。在实际开发中,结合现代 JavaScript 的语法特性,灵活运用原型链,能够让我们充分发挥 JavaScript 的强大功能。