深入理解 `__proto__` 与 `prototype`:一张图彻底搞懂 JS 的原型指向

在JavaScript的深层机制中,__proto__prototype 是两个核心概念,它们共同构成了这门语言独特的原型继承模型。许多初学者,乃至一些有经验的开发者,都曾被这两个看似相似却职责截然不同的属性所困扰。要真正驾驭JavaScript,理解它们是绕不开的关键一步。今天,我们将抽丝剥茧,深入探讨 __proto__prototype 的本质、作用及其在原型链中的精妙互动。


一、 JavaScript对象的核心:从何而来,如何继承?

JavaScript是一种面向对象的语言,但它与传统的基于类的语言(如Java、C++)有着根本的区别。在JavaScript中,没有“类”的概念(ES6引入的class关键字只是语法糖,其底层依然是原型),对象之间通过“原型”建立联系,实现属性和方法的共享。这种机制被称为“原型继承”。

想象一下,你有一张蓝图,可以根据这张蓝图制造出许多结构相似的产品。这些产品拥有蓝图上定义的基本特征,但每个产品也可以有自己独特的修改。在JavaScript中,这张“蓝图”就是原型,而产品就是对象实例。理解__proto__prototype,就是理解这张蓝图如何被创建、如何被关联到产品,以及产品如何沿着这条关联线找到蓝图上的特征。


二、 prototype 属性:构造函数的蓝图

我们首先来认识 prototype

2.1 prototype 是什么?

prototype 是一个函数才拥有的特殊属性。更准确地说,它是一个构造函数(或任何可以作为构造函数使用的函数)拥有的属性。这个属性的值是一个对象,我们称之为“原型对象”。

当一个函数被设计为通过 new 关键字来创建新对象时,这个函数的 prototype 属性就显得尤为重要。它承载着所有由该构造函数创建的实例对象所能共享的属性和方法。

2.2 prototype 的作用

prototype 属性的核心作用是为由该函数构造出来的实例提供共享的属性和方法

为什么需要共享?考虑以下场景:如果你有100个Person对象,每个对象都有一个sayHello方法。如果sayHello方法直接定义在每个实例上,那么内存中就会有100份一模一样的sayHello函数代码,这显然是巨大的资源浪费。

sayHello定义在Person.prototype上,所有的Person实例就都能访问到同一个sayHello函数,大大节省了内存,并提升了代码的复用性。

2.3 代码示例:prototype 的基本使用

// 1. 定义一个构造函数 Person
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 2. 在 Person 的 prototype 属性上添加方法
// 所有的 Person 实例都将共享这个 sayHello 方法
Person.prototype.sayHello = function() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

// 3. 在 Person 的 prototype 属性上添加属性
// 所有的 Person 实例都将共享这个 species 属性
Person.prototype.species = "Human";

// 4. 创建 Person 的实例
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);

// 5. 访问实例的属性和方法
person1.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
person2.sayHello(); // 输出: Hello, my name is Bob and I am 25 years old.

console.log(person1.species); // 输出: Human
console.log(person2.species); // 输出: Human

// 6. 验证方法和属性是共享的
console.log(person1.sayHello === person2.sayHello); // 输出: true
console.log(person1.species === person2.species);   // 输出: true

// 7. 验证实例本身并没有这些方法和属性,它们是继承而来的
console.log(person1.hasOwnProperty('sayHello')); // 输出: false
console.log(person1.hasOwnProperty('species'));   // 输出: false
console.log(person1.hasOwnProperty('name'));      // 输出: true

从上面的例子可以看出,sayHellospecies被定义在Person.prototype上,而不是直接在Person构造函数内部(那样会为每个实例创建独立的副本)。当person1person2尝试访问sayHellospecies时,JavaScript会沿着原型链查找,最终在Person.prototype上找到它们。

2.4 prototype 对象的 constructor 属性

每个 prototype 对象都会自动获得一个 constructor 属性,这个属性指向它关联的构造函数本身。

function MyFunction() {}

console.log(MyFunction.prototype.constructor === MyFunction); // 输出: true

const obj = new MyFunction();
console.log(obj.constructor === MyFunction); // 输出: true
// 实际上,obj.constructor 是通过原型链找到 MyFunction.prototype.constructor

这个 constructor 属性在某些情况下很有用,例如当你需要知道一个对象是由哪个构造函数创建的。

2.5 构造函数、实例与 prototype 属性的关系概览

概念 类型 prototype 属性 作用 示例
构造函数 function 有,指向一个原型对象 定义实例的初始属性,并关联其原型对象 Person
原型对象 object 无(除非它也是一个函数) 存储所有实例共享的方法和属性 Person.prototype
实例对象 object 拥有自己的属性,并通过原型链继承原型对象上的属性和方法 person1, person2

三、 __proto__ 属性:原型链的链接器

现在我们来揭开 __proto__ 的神秘面纱。

3.1 __proto__ 是什么?

__proto__ 是一个对象才拥有的特殊属性(注意:函数也是对象,所以函数也有__proto__)。它是一个内部属性,通常被称为“隐式原型”或“原型链接”。在现代JavaScript中,推荐使用 Object.getPrototypeOf() 来访问它,以避免直接操作这个非标准(但广泛实现)的属性。

__proto__ 的值是一个指向另一个对象的引用,这个被指向的对象就是当前对象的原型。

3.2 __proto__ 的作用:构建原型链

__proto__ 的核心作用是建立对象之间的原型链。当JavaScript引擎在访问一个对象的属性时,如果该对象本身没有这个属性,它就会沿着__proto__指向的原型对象继续查找,如果原型对象也没有,就继续沿着原型对象的__proto__向上查找,直到找到该属性或到达原型链的末端(null)。这个查找过程就是“原型链查找”。

3.3 代码示例:__proto__ 的原型链查找

我们继续使用 Person 构造函数和 person1 实例:

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

Person.prototype.sayHello = function() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

Person.prototype.species = "Human";

const person1 = new Person("Alice", 30);

// 1. 验证 person1 的 __proto__ 指向 Person.prototype
console.log(person1.__proto__ === Person.prototype); // 输出: true

// 2. 验证原型链的下一环:Person.prototype 的 __proto__ 指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // 输出: true

// 3. 验证原型链的终点:Object.prototype 的 __proto__ 是 null
console.log(Object.prototype.__proto__ === null); // 输出: true

// 4. 属性查找过程演示
// 当访问 person1.sayHello() 时:
// - person1 自身没有 sayHello 属性。
// - 查找 person1.__proto__ (即 Person.prototype)。
// - Person.prototype 上有 sayHello 属性,执行它。
person1.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.

// 当访问 person1.toString() 时:
// - person1 自身没有 toString 属性。
// - 查找 person1.__proto__ (即 Person.prototype)。
// - Person.prototype 自身没有 toString 属性。
// - 查找 Person.prototype.__proto__ (即 Object.prototype)。
// - Object.prototype 上有 toString 属性,执行它。
console.log(person1.toString()); // 输出: [object Object]

// 5. 动态修改原型链上的属性
Person.prototype.species = "Homo Sapiens"; // 修改原型上的属性
console.log(person1.species); // 输出: Homo Sapiens (因为 person1 自身没有 species,会沿着原型链找到修改后的值)

// 6. 实例“覆盖”原型链上的属性(shadowing)
person1.species = "Alien"; // 给 person1 自身添加一个 species 属性
console.log(person1.species); // 输出: Alien (此时访问的是 person1 自身的属性)
console.log(person1.__proto__.species); // 输出: Homo Sapiens (原型上的属性并没有改变)
delete person1.species; // 删除 person1 自身的 species 属性
console.log(person1.species); // 输出: Homo Sapiens (再次访问时,会沿着原型链找到原型上的属性)

从上述例子可以看出,__proto__ 就像一条链条,将对象与它们的上层原型连接起来。当你在一个对象上查找属性时,JavaScript会沿着这条链条向上寻找,直到找到匹配的属性或者链条的末尾。

3.4 Object.getPrototypeOf()__proto__

__proto__ 是一个历史遗留的、非标准的属性,但由于其普遍实现,它在很多地方仍然被使用。然而,ECMAScript标准推荐使用 Object.getPrototypeOf() 来获取一个对象的原型,以及 Object.setPrototypeOf() 来设置一个对象的原型。

// 获取原型
console.log(Object.getPrototypeOf(person1) === Person.prototype); // 输出: true

// 设置原型(一般不推荐在生产代码中频繁使用,因为它可能导致性能问题)
const obj = {};
const protoObj = { z: 30 };
Object.setPrototypeOf(obj, protoObj);
console.log(obj.z); // 输出: 30
console.log(obj.__proto__ === protoObj); // 输出: true

3.5 函数的 __proto__ 属性

函数本身也是对象,所以它们也有 __proto__。所有函数(包括构造函数,如Person)的 __proto__ 都指向 Function.prototypeFunction.prototype 也是一个对象,它的 __proto__ 又指向 Object.prototype

function Person() {}

console.log(Person.__proto__ === Function.prototype); // 输出: true
console.log(Function.prototype.__proto__ === Object.prototype); // 输出: true
console.log(Object.prototype.__proto__ === null); // 输出: true

这条链条描述了函数作为对象本身的继承关系,与函数作为构造函数创建的实例的继承关系是并行的、但又有所交织的两个概念。


四、 __proto__prototype 的精妙互联

现在,我们把 __proto__prototype 放在一起看,它们的联系是理解JavaScript原型继承的关键。

4.1 核心连接:new 操作符的魔力

当你使用 new 操作符调用一个构造函数时,会发生以下关键步骤:

  1. 创建一个新对象:JavaScript引擎会创建一个全新的空对象。
  2. 建立原型链接:这个新创建的对象的 __proto__ 属性会被链接到构造函数的 prototype 属性所指向的原型对象
    新对象.__proto__ = 构造函数.prototype; 这是 __proto__prototype 发生关联的核心点。
  3. 绑定 this 并执行构造函数:构造函数被调用,this 上下文会被绑定到这个新创建的对象上。构造函数内部的 this.property = value; 语句会给这个新对象添加自身的属性。
  4. 返回新对象:如果构造函数没有显式返回一个非原始值(对象、数组、函数等),那么就会返回这个新创建的对象。

简而言之:new Constructor() 产生的实例对象,其 __proto__ 会指向 Constructor.prototype

4.2 图示化关系(文字描述)

让我们用一个简化的文本图来描述这种关系:

// 构造函数
function Constructor() { /* ... */ }

// Constructor 的 prototype 属性
Constructor.prototype
    (这是一个普通对象,上面可以定义共享的方法和属性,如 sayHello)
    .constructor -> 指向 Constructor

// 通过 new Constructor() 创建的实例
const instance = new Constructor();

// 实例与原型对象的链接:
instance.__proto__ === Constructor.prototype  // !!!核心连接点

进一步扩展到原型链的根:

// 构造函数链 (函数作为对象)
Constructor.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

// 实例链 (实例对象)
instance.__proto__ === Constructor.prototype
Constructor.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

这里需要注意的是,Constructor.prototype 是一个普通对象,它的__proto__指向Object.prototype。这意味着,即使是原型对象本身,也继承了Object.prototype上的方法(如toStringhasOwnProperty)。

4.3 代码示例:深入理解 new 的机制

function Vehicle(type) {
    this.type = type;
}

Vehicle.prototype.getModel = function() {
    return `This is a ${this.type} model.`;
};

const car = new Vehicle("Car");

// 1. 验证 car 的直接属性
console.log(car.type); // 输出: Car
console.log(car.hasOwnProperty('type')); // 输出: true

// 2. 验证 car 继承的方法
console.log(car.getModel()); // 输出: This is a Car model.
console.log(car.hasOwnProperty('getModel')); // 输出: false

// 3. 验证 car 与 Vehicle.prototype 的链接
console.log(car.__proto__ === Vehicle.prototype); // 输出: true
console.log(Object.getPrototypeOf(car) === Vehicle.prototype); // 输出: true

// 4. 验证 Vehicle.prototype 的原型
console.log(Vehicle.prototype.__proto__ === Object.prototype); // 输出: true

// 5. 验证 Vehicle 构造函数作为对象的原型
console.log(Vehicle.__proto__ === Function.prototype); // 输出: true
console.log(Function.prototype.__proto__ === Object.prototype); // 输出: true

这个例子清晰地展示了:

  • car 实例拥有自己的 type 属性。
  • car 实例通过 __proto__ 链接到 Vehicle.prototype,从而获得了 getModel 方法。
  • Vehicle.prototype 本身又通过其 __proto__ 链接到 Object.prototype,因此 Vehicle.prototype 上的方法(虽然例子中没有直接展示)和 car 实例都能访问到 Object.prototype 上的通用方法。
  • Vehicle 函数作为一个对象,它的原型链则通过 Function.prototype 最终连到 Object.prototype

五、 原型链的实际应用:继承与内置对象

理解了 __proto__prototype 的关系,我们就能更好地掌握JavaScript的继承机制,以及内置对象的工作原理。

5.1 模拟经典继承:构造函数之间的继承

在ES6的 class 语法糖出现之前,JavaScript中实现“类”继承,通常需要手动操作原型链。

// 父构造函数
function Animal(name) {
    this.name = name;
    this.age = 0; // 默认年龄
}

Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound.`);
};

Animal.prototype.eat = function() {
    console.log(`${this.name} is eating.`);
};

// 子构造函数
function Dog(name, breed) {
    // 1. 调用父构造函数,继承父类的实例属性
    Animal.call(this, name); // 关键:绑定 this
    this.breed = breed;
}

// 2. 继承父类的原型方法
// Dog.prototype = Animal.prototype; // 错误!这样会导致 Dog.prototype 的修改影响 Animal.prototype
// Dog.prototype = new Animal(); // 也可以,但会创建不必要的 Animal 实例,且可能执行父构造函数的副作用
Dog.prototype = Object.create(Animal.prototype); // 推荐方式:创建一个新对象,其 __proto__ 指向 Animal.prototype

// 3. 修复 constructor 指向
Dog.prototype.constructor = Dog;

// 4. 为 Dog 添加自己的原型方法
Dog.prototype.bark = function() {
    console.log(`${this.name} barks! Woof!`);
};

const myDog = new Dog("Buddy", "Golden Retriever");

console.log(myDog.name);  // Buddy (来自 Animal 构造函数)
console.log(myDog.breed); // Golden Retriever (来自 Dog 构造函数)
myDog.speak(); // Buddy makes a sound. (来自 Animal.prototype)
myDog.eat();   // Buddy is eating. (来自 Animal.prototype)
myDog.bark();  // Buddy barks! Woof! (来自 Dog.prototype)

// 验证原型链
console.log(myDog.__proto__ === Dog.prototype);                 // true
console.log(Dog.prototype.__proto__ === Animal.prototype);      // true
console.log(Animal.prototype.__proto__ === Object.prototype);   // true

这里的关键在于 Dog.prototype = Object.create(Animal.prototype);Object.create() 创建了一个新对象,并将其 __proto__ 设置为 Animal.prototype。这样,Dog.prototype 就拥有了 Animal.prototype 作为其原型,从而实现了方法继承。同时,Dog.prototype 自身可以添加 bark 等特有方法,而不会影响到 Animal.prototype。最后,Dog.prototype.constructor = Dog; 修复了 constructor 指向,确保 myDog.constructor 正确指向 Dog

5.2 Object.prototype:原型链的根

几乎所有JavaScript对象(除了那些通过 Object.create(null) 创建的纯粹对象)的原型链最终都会追溯到 Object.prototypeObject.prototype 是JavaScript中所有对象的终极原型,它包含了许多通用的方法,例如:

  • toString()
  • hasOwnProperty()
  • isPrototypeOf()
  • valueOf()

这些方法在任何普通对象上都可以调用,因为它们通过原型链被继承。

const obj = {}; // 等同于 new Object();
console.log(obj.__proto__ === Object.prototype); // true

const arr = []; // 等同于 new Array();
console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true

function foo() {}
console.log(foo.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true

5.3 内置对象与它们的原型

JavaScript的内置对象(如ArrayStringNumberDateFunction等)也遵循相同的原型机制。它们的构造函数拥有 prototype 属性,其值是各自的原型对象,包含了该类型实例的特有方法。

内置构造函数 实例类型 实例的 __proto__ 指向 原型对象的 __proto__ 指向 示例方法
Array [] Array.prototype Object.prototype push, pop, forEach
String "" String.prototype Object.prototype length, toUpperCase
Number 123 Number.prototype Object.prototype toFixed, toExponential
Function function Function.prototype Object.prototype call, apply, bind
Object {} Object.prototype null toString, hasOwnProperty

示例:数组的原型链

const myArray = [1, 2, 3];

// myArray 实例的隐式原型指向 Array.prototype
console.log(myArray.__proto__ === Array.prototype); // true

// Array.prototype 的隐式原型指向 Object.prototype
console.log(Array.prototype.__proto__ === Object.prototype); // true

// Object.prototype 的隐式原型指向 null,原型链的终点
console.log(Object.prototype.__proto__ === null); // true

// 调用数组方法
myArray.push(4); // push 方法来自 Array.prototype
console.log(myArray); // [1, 2, 3, 4]

// 调用对象通用方法
console.log(myArray.hasOwnProperty(0)); // hasOwnProperty 方法来自 Object.prototype

六、 实际应用与最佳实践

理解 __proto__prototype 不仅仅是理论知识,它对编写高效、可维护的JavaScript代码至关重要。

6.1 内存效率和共享方法

始终将方法定义在构造函数的 prototype 上,而不是在构造函数内部。这是为了实现方法共享,避免为每个实例创建独立的函数副本,从而节省内存。

不推荐:

function BadPerson(name) {
    this.name = name;
    this.sayHello = function() { // 每个实例都会创建自己的 sayHello 函数
        console.log(`Hello, ${this.name}`);
    };
}

推荐:

function GoodPerson(name) {
    this.name = name;
}
GoodPerson.prototype.sayHello = function() { // 所有实例共享同一个 sayHello 函数
    console.log(`Hello, ${this.name}`);
};

6.2 hasOwnProperty() 的重要性

for...in 循环会遍历对象自身以及原型链上可枚举的属性。为了避免遍历到继承的属性,或者在处理对象属性时只关注对象自身的属性,应该结合 hasOwnProperty() 方法使用。

function Car() {
    this.brand = 'Toyota';
}
Car.prototype.wheels = 4;

const myCar = new Car();

for (let key in myCar) {
    if (myCar.hasOwnProperty(key)) {
        console.log(`Own property: ${key}: ${myCar[key]}`); // 输出: Own property: brand: Toyota
    } else {
        console.log(`Inherited property: ${key}: ${myCar[key]}`); // 输出: Inherited property: wheels: 4
    }
}

6.3 ES6 class 语法糖下的原型

ES6引入的 class 关键字提供了一种更简洁、更符合传统面向对象习惯的方式来创建构造函数和处理继承。但要记住,class 只是语法糖,其底层仍然是基于原型链的。

// ES5 方式
function Person(name) {
    this.name = name;
}
Person.prototype.greet = function() {
    console.log(`Hello from ${this.name}`);
};

function Student(name, studentId) {
    Person.call(this, name);
    this.studentId = studentId;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function() {
    console.log(`${this.name} (ID: ${this.studentId}) is studying.`);
};

// ES6 class 方式
class PersonClass {
    constructor(name) {
        this.name = name;
    }
    greet() { // 这个方法会自动添加到 PersonClass.prototype
        console.log(`Hello from ${this.name}`);
    }
}

class StudentClass extends PersonClass {
    constructor(name, studentId) {
        super(name); // 相当于 PersonClass.call(this, name);
        this.studentId = studentId;
    }
    study() { // 这个方法会自动添加到 StudentClass.prototype
        console.log(`${this.name} (ID: ${this.studentId}) is studying.`);
    }
}

const student = new StudentClass("Charlie", "S123");
student.greet(); // Hello from Charlie
student.study(); // Charlie (ID: S123) is studying.

// 验证 class 底层依然是原型
console.log(student.__proto__ === StudentClass.prototype); // true
console.log(StudentClass.prototype.__proto__ === PersonClass.prototype); // true
console.log(StudentClass.prototype.constructor === StudentClass); // true

class 语法自动处理了 prototype 的设置、constructor 的指向以及 super 关键字来调用父类构造函数和方法,使得原型继承的实现更加清晰和简洁。

6.4 避免直接修改 __proto__Object.prototype

  • 修改 __proto__:虽然可以通过 obj.__proto__ = anotherObj;Object.setPrototypeOf(obj, anotherObj); 来改变一个对象的原型,但这通常会导致严重的性能问题,尤其是在旧版JavaScript引擎中。因为它会改变对象的内部结构,导致V8等引擎无法进行优化。此外,它还可能使代码难以理解和调试。因此,除非有非常特殊和明确的理由,否则不建议在运行时修改一个对象的原型。
  • 修改 Object.prototype 或其他内置原型:向 Object.prototypeArray.prototype 等内置原型添加属性或方法被称为“原型污染”。这会影响到所有继承自该原型的对象,可能导致命名冲突、意外行为,甚至安全漏洞,尤其是在大型项目或与第三方库集成时。因此,这是绝对应该避免的实践。

6.5 原型链的性能考量

原型链的长度会影响属性查找的性能。如果原型链过长,查找一个不存在的属性可能需要遍历很多层,从而增加查找时间。在大多数实际应用中,这种性能影响微乎其微,除非你构建了极其深奥的原型继承结构。合理设计原型链,保持其扁平化,有助于维持良好的性能。


七、 常见误区与澄清

  1. __proto__ 不是 prototype:这是最核心的误区。

    • prototype函数的一个属性,它指向一个对象(原型对象),这个对象包含了由该函数创建的实例所共享的方法和属性。
    • __proto__对象的一个内部属性(或访问器),它指向当前对象的原型,即它继承自的那个对象。它用于构建原型链。
    • 关系:实例.__proto__ === 构造函数.prototype
  2. JavaScript是基于原型的,而不是基于类的:即使有了class关键字,JavaScript的本质仍然是原型继承。class只是为原型继承提供了一个更方便、更熟悉的语法糖。

  3. 函数不继承自它们的 prototype 属性:一个函数(例如 Person)有 prototype 属性,但它自身并不继承 Person.prototype 上的方法。作为对象,Person 函数通过 Person.__proto__ 继承自 Function.prototypePerson.prototype 是为 new Person() 创建的实例准备的。

  4. 原型链不仅用于方法,也用于属性:原型链查找机制对属性和方法都适用。如果一个实例访问了一个属性,而这个属性不在实例自身上,JavaScript就会沿着原型链查找,直到找到该属性或链的末端。


八、 核心机制的掌握

深入理解__proto__prototype,意味着你掌握了JavaScript对象创建和继承的底层逻辑。prototype是构造函数定义共享特征的蓝图,而__proto__则是将这份蓝图实际链接到每个实例上的纽带,共同编织出JavaScript独特而强大的原型继承网络。这种机制不仅高效地实现了代码复用,也为JavaScript带来了极大的灵活性和动态性,是构建复杂应用不可或缺的基石。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注