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 自身的属性)
总结对比
| 特性 | prototype | proto (或 [[Prototype]]) |
|---|---|---|
| 谁拥有? | 函数 (作为构造函数的模板) | 对象实例 (指向其继承来源) |
| 是什么? | 一个对象,包含共享的属性/方法 | 一个指向原型对象的引用/链接 |
| 目的? | 定义实例应该继承自哪里 | 实现实际的继承查找,构成原型链 |
| 关系 | 实例.proto 指向 构造函数.prototype | 实例.proto 指向 构造函数.prototype |
| 标准访问 | 直接访问 FunctionName.prototype | Object.getPrototypeOf(obj) / Object.setPrototypeOf(obj) |
简单记忆:
- prototype 是函数(类)的属性,用来定义蓝图。
- proto 是实例对象的属性,用来连接到蓝图,实现继承。
好的,这里有一些关于 JavaScript 构造函数、prototype 和 proto 的练习题,可以帮助你巩固理解。
练习 1:创建基本构造函数和实例
- 创建一个名为 Product 的构造函数。
- 该构造函数接收 name (字符串) 和 price (数字) 作为参数,并将它们设置为实例自身的属性。
- 在 Product.prototype 上添加一个名为 displayInfo 的方法,该方法在控制台打印产品的名称和价格,格式为 "Product: [name], Price: $[price]"。
- 使用 new 关键字创建两个 Product 实例:一个名为 "Laptop",价格为 1200;另一个名为 "Mouse",价格为 25。
- 调用这两个实例的 displayInfo 方法,检查输出是否正确。
- 使用 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 构造函数和实例:
- 使用 hasOwnProperty 检查 laptop 实例是否直接拥有 name 属性和 price 属性。
- 使用 hasOwnProperty 检查 laptop 实例是否直接拥有 displayInfo 方法。
- 解释为什么第二个检查返回 false。
- 不使用 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. 实例状态
- 创建一个名为 Counter 的构造函数。
- 在构造函数内部,使用 this.count = 0; 初始化一个实例属性 count。
- 在构造函数内部,添加一个名为 incrementInstance 的方法,该方法将 this.count 加 1。
- 在 Counter.prototype 上添加一个名为 sharedValue 的属性,并将其初始化为 0。
- 在 Counter.prototype 上添加一个名为 incrementShared 的方法,该方法将 this.constructor.prototype.sharedValue(或 Counter.prototype.sharedValue)加 1。注意:这里直接修改原型上的属性。
- 创建两个 Counter 实例:counterA 和 counterB。
- 调用 counterA.incrementInstance() 两次。
- 调用 counterB.incrementInstance() 一次。
- 打印 counterA.count 和 counterB.count。它们应该不同。
- 调用 counterA.incrementShared() 一次。
- 调用 counterB.incrementShared() 一次。
- 打印 counterA.sharedValue 和 counterB.sharedValue(或者直接打印 Counter.prototype.sharedValue)。它们应该相同,并且等于 2。
- 思考并解释 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:简单原型继承
- 创建一个名为 Animal 的构造函数,接收 name 参数,并将其设置到 this.name。
- 在 Animal.prototype 上添加一个 speak 方法,打印 "Some generic sound"。
- 创建一个名为 Dog 的构造函数,接收 name 和 breed 参数。
- 在 Dog 构造函数内部,使用 Animal.call(this, name) 来调用父构造函数,以设置 name 属性。
- 将 breed 设置到 this.breed。
- 关键步骤: 设置 Dog.prototype 继承自 Animal.prototype。常用的方法是 Object.create(Animal.prototype)。
- 关键步骤: 修复 Dog.prototype.constructor,使其指向 Dog 本身。
- 在 Dog.prototype 上覆盖 speak 方法,使其打印 "Woof! Woof!"。
- 在 Dog.prototype 上添加一个 fetch 方法,打印 "[name] is fetching the ball."。
- 创建一个 Dog 实例,例如 const myDog = new Dog('Buddy', 'Golden Retriever');。
- 调用 myDog.speak(),检查输出是否为 "Woof! Woof!"。
- 调用 myDog.fetch(),检查输出是否包含正确的名字。
- 检查 myDog instanceof Dog 和 myDog instanceof Animal 的结果(都应为 true)。
- 检查 Object.getPrototypeOf(myDog) 是否等于 Dog.prototype。
- 检查 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 的原型链上。