JS OBJECT 4 prototype & __proto__

97 阅读4分钟

JavaScript 对象中的 prototype 和 proto(以及其标准表示 [[Prototype]])。这两个属性是理解 JavaScript 原型继承机制的关键

1. prototype 属性

  • 谁拥有它? 函数(特指构造函数,但实际上除了箭头函数和一些内建函数外,大多数函数都有 prototype 属性)。它不是普通对象实例的属性(除非那个对象恰好也是一个函数)。

  • 它是什么? 它是一个对象。当一个函数被定义时,会自动为它创建一个 prototype 属性,这个属性的值是一个对象,通常被称为“原型对象”。

  • 它的用途?

    • 作为模板/蓝图: 这个 prototype 对象是用来存放那些希望被 通过该构造函数创建的所有实例 共享的属性和方法的。
    • new 操作符的关键: 当使用 new 关键字调用一个构造函数来创建新对象时,新创建的对象的内部 [[Prototype]] 链接(也就是 proto 指向的)会被设置为指向该构造函数的 prototype 对象
  • 关键点: prototype 是构造函数用来“规定”其未来实例的原型(即实例应该从哪里继承属性和方法)。

示例:

function Dog(name) {
  this.name = name; // 实例自身的属性
}

// Dog 是一个函数,所以它有 prototype 属性
console.log(typeof Dog.prototype); // "object"
console.log(Dog.prototype);        // { constructor: ƒ Dog(), __proto__: Object } (默认包含 constructor 和继承自 Object.prototype)

// 向 Dog 的 prototype 对象添加共享方法
Dog.prototype.bark = function() {
  console.log(`Woof! My name is ${this.name}`);
};

// 使用 new 创建实例
const dog1 = new Dog('Buddy');
const dog2 = new Dog('Lucy');

// dog1 和 dog2 是对象实例,它们通常 *没有* 自己的 .prototype 属性
console.log(dog1.prototype); // undefined
console.log(dog2.prototype); // undefined

// 但 dog1 和 dog2 可以调用继承自 Dog.prototype 的方法
dog1.bark(); // Woof! My name is Buddy
dog2.bark(); // Woof! My name is Lucy
    

2. proto 属性 (或 [[Prototype]] 内部槽)

  • 谁拥有它? 几乎所有的 JavaScript 对象实例 都有一个内部链接指向它的原型。proto 是访问这个内部链接的一种(非标准但广泛实现的)方式。

  • 它是什么? 它是一个指向另一个对象的引用(指针) 。这个引用的对象就是当前对象的“原型”。

  • 它的用途?

    • 原型链查找: 当你试图访问一个对象的属性或方法时,如果对象本身没有找到,JavaScript 引擎就会通过 proto 链接去它的原型对象上查找。如果原型对象上还没有,就继续沿着原型对象的 proto 向上查找,直到找到该属性/方法,或者到达原型链的顶端(即 Object.prototype 的 proto,其值为 null)。
    • 实现继承: proto 构成了 JavaScript 中对象之间继承关系的实际链条。
  • 标准访问方式:

    • 获取对象的原型:Object.getPrototypeOf(obj)
    • 设置对象的原型:Object.setPrototypeOf(obj, prototypeObj) (ES6+,需谨慎使用,可能影响性能)
  • 关键点: proto实例对象用来“指向”其原型、实现属性/方法查找和继承的链接。

proto 与 prototype 的关系(核心)

当使用 new ConstructorFunction() 创建一个实例时,关键的连接发生了:

instance.proto 被设置为 ConstructorFunction.prototype

或者用标准方法表示:

Object.getPrototypeOf(instance) 指向 ConstructorFunction.prototype

示例(续):

function Cat(name) {
  this.name = name;
}
Cat.prototype.meow = function() {
  console.log(`Meow, I'm ${this.name}`);
};

const cat1 = new Cat('Whiskers');

// cat1 是一个对象实例,它有 __proto__
console.log(cat1.__proto__); // 指向 Cat.prototype 对象

// 验证这个核心关系
console.log(cat1.__proto__ === Cat.prototype); // true

// 使用标准方法验证
console.log(Object.getPrototypeOf(cat1) === Cat.prototype); // true

// Cat.prototype 也是一个对象,它也有 __proto__,指向 Object.prototype
console.log(Cat.prototype.__proto__ === Object.prototype); // true
console.log(Object.getPrototypeOf(Cat.prototype) === Object.prototype); // true

// Object.prototype 的 __proto__ 是 null,原型链顶端
console.log(Object.prototype.__proto__); // null
console.log(Object.getPrototypeOf(Object.prototype)); // null

// 属性查找示例
cat1.meow(); // 在 cat1 上找不到 meow,通过 __proto__ 找到 Cat.prototype.meow

console.log(cat1.toString()); // 1. cat1 上没有 ->
                              // 2. Cat.prototype 上没有 ->
                              // 3. Object.prototype 上找到 toString -> 调用

console.log(cat1.hasOwnProperty('name')); // 1. cat1 上没有 ->
                                        // 2. Cat.prototype 上没有 ->
                                        // 3. Object.prototype 上找到 hasOwnProperty -> 调用
                                        // hasOwnProperty 在 cat1 的上下文 (this) 中执行
                                        // -> true (因为 name 是 cat1 自身的属性)
    
总结对比
特性prototypeproto (或 [[Prototype]])
谁拥有?函数 (作为构造函数的模板)对象实例 (指向其继承来源)
是什么?一个对象,包含共享的属性/方法一个指向原型对象引用/链接
目的?定义实例应该继承自哪里实现实际的继承查找,构成原型链
关系实例.proto 指向 构造函数.prototype实例.proto 指向 构造函数.prototype
标准访问直接访问 FunctionName.prototypeObject.getPrototypeOf(obj) / Object.setPrototypeOf(obj)

简单记忆:

  • prototype 是函数(类)的属性,用来定义蓝图。
  • proto 是实例对象的属性,用来连接到蓝图,实现继承。

好的,这里有一些关于 JavaScript 构造函数、prototype 和 proto 的练习题,可以帮助你巩固理解。

练习 1:创建基本构造函数和实例

  1. 创建一个名为 Product 的构造函数。
  2. 该构造函数接收 name (字符串) 和 price (数字) 作为参数,并将它们设置为实例自身的属性。
  3. 在 Product.prototype 上添加一个名为 displayInfo 的方法,该方法在控制台打印产品的名称和价格,格式为 "Product: [name], Price: $[price]"。
  4. 使用 new 关键字创建两个 Product 实例:一个名为 "Laptop",价格为 1200;另一个名为 "Mouse",价格为 25。
  5. 调用这两个实例的 displayInfo 方法,检查输出是否正确。
  6. 使用 Object.getPrototypeOf() 检查其中一个实例的原型是否确实是 Product.prototype。
// ----- 在这里写你的代码 -----

// 1. 定义构造函数
function Product(/* ... */) {
  // 2. 设置实例属性
}

// 3. 添加 prototype 方法
Product.prototype.displayInfo = function() {
  // ...
};

// 4. 创建实例
const laptop = /* ... */;
const mouse = /* ... */;

// 5. 调用方法
laptop.displayInfo(); // 预期的输出: Product: Laptop, Price: $1200
mouse.displayInfo();  // 预期的输出: Product: Mouse, Price: $25

// 6. 检查原型
console.log(Object.getPrototypeOf(laptop) === Product.prototype); // 预期的输出: true
console.log(laptop instanceof Product);                        // 预期的输出: true

// -----------------------------
    

练习 2:hasOwnProperty 和原型链查找

基于练习 1 的 Product 构造函数和实例:

  1. 使用 hasOwnProperty 检查 laptop 实例是否直接拥有 name 属性和 price 属性。
  2. 使用 hasOwnProperty 检查 laptop 实例是否直接拥有 displayInfo 方法。
  3. 解释为什么第二个检查返回 false。
  4. 不使用 displayInfo,直接访问 laptop.toString()。思考并解释 toString 方法是从哪里来的(原型链上的哪个对象提供的)?
// ----- 在这里写你的代码 (假设练习 1 的代码已存在) -----

// 1. 检查自身属性
console.log("Laptop has own property 'name':", laptop.hasOwnProperty('name'));   // 预期: true
console.log("Laptop has own property 'price':", laptop.hasOwnProperty('price')); // 预期: true

// 2. 检查自身方法 (实际是原型上的)
console.log("Laptop has own property 'displayInfo':", laptop.hasOwnProperty('displayInfo')); // 预期: false

// 3. 解释:
// [在这里写下你的解释]
// ... 因为 displayInfo 方法是定义在 Product.prototype 上的,laptop 实例通过原型链继承了它,而不是直接拥有它。

// 4. 调用 toString 并解释来源
console.log(laptop.toString()); // 预期: "[object Object]" (或其他,取决于环境/覆盖)
// [在这里写下你的解释]
// ... laptop 自身没有 toString -> Product.prototype 上没有 toString -> Object.prototype 上有 toString,所以调用的是 Object.prototype.toString。

// -----------------------------
    

练习 3:共享状态 vs. 实例状态

  1. 创建一个名为 Counter 的构造函数。
  2. 在构造函数内部,使用 this.count = 0; 初始化一个实例属性 count。
  3. 在构造函数内部,添加一个名为 incrementInstance 的方法,该方法将 this.count 加 1。
  4. 在 Counter.prototype 上添加一个名为 sharedValue 的属性,并将其初始化为 0。
  5. 在 Counter.prototype 上添加一个名为 incrementShared 的方法,该方法将 this.constructor.prototype.sharedValue(或 Counter.prototype.sharedValue)加 1。注意:这里直接修改原型上的属性。
  6. 创建两个 Counter 实例:counterA 和 counterB。
  7. 调用 counterA.incrementInstance() 两次。
  8. 调用 counterB.incrementInstance() 一次。
  9. 打印 counterA.count 和 counterB.count。它们应该不同。
  10. 调用 counterA.incrementShared() 一次。
  11. 调用 counterB.incrementShared() 一次。
  12. 打印 counterA.sharedValue 和 counterB.sharedValue(或者直接打印 Counter.prototype.sharedValue)。它们应该相同,并且等于 2。
  13. 思考并解释 this.count 和 sharedValue 行为不同的原因。
// ----- 在这里写你的代码 -----

function Counter() {
  // 2. 实例属性
  this.count = 0;
  // 3. 实例方法
  this.incrementInstance = function() {
    this.count++;
  };
}

// 4. 共享属性
Counter.prototype.sharedValue = 0;

// 5. 共享方法 (修改共享属性)
Counter.prototype.incrementShared = function() {
  // this.constructor.prototype.sharedValue++; // 也可以
  Counter.prototype.sharedValue++;
};

// 6. 创建实例
const counterA = new Counter();
const counterB = new Counter();

// 7. 操作 counterA 实例
counterA.incrementInstance();
counterA.incrementInstance();

// 8. 操作 counterB 实例
counterB.incrementInstance();

// 9. 打印实例 count
console.log("counterA.count:", counterA.count); // 预期: 2
console.log("counterB.count:", counterB.count); // 预期: 1

// 10. 操作 counterA 共享
counterA.incrementShared();

// 11. 操作 counterB 共享
counterB.incrementShared();

// 12. 打印共享 value
console.log("counterA.sharedValue:", counterA.sharedValue); // 预期: 2
console.log("counterB.sharedValue:", counterB.sharedValue); // 预期: 2
console.log("Counter.prototype.sharedValue:", Counter.prototype.sharedValue); // 预期: 2

// 13. 解释
// [在这里写下你的解释]
// ... this.count 是每个实例独有的属性,修改一个实例的 count 不会影响另一个。
// ... sharedValue 是定义在 prototype 上的属性,所有实例共享同一个 prototype,
// ... 因此通过任何实例(或直接通过 Counter.prototype)修改 sharedValue 都会影响所有实例访问到的值。

// -----------------------------
    

练习 4:简单原型继承

  1. 创建一个名为 Animal 的构造函数,接收 name 参数,并将其设置到 this.name。
  2. 在 Animal.prototype 上添加一个 speak 方法,打印 "Some generic sound"。
  3. 创建一个名为 Dog 的构造函数,接收 name 和 breed 参数。
  4. 在 Dog 构造函数内部,使用 Animal.call(this, name) 来调用父构造函数,以设置 name 属性。
  5. 将 breed 设置到 this.breed。
  6. 关键步骤: 设置 Dog.prototype 继承自 Animal.prototype。常用的方法是 Object.create(Animal.prototype)。
  7. 关键步骤: 修复 Dog.prototype.constructor,使其指向 Dog 本身。
  8. 在 Dog.prototype 上覆盖 speak 方法,使其打印 "Woof! Woof!"。
  9. 在 Dog.prototype 上添加一个 fetch 方法,打印 "[name] is fetching the ball."。
  10. 创建一个 Dog 实例,例如 const myDog = new Dog('Buddy', 'Golden Retriever');。
  11. 调用 myDog.speak(),检查输出是否为 "Woof! Woof!"。
  12. 调用 myDog.fetch(),检查输出是否包含正确的名字。
  13. 检查 myDog instanceof Dog 和 myDog instanceof Animal 的结果(都应为 true)。
  14. 检查 Object.getPrototypeOf(myDog) 是否等于 Dog.prototype。
  15. 检查 Object.getPrototypeOf(Dog.prototype) 是否等于 Animal.prototype。
// ----- 在这里写你的代码 -----

// 1. 父构造函数 Animal
function Animal(name) {
  this.name = name;
}

// 2. 父 prototype 方法
Animal.prototype.speak = function() {
  console.log("Some generic sound");
};

// 3. 子构造函数 Dog
function Dog(name, breed) {
  // 4. 调用父构造函数
  Animal.call(this, name);
  // 5. 设置子特有属性
  this.breed = breed;
}

// 6. 设置原型继承 !!!
Dog.prototype = Object.create(Animal.prototype);

// 7. 修复 constructor !!!
Dog.prototype.constructor = Dog;

// 8. 覆盖父方法
Dog.prototype.speak = function() {
  console.log("Woof! Woof!");
};

// 9. 添加子特有方法
Dog.prototype.fetch = function() {
  console.log(`${this.name} is fetching the ball.`);
};

// 10. 创建实例
const myDog = new Dog('Buddy', 'Golden Retriever');

// 11. 调用覆盖的方法
myDog.speak(); // 预期: Woof! Woof!

// 12. 调用子特有方法
myDog.fetch(); // 预期: Buddy is fetching the ball.

// 13. 检查 instanceof
console.log("myDog instanceof Dog:", myDog instanceof Dog);     // 预期: true
console.log("myDog instanceof Animal:", myDog instanceof Animal); // 预期: true

// 14. 检查实例原型
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // 预期: true

// 15. 检查子原型的原型 (继承链)
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // 预期: true

// -----------------------------
    

练习 5:预测输出和理解 proto

仔细阅读下面的代码,预测每一行 console.log 的输出,并简要解释原因。

function Foo(id) {
  this.id = id;
}
Foo.prototype.identify = function() {
  console.log("Foo prototype identify:", this.id);
};

const objA = new Foo(1);

function Bar(id) {
  this.id = id;
}
// Bar 的 prototype 仍然是默认的 { constructor: Bar, __proto__: Object.prototype }
Bar.prototype.identify = function() {
   console.log("Bar prototype identify:", this.id);
}

const objB = new Bar(2);

const objC = { id: 3 }; // 普通对象字面量

// !! 关键操作:改变 objC 的原型
Object.setPrototypeOf(objC, Foo.prototype);
// 另一种(非标准)方式是 objC.__proto__ = Foo.prototype;

// --- 预测输出 ---

// 1.
objA.identify();
// 预测: Foo prototype identify: 1
// 原因: objA 是 Foo 的实例,其 __proto__ 指向 Foo.prototype,调用 identify 时 this 是 objA。

// 2.
objB.identify();
// 预测: Bar prototype identify: 2
// 原因: objB 是 Bar 的实例,其 __proto__ 指向 Bar.prototype,调用 identify 时 this 是 objB。

// 3.
// objC.identify(); // 如果直接调用,会发生什么?为什么?
// 预测: Foo prototype identify: 3
// 原因: objC 本身没有 identify 方法。它的 __proto__ 被手动设置为了 Foo.prototype。
//       因此,它会查找到 Foo.prototype.identify 并调用。此时,方法内的 this 指向 objC。

// 4.
console.log(Object.getPrototypeOf(objA) === Foo.prototype);
// 预测: true
// 原因: new Foo() 将实例的原型设置为 Foo.prototype。

// 5.
console.log(Object.getPrototypeOf(objB) === Bar.prototype);
// 预测: true
// 原因: new Bar() 将实例的原型设置为 Bar.prototype。

// 6.
console.log(Object.getPrototypeOf(objC) === Foo.prototype);
// 预测: true
// 原因: Object.setPrototypeOf(objC, Foo.prototype) 显式改变了 objC 的原型。

// 7.
console.log(objA instanceof Foo);
// 预测: true
// 原因: objA 是通过 Foo 构造函数创建的,其原型链上包含 Foo.prototype。

// 8.
console.log(objC instanceof Foo);
// 预测: true
// 原因: objC 的原型链上现在包含了 Foo.prototype (因为第 6 步的操作),instanceof 会沿着原型链检查。

// 9.
console.log(Foo.prototype.isPrototypeOf(objA));
// 预测: true
// 原因: isPrototypeOf 检查调用对象 (Foo.prototype) 是否存在于参数对象 (objA) 的原型链上。

// 10.
console.log(Foo.prototype.isPrototypeOf(objC));
// 预测: true
// 原因: 同上,objC 的原型链现在包含 Foo.prototype。

// 11.
console.log(Bar.prototype.isPrototypeOf(objA));
// 预测: false
// 原因: Bar.prototype 不在 objA 的原型链上。