JavaScript原型、原型链解析

63 阅读2分钟

前言

JavaScript 作为一门基于原型的语言,理解原型和原型链是掌握 JavaScript 面向对象编程的关键。本文将深入浅出地剖析 JavaScript 的原型机制,帮助您全面理解这一核心概念。

一、为什么需要原型

1.1 从对象创建说起

在理解原型之前,我们先看一个简单对象创建的例子:

// 简单对象字面量
const person = {
  name: 'John',
  age: 30,
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

这种方式创建少量对象没问题,但当需要创建多个相似对象时,就会出现代码冗余:

// 创建多个相似对象的问题
const person1 = {
  name: 'John',
  age: 30,
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

const person2 = {
  name: 'Jane',
  age: 25,
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
};
// 每个对象都有相同的greet方法,造成内存浪费

1.2 工厂函数模式

function createPerson(name, age) {
  return {
    name,
    age,
    greet() {
      console.log(`Hello, my name is ${this.name}`);
    }
  };
}

const person1 = createPerson('John', 30);
const person2 = createPerson('Jane', 25);

这样虽然解决了代码重复的问题,但每个对象仍然有自己独立的 greet 方法,仍然存在内存浪费。

二、原型(Prototype)的基本概念

2.1 什么是原型

  • 在 JavaScript 中,每个函数都有一个特殊的属性 prototype(显式原型属性),它指向一个对象,这个对象就是该函数的原型对象。
  • 原型对象中,有一个属性constructor,它指向函数对象
    image.png
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 为Person函数的原型添加方法
Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const person1 = new Person('John', 30);
const person2 = new Person('Jane', 25);

person1.greet(); // Hello, my name is John
person2.greet(); // Hello, my name is Jane

// greet方法现在在原型上,而不是每个实例上
console.log(person1.greet === person2.greet); // true

2.2 __proto__ 与原型对象

每个 JavaScript 对象(除 null 外)都有一个内部属性 [[Prototype]](隐式原型属性)(在大多数浏览器中可通过 __proto__ 访问),指向它的原型对象。

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

const person = new Person('John');

// person.__proto__ 指向 Person.prototype
console.log(person.__proto__ === Person.prototype); // true

// Person.prototype.__proto__ 指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // true

// Object.prototype.__proto__ 指向 null
console.log(Object.prototype.__proto__ === null); // true

图解: image2.png

三、深入理解原型链

原型链是 JavaScript 实现继承的基础机制。每个对象都有一个原型,原型本身也是一个对象,它也有自己的原型,如此层层向上,直到一个对象的原型为 null(这通常是 Object.prototype 的原型),形成一条链式结构。

3.1 原型链查找机制

当我们访问一个对象的属性或方法时,JavaScript 引擎会按照以下顺序查找:

  1. 首先在对象自身属性中查找
  2. 如果找不到,则在其原型对象中查找
  3. 如果还找不到,则在原型对象的原型中查找,依此类推
  4. 直到找到属性或到达原型链末端(null)
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const john = new Person('John');

// 1. 查找john自身的toString方法 → 未找到
// 2. 查找Person.prototype的toString方法 → 未找到
// 3. 查找Object.prototype的toString方法 → 找到
john.toString(); // [object Object]

3.2 完整的原型链结构

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

Person.prototype.species = 'Homo sapiens';

const john = new Person('John');

// 原型链关系:
// john -> Person.prototype -> Object.prototype -> null

console.log(john.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

// 属性查找演示
console.log(john.name); // "John" - 自身属性
console.log(john.species); // "Homo sapiens" - 来自Person.prototype
console.log(john.toString); // function - 来自Object.prototype

图解: image3.png

3.3 修改原型的影响

由于原型对象是共享的,对原型的修改会影响所有实例:

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

const person1 = new Person('John');
const person2 = new Person('Jane');

// 修改原型
Person.prototype.greet = function() {
  console.log(`Hello, I'm ${this.name}`);
};

// 所有实例都能访问新方法
person1.greet(); // Hello, I'm John
person2.greet(); // Hello, I'm Jane

// 甚至在实例创建后添加的方法也能访问

3.4 属性屏蔽

当实例自身属性与原型属性同名时,实例属性会"屏蔽"原型属性:

function Person() {}
Person.prototype.name = 'Prototype Name';

const person = new Person();
console.log(person.name); // "Prototype Name"

// 添加实例自身属性
person.name = 'Instance Name';
console.log(person.name); // "Instance Name" - 屏蔽了原型属性

// 删除自身属性后,又能访问原型属性
delete person.name;
console.log(person.name); // "Prototype Name"