前言
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,它指向函数对象
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
图解:
三、深入理解原型链
原型链是 JavaScript 实现继承的基础机制。每个对象都有一个原型,原型本身也是一个对象,它也有自己的原型,如此层层向上,直到一个对象的原型为 null(这通常是 Object.prototype 的原型),形成一条链式结构。
3.1 原型链查找机制
当我们访问一个对象的属性或方法时,JavaScript 引擎会按照以下顺序查找:
- 首先在对象自身属性中查找
- 如果找不到,则在其原型对象中查找
- 如果还找不到,则在原型对象的原型中查找,依此类推
- 直到找到属性或到达原型链末端(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
图解:
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"