深入探究 JavaScript 原型:理解面向对象编程的基石

44 阅读5分钟

引言

在 JavaScript 的世界里,原型(Prototype)是一个至关重要却又常让开发者困惑的概念。它是 JavaScript 实现面向对象编程的独特方式,与传统基于类的面向对象语言有所不同。本文将通过通俗易懂的例子,带你深入了解原型的方方面面,帮助你掌握这一核心知识。

一、从 “拿到小米 su7” 说起

想象我们要在代码中表示一辆小米 su7 汽车,这里可以借助 JavaScript 的类(通过构造函数模拟)和实例的概念。

  1. 类与构造函数:我们可以定义一个构造函数 Car,它就像一个汽车制造蓝图。例如:

javascript

function Car(color) {
    // this 指向实例对象
    // this.color = color;
    // this.name = '小米su7';
    // this.height = 1.4;
    // this.weight = 1.5;
    // this.long = 4800
}
  1. 创建实例:通过 new Car 的方式,我们可以创建出具体的汽车实例,就像从蓝图制造出真实的汽车。例如:

javascript

const car1 = new Car('霞光紫');
const car2 = new Car('海湾蓝');
  1. prototype 对象prototype 对象扮演着关键角色。它上面设置的属性和方法,能让这个类的所有实例共享。比如我们为 Car 的 prototype 对象添加属性和方法:

javascript

Car.prototype = {
    drive() {
        console.log('drive,下赛道');
    },
    name: '小米su7',
    height: 1.4,
    weight: 1.5,
    long: 4800,
};

这样,car1 和 car2 等所有 Car 类的实例都能访问到这些共享的属性和方法。

二、构造函数详解

  1. constructor 构造函数:当我们使用 new 关键字调用一个构造函数时,会发生一系列神奇的事情。构造函数内部的 this 会指向新创建的对象,这个过程就是构造新对象的过程。每个通过构造函数创建的实例都有自己独立的空间,比如我们可以在构造函数内部给实例添加独有的属性,像 this.color = color;。但如果在构造函数中定义过多属性和方法,每个实例都会重复拥有这些内容,造成内存浪费。

  2. prototype 对象 - 原型

    • 共享属性和方法prototype 对象的意义重大,它设置的属性和方法能被这个类的所有实例共享。这就好比孔子,虽然不是每个中国人都直接是孔子,但大家都能从文化传承(类似原型链)中受到孔子思想的影响。在 JavaScript 中,这是基于原型的面向对象编程方式。
    • 实例与原型的连接:每个函数都有一个 prototype 属性,它指向类的原型对象。这个对象的值是一个普通对象,里面的属性和方法会被所有实例共享。同时,每个实例都有一个 __proto__ 属性,它指向类的原型对象。通过这种连接,实例可以通过原型链找到类的原型对象上的属性和方法。例如,car1.__proto__ 就指向 Car.prototype,所以 car1 能访问到 Car.prototype 上定义的 drive 方法、name 属性等。
    • 与传统 class 的关系:JavaScript 原型式的面向对象和传统基于类(如 Java 等语言)的面向对象不同,它不是基于严格的血缘关系(类继承),而是通过原型链来实现类似的效果。在 ES6 引入 class 语法糖之前,JavaScript 一直通过这种原型式的方式实现面向对象编程。虽然 class 语法看起来更像传统的类,但本质上还是基于原型的。

三、代码实例分析

  1. 汽车构造函数示例

javascript

function Car(color) {
    // this 指向实例对象
    // this.color = color;
    // this.name = '小米su7';
    // this.height = 1.4;
    // this.weight = 1.5;
    // this.long = 4800
}
Car.prototype = {
    drive() {
        console.log('drive,下赛道');
    },
    name: '小米su7',
    height: 1.4,
    weight: 1.5,
    long: 4800,
};

const car1 = new Car('霞光紫');
console.log(car1, car1.name, car1.weight);
car1.drive();
const car2 = new Car('海湾蓝');
console.log(car2, car2.name, car2.weight);
  • 首先定义了 Car 构造函数,虽然这里注释掉了在构造函数内部为实例添加属性的代码,但我们知道如果在这里添加属性,每个实例都会有自己独立的这些属性。
  • 然后通过 Car.prototype 重新定义了原型对象,添加了 drive 方法以及一些汽车的属性。
  • 创建 car1 和 car2 两个实例,通过 console.log 可以看到它们都能访问到 prototype 对象上的属性,并且能调用 drive 方法。这充分展示了实例通过原型链访问原型对象属性和方法的过程。
  1. 人物构造函数示例

javascript

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.speci = '人类';
const p1 = new Person('张三', 18);
console.log(p1.name, p1.speci);
const p2 = new Person('李四', 20);
console.log(p2.name, p2.speci);
console.log(p1.__proto__, '////');
  • 定义 Person 构造函数,在构造函数内部为实例添加了 name 和 age 属性。
  • 通过 Person.prototype 添加了 speci 属性,代表人物的类别。
  • 创建 p1 和 p2 实例,它们都能访问到 prototype 上的 speci 属性。最后打印 p1.__proto__,可以看到它指向 Person.prototype,进一步验证了实例与原型对象的连接关系。

总结

JavaScript 的原型机制是其面向对象编程的核心。理解构造函数、prototype 对象以及实例之间的关系,对于编写高效、可维护的 JavaScript 代码至关重要。通过共享属性和方法,原型机制不仅节省了内存,还提供了一种独特的代码组织方式。希望通过本文的介绍,你对原型有了更清晰的认识,能够在实际开发中灵活运用这一强大的特性。