🔥读懂 JS 原型链,轻松驾驭对象继承与代码复用

160 阅读4分钟

1 什么是原型?

1.1 定义

每个函数(Function) 都有一个 prototype 属性,这个属性是一个指针,指向一个对象,这个对象就是该函数的原型对象。

1.2 普通构造函数与原型的对比

这里有个构造函数Person:

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

New 两个实例对象:

const p1 = new Person ("张三")
const p2 = new Person ("小美")
console.log(p1 === p2) //false

两个实例对象虽然都是由同一个构造函数 new 出来的,但是他们是相互独立的,给他们添加一个共同方法:

function Person (name) {
    this.name = name;
    // 定义方法
    this.sayHello = function() {
        console.log("Hello, I'm " + this.name);
    };
}
let p1 = new Person("Alice");
let p2 = new Person("Bob");

p1.sayHello(); // Hello, I'm Alice
p2.sayHello(); // Hello, I'm Bob

但是问题来了,每次创建新实例时,都会重新创建该实例的所有方法,这会导致较高的内存开销,尤其是在创建大量对象时。

所以我们可以用原型来实现共享方法和属性。

function Person (name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log("Hello, I'm " + this.name);
};
let p1 = new Person("Alice");
let p2 = new Person("Bob");

p1.sayHello(); // Hello, I'm Alice
p2.sayHello(); // Hello, I'm Bob

console.log(p1.sayHello === p2.sayHello); // true,表明两个实例共享同一个方法

好处:

  1. 使用原型可以节省内存,所有实例共享原型上的方法,只有属性是各自独立的,从而大大减少了内存使用。
  2. 可以动态地向原型添加新的方法或属性,这些更改会立即反映在所有实例上。
  3. 通过原型链实现继承,允许子类继承父类的方法和属性。

缺点:

  1. 如果多个人都尝试修改同一个原型,可能会导致意外覆盖或冲突。

对比:

特性普通构造函数(不使用原型)使用原型
方法定义在构造函数内部为每个实例单独定义在构造函数的原型对象上定义
内存 使用高,每个实例都有自己的方法副本低,所有实例共享原型上的方法
灵活性较低,不易于扩展高,支持动态扩展和继承
维护成本简单但可能导致重复代码更加灵活但需要理解原型链

2 原型链

我们知道原型带来了很多好处,那他是如何实现方法属性共享的呢?

原型链。

原型链是实现继承的主要机制。

2.1 原型链的形成

当使用构造函数创建对象时,新创建的对象实例会有一个内部属性 [[Prototype]](通常用 .__proto__表示),它指向构造函数的 prototype 对象。而原型对象本身也可能有自己的 proto,这样就形成了一个链条,直到 Object.prototype,最终指向 null。

2.1.1 属性查找机制

当你访问一个对象的属性或方法时,JavaScript 引擎会先在该对象自身查找,如果没有找到,就会沿着原型链向上查找,直到找到为止,或者到达原型链末端(null)为止。

let obj = {};
console.log(obj.toString()); 
// 调用了 Object.prototype.toString()

虽然 obj 自己没有 toString() 方法,但它继承了 Object.prototype 上的该方法。

2.1.2 constructor 属性

原型对象 prototype 也是一个对象,也有自己的属性和方法。它上面默认有个 constructor 属性,指向构造函数。

function Star(){
}

此时:它的原型对象有以下属性,还没有方法 (要手动给它添加)

Star.prototype{
    constructor : Star // 指向构造函数
}

2.2 原型链的应用与继承

// 1.创建函数Animal
function Animal() {} 
// 2.给Animald的原型对象加上eat方法
Animal.prototype.eat = function() {
    console.log("Animal is eating.");
};
// 3.创建函数Dog
function Dog() {}
// 4.将Dog的原型对象指向Animal实例
Dog.prototype = new Animal(); // 继承 Animal
// 5.在Dog原型对象上添加bark方法
Dog.prototype.bark = function() {
    console.log("Woof!");
};
// 6.创建dog实例对象
let dog = new Dog();
// 7.调用原型链上的方法
dog.eat();  // 来自 Animal
dog.bark(); // 来自 Dog

在这个例子中,Dog 的原型指向了一个 Animal 实例,从而实现了继承。

他的原型链是这样的:

结语

原型是 JavaScript 中最具特色和魅力的特性之一,深入理解原型链的工作原理和应用技巧,不仅能让我们编写出更加高效、优雅的代码,还能帮助我们更好地理解 JavaScript 的面向对象编程模型。无论是性能优化、设计模式,还是解决复杂的业务逻辑,原型都能成为我们手中的得力武器。