JavaScript 面向对象探秘:从构造函数到原型链的优雅继承

0 阅读4分钟

引言:万物皆对象的困惑

在 JavaScript 的世界里,我们习惯了“万物皆对象”。但与 Java、C++ 等传统面向对象语言不同,JavaScript 并没有类(ES6 之前的语法糖之下)的概念,而是基于原型(Prototype)构建的。

初学者往往对 thisprototype__proto__ 感到困惑:为什么方法要写在 prototype 上?实例是如何访问到构造函数之外的方法的?今天,我们就通过几个简单的代码片段,带你彻底搞懂 JS 的原型式面向对象。


1. 构造函数:对象的“工厂”

在 ES5 时代,我们使用首字母大写的函数作为构造函数来创建对象。构造函数解决了我们需要批量生产相似对象的问题。

看下面这段代码:


function Car(color) {
  // this 指向新创建的实例
  this.color = color; 
}
// 共享属性
Car.prototype = {
  drive() { console.log('drive, 下赛道'); },
  name: 'su7'
}
const car1 = new Car('霞光紫');
car1.drive(); // "drive, 下赛道"

核心点: 构造函数内部的 this 指向新创建的实例,用于定义每个实例独有的属性(如 color)。


2. 原型(Prototype):共享的“基因库”

如果把所有方法都写在构造函数里,每次 new 一个对象,内存中就会多一份方法的副本,这非常浪费资源。

JavaScript 的解决方案是 prototype。正如文档 8.md 所述:

“prototype 属性的值是一个对象,它上面的属性和方法会被所有实例共享。”

我们来看一个经典的 Person 案例:


function Person(name, age) { 
  this.name = name; 
  this.age = age; 
}
// 将属性挂载到原型上
Person.prototype.speci = '人类'; 

const person1 = new Person('张三', 18);
console.log(person1.speci); // "人类"

关键机制: 实例对象内部有一个私有属性 __proto__(现在标准推荐使用 Object.getPrototypeOf()),它指向构造函数的 prototype 对象。当访问 person1.speci 时,如果实例上没有,引擎就会去 Person.prototype 上找。


3. 原型链继承:模拟“血缘关系”

传统的 Class 面向对象是“血缘关系”,而 JS 是“原型式”的。如何实现继承?答案是原型链

我们可以利用 prototype 指向另一个构造函数的实例,来实现属性的层层继承。:


var obj = { species: '动物' };
function Animal() { }
Animal.prototype = obj; // Animal 继承了 obj 的属性

function Person() { }
Person.prototype = new Animal(); // Person 继承了 Animal

var su = new Person();
console.log(su.species); // "动物"

继承逻辑:

  1. su__proto__ 指向 Person.prototype(即 new Animal())。
  2. new Animal()__proto__ 指向 Animal.prototype(即 obj)。
  3. 当查找 su.species 时,引擎会沿着这条链一直找到 obj 上的 species

4. 原型链的终点:Object.prototype

所有的对象,最终都会指向 Object.prototype。这也是为什么我们所有的对象都能调用 .toString() 方法的原因。

// 6.html
console.log(su.toString()); // 能调用,因为原型链最终指向了 Object.prototype
console.log(su.__proto__.__proto__); // 指向 Object.prototype

注意: Object.prototype__proto__ 指向 null,标志着原型链的结束。


5. 雷点和实践

在使用原型时,有一个容易踩的坑:

// 5.html
Person.prototype.species = '人类';
var su = new Person();
su.species = 'LOL达人'; // 这是在实例上新建了一个属性,而不是修改原型

解释: 当你给实例设置一个与原型同名的属性时,JS 引擎会在实例上直接创建该属性(遮蔽效应),而不会修改原型上的值。如果你删除了实例的这个属性,它依然会回到原型上取值。


结语

理解构造函数、实例与原型三者的关系,是掌握 JavaScript 面向对象的基石。

  • 构造函数是模版(Constructor)。
  • 实例是具体的对象。
  • 原型是所有实例共享的属性和方法的容器。
  • 原型链是实现继承的机制。

最后用一张图总结下 助你更好理解原型和构造函数

847b320a57fb482f15d997e0c39e016f.png

图的上半部分主要展示了自定义构造函数 Person 的内部关系:

  • 构造函数 Person

    • 它是一个函数,用来通过 new 关键字创建实例(如 new Person())。
    • 它有一个指向原型对象的属性:prototype
  • 原型对象 Person.prototype

    • 这是构造函数的“原型”,它是一个对象。
    • 它有一个指向构造函数的属性:constructor
    • 关系Person.prototype.constructor 指向 Person。这是一个循环引用,确保原型知道是谁构造了它。
  • 实例对象 person

    • 这是通过 new Person() 创建出来的具体对象。
    • 它有一个内部指针: __proto__ (注意:这是非标准但广泛支持的属性,标准中对应 [[Prototype]])。
    • 关系person.__proto__ 指向 Person.prototype。这是原型链的核心:实例通过这个指针去原型对象上查找方法和属性。

小结:实例的 __proto__ 指向构造函数的 prototype,而 prototype 的 constructor 又指回构造函数。

2. 继承的终点(下半部分)

图的下半部分展示了所有对象的最终归宿——Object

  • Object()

    • 这是 JS 内置的顶级构造函数。
    • 同样,Object.prototype 的 constructor 指向 Object
  • 连接点

    • 注意看中间那条向下的箭头:Person.prototype 的 __proto__ 指向了 Object.prototype
    • 这意味着Person 的原型对象本身也是一个对象(它是由 Object 构造出来的),所以它也要遵循对象的规则,去继承 Object.prototype 上的通用方法(如 toStringhasOwnProperty 等)。