深入浅出理解JavaScript原型与原型链

先让我们结合生活案例理解原型原型链相关概念,想象一下一个大家庭,有很多成员。

1. 原型 (Prototype) - 家族的共同特征或技能模板

  • 概念对应: 家族中代代相传的共同特征、习惯、或者家族里独有的某个手艺或知识。
  • 例子: 假设你们家族的成员普遍都有高个子、善于烹饪一道祖传菜肴,或者懂得某种特定的修理技巧。这些“高个子”、“祖传菜肴烹饪方法”、“修理技巧” 就像是家族的原型属性和方法

2. 对象 (Object) - 家族中的个体

  • 概念对应: 你、你的爸爸、你的爷爷、你的曾祖父等家族里的每一个具体的人。
  • 例子: 你是一个具体的“人”对象,你的爸爸是另一个具体的“人”对象,等等。

3. [[Prototype]] (内部链接) 或 __proto__ - 子女指向父母的链接

  • 概念对应: 子女天然地与父母相关联。在原型中,这个链接是单向的,从子女指向父母。
  • 例子: 你出生后,你自然就知道你的爸爸是谁。你有一个内部的“链接”或者“指针”,指向你的爸爸。你的爸爸也有一个链接指向他的爸爸(你的爷爷),以此类推。

4. 原型链 (Prototype Chain) - 家族的血脉/祖谱线

  • 概念对应: 这个由“子女指向父母”的链接形成的链条。从你出发,沿着你指向你爸爸的链接,再沿着你爸爸指向你爷爷的链接,一直向上追溯。
  • 例子: 你的原型链就是: 你 -> 你的爸爸 -> 你的爷爷 -> 你的曾祖父 -> ... 这是一个向上追溯的祖谱线。

5. 属性/方法查找 - 寻找特征或解决问题

  • 概念对应: 当你需要某种技能或想知道某个特征时,你会沿着原型链去寻找。
  • 过程:
    • 第一步: 你首先看看自己是否具备这个特征或技能 (查找对象自身的属性)。比如,你会不会做那道祖传菜肴?如果你自己就会,太好了,查找结束,你直接使用自己的技能。
    • 第二步: 如果你自己不会 (对象本身没有该属性/方法),你就会问你的爸爸 (沿着 [[Prototype]] 链接到你的原型)。“爸爸,你会做那道祖传菜肴吗?”
    • 第三步: 如果你爸爸会,他就会告诉你方法或者帮你做 (在原型上找到)。查找结束。
    • 第四步: 如果你爸爸也不会 (你的原型上也没有),你爸爸可能会告诉你,“我爷爷(你的曾祖父,也就是你爸爸的原型)会!” (沿着原型链继续向上)。于是你去问你的爷爷。
    • 第五步: 这个过程会一直持续下去,直到找到会做这道菜的人 (在原型链上找到属性/方法)。
    • 如果直到最老的祖先都找不到... 那说明这道菜肴不是你们家族的技能,或者信息失传了 (到达原型链终点仍未找到)。

6. Object.prototype - 人类的基本共性 / 最初的祖先

  • 概念对应: 这是一个所有人类都共享的最基本、最普遍的特征或能力集合。比如,所有人类都能呼吸、吃饭、睡觉(这是最基本的方法)。
  • 例子: 在我们的家族链条向上追溯到足够远时,会到达一个所有人都共有的“人类祖先”。这个“人类祖先”拥有的基本能力,是所有后代都继承的。在 JavaScript 中,Object.prototype 就好比这个提供了所有对象最基本方法的“祖先”。

7. 原型链的终点 (null) - 追溯到第一个人类之前

  • 概念对应: 当你追溯祖谱线,到了第一个“人”那里,再往上就没有“人”作为祖先了。链条就断了。
  • 例子: Object.prototype 的原型是 null。这表示 Object.prototype 是这个继承体系中“人类”的开端,再往上就没有通过原型链关联的更基础的对象了。

8. 属性覆盖 (Overriding) - 青出于蓝或有自己的特长

  • 概念对应: 你可能继承了爸爸妈妈的一些特征或技能,但你可能做得更好,或者发展出了自己独有的版本。
  • 例子: 你的家族原型是“高个子”,你爸爸也是高个子。但你可能更高!当你询问你的身高时,是直接使用你自己的身高数据,而不是你爸爸的身高(你的个体属性覆盖了原型的属性)。或者,你学会了比祖传方法更先进的烹饪技巧,当你做菜时,用的是你自己的新方法,而不是去问爸爸或爷爷的旧方法。

原型原型链概念以及代码示例

1. 什么是原型 (Prototype)?

原型是 JavaScript 中一个普通的对象,它是另一个对象继承属性和方法的来源。

  • 几乎所有 JavaScript 对象都有一个内部的 [[Prototype]] 属性,指向它的原型。
  • 你可以通过 Object.getPrototypeOf(obj) 方法来获取对象的原型。
  • 虽然不推荐在生产代码中使用,但 obj.__proto__ 这个属性在很多环境中也暴露了 [[Prototype]],并且在学习和调试时很直观。

代码示例 : 查看对象的原型

// 通过对象字面量创建的对象
const objLiteral = {};
console.log("--- objLiteral 原型 ---");
console.log(Object.getPrototypeOf(objLiteral)); // 输出: [Object: null prototype] {} (即 Object.prototype 对象)
console.log(objLiteral.__proto__); // 输出: [Object: null prototype] {} (与上面相同)
console.log(Object.getPrototypeOf(objLiteral) === Object.prototype); // 输出: true

console.log("\n--- 数组的原型 ---");
const arr = [];
console.log(Object.getPrototypeOf(arr)); // 输出: [] (即 Array.prototype 对象)
console.log(Object.getPrototypeOf(arr) === Array.prototype); // 输出: true

console.log("\n--- 函数的原型 ---");
function myFunc() {}
console.log(Object.getPrototypeOf(myFunc)); // 输出: [Function] (即 Function.prototype 对象)
console.log(Object.getPrototypeOf(myFunc) === Function.prototype); // 输出: true

解释:

  • 通过字面量 {} 创建的对象,其原型默认指向内置的 Object.prototype 对象。
  • 数组的原型指向 Array.prototype
  • 函数的原型指向 Function.prototype(注意:这是函数对象本身的原型,不是它作为构造函数时的 prototype 属性)。

2. 函数的 prototype 属性 (用于构造函数)

函数除了有自己的原型(指向 Function.prototype),还有一个特殊的属性叫做 prototype这个 prototype 属性是给通过该函数作为构造函数 (new 关键字) 创建出来的对象准备的,作为这些新创建对象的原型。

代码示例 : 函数的 prototype 属性与 new 关键字

// 定义一个构造函数
function Dog(name) {
  this.name = name; // name 属性会直接添加到每个实例上
}

// 在构造函数的 prototype 属性上添加方法
// 这些方法会被所有 Dog 实例共享
Dog.prototype.bark = function() {
  console.log(this.name + " says Woof!");
};

Dog.prototype.species = "Canine"; // 共享属性

// 使用 new 关键字创建 Dog 的实例
const dog1 = new Dog("Buddy");
const dog2 = new Dog("Lucy");

console.log("--- Dog 实例的原型 ---");
// dog1 的原型(__proto__) 指向 Dog.prototype
console.log(Object.getPrototypeOf(dog1) === Dog.prototype); // 输出: true
// dog2 的原型(__proto__) 也指向 Dog.prototype
console.log(Object.getPrototypeOf(dog2) === Dog.prototype); // 输出: true

console.log("\n--- Dog 实例访问属性和方法 ---");
dog1.bark(); // 输出: Buddy says Woof! (从 Dog.prototype 继承)
console.log(dog2.species); // 输出: Canine (从 Dog.prototype 继承)
console.log(dog1.name); // 输出: Buddy (实例自身的属性)

console.log("\n--- 构造函数 Dog 本身的原型 ---");
// Dog 函数本身的原型指向 Function.prototype
console.log(Object.getPrototypeOf(Dog) === Function.prototype); // 输出: true

解释:

  • Dog 函数有一个 prototype 属性,指向一个对象。
  • 我们在 Dog.prototype 上添加了 bark 方法和 species 属性。
  • 使用 new Dog("...") 创建的 dog1dog2 实例,它们的 [[Prototype]] (即 __proto__) 都被设置为了 Dog.prototype 对象。
  • 因此,dog1dog2 可以通过原型链访问并使用 Dog.prototype 上的 barkspecies
  • name 属性是直接添加到 this (也就是实例本身) 上的,不是通过原型继承的。

3. 什么是原型链 (Prototype Chain)?

原型链是由对象的 [[Prototype]] 属性连接起来形成的链条。当查找一个对象的属性或方法时,如果在对象本身没有找到,就会沿着这个链向上查找,直到找到该属性或方法,或者到达链的终点。

原型链的连接方式:

  • 通过对象字面量 {} 创建的对象: {}.__proto__ 指向 Object.prototype
  • 通过 new Constructor() 创建的对象: instance.__proto__ 指向 Constructor.prototype
  • 一个对象的原型的原型: Object.getPrototypeOf(obj).__proto__ 指向 Object.getPrototypeOf(Object.getPrototypeOf(obj)).

原型链的终点:

原型链的终点是 Object.prototype,而 Object.prototype 的原型是 nullnull 表示链的结束,再往上就没有可以查找的原型了。

Object.prototype.__proto__ === null

代码示例 : 跟踪原型链

function Animal() {}
Animal.prototype.sound = "Generic Sound";

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

// 让 Dog 的原型指向一个 Animal 实例 (或者更常见的是 Object.create(Animal.prototype))
// 这里为了演示原型链,我们直接设置 __proto__,但在实际继承中通常不是这么做
// 更标准的继承方式是 Dog.prototype = Object.create(Animal.prototype);
Object.setPrototypeOf(Dog.prototype, Animal.prototype);

const myDog = new Dog("Rex");

console.log("--- 原型链追踪 ---");

// 1. myDog 实例本身
console.log("myDog:", myDog); // { name: "Rex" }

// 2. myDog 的原型 (Dog.prototype)
const dogProto = Object.getPrototypeOf(myDog);
console.log("myDog的原型 (Dog.prototype):", dogProto); // Dog { sound: "Generic Sound" } (这里可能显示为 Dog {},取决于环境,但它链接到了 Animal.prototype)

// 3. Dog.prototype 的原型 (Animal.prototype)
const animalProto = Object.getPrototypeOf(dogProto);
console.log("Dog.prototype的原型 (Animal.prototype):", animalProto); // Animal { sound: "Generic Sound" }

// 4. Animal.prototype 的原型 (Object.prototype)
const objectProto = Object.getPrototypeOf(animalProto);
console.log("Animal.prototype的原型 (Object.prototype):", objectProto); // [Object: null prototype] {}

// 5. Object.prototype 的原型 (null)
const endOfChain = Object.getPrototypeOf(objectProto);
console.log("Object.prototype的原型:", endOfChain); // 输出: null

// 链条: myDog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null

解释: 这个例子展示了一个稍微复杂一点的原型链:myDog 实例的原型是 Dog.prototype,而我们将 Dog.prototype 的原型设置为了 Animal.prototype。所以当查找属性时,会依次在 myDogDog.prototypeAnimal.prototypeObject.prototype 上查找,直到找到或到达 null

4. 如何通过原型链查找对象的属性或方法?

查找规则:先找自己,找不到就沿着 [[Prototype]] 向上找,直到找到或到顶 (null)。

代码示例 : 原型链上的属性查找

// 构造函数 A
function A() {
  this.propA_instance = "Instance A";
}
A.prototype.propA_proto = "Prototype A";
A.prototype.methodA = function() { console.log("Method A"); };
 
// 构造函数 B,继承自 函数A (通过设置 B.prototype 的原型为 A.prototype)
function B() {
  this.propB_instance = "Instance B";
}
// 更实际的继承方式:用到Object.setPrototypeOf()方法,该方法会将 B.prototype 的原型设置为 A.prototype
//这意味着 B.prototype 将会继承 A.prototype 上的属性和方法。
Object.setPrototypeOf(B.prototype, A.prototype);
B.prototype.propB_proto = "Prototype B";
B.prototype.methodB = function() { console.log("Method B"); };
 
const instanceB = new B();
 
console.log("--- 属性查找过程 ---");
 // 查找顺序:1 propA_instance: instanceB 没有
 //2 B.prototype 没有
 //3 去 B.prototype 的原型 (A.prototype) 找
 //4 A.prototype 没有
 // (注意: propA_instance 是添加到 A 的实例上的,不在 A.prototype 上)
 // 5 继续去 A.prototype 的原型 (Object.prototype) 找,没有
 // 6 到达 null,查找停止,没找到就显示undefined
 
//查找 propB_instance: 先从 instanceB 本身找,找到
console.log("instanceB.propB_instance:", instanceB.propB_instance); // 输出:instanceB.propB_instance: Instance B
 
// 查找 propB_proto: instanceB 没有,去 instanceB 的原型 (B.prototype) 找,找到
console.log("instanceB.propB_proto:", instanceB.propB_proto); // 输出:instanceB.propB_proto: Prototype B
 
//依次查找都没有找到,输出undefined
console.log("instanceB.propA_instance:", instanceB.propA_instance); // 输出: instanceB.propA_instance:undefined
 
// 查找 propA_proto: instanceB 没有,B.prototype 没有,去 B.prototype 的原型 (A.prototype) 找,找到
console.log("instanceB.propA_proto:", instanceB.propA_proto); // 输出: instanceB.propA_proto:Prototype A
 
//查找 toString (来自 Object.prototype): instanceB 没有,B.prototype 没有,A.prototype 没有,去 A.prototype 的原型 (Object.prototype) 找,找到
console.log("instanceB.toString:", instanceB.toString); // 输出:instanceB.toString: function toString() { [native code] }
 
console.log("\n--- 方法调用过程 ---");
instanceB.methodB(); // 输出: Method B (在 B.prototype 上找到)
instanceB.methodA(); // 输出: Method A (在 A.prototype 上找到)
// instanceB.nonExistentMethod(); // 会抛出 TypeError (找不到)
 
// 覆盖属性/方法:实例属性会“屏蔽”原型上的同名属性
instanceB.propA_proto = "Overridden in Instance B";
console.log("instanceB.propA_proto after override:", instanceB.propA_proto); // 输出:instanceB.propA_proto after override: Overridden in Instance B
// 原型上的属性未改变
console.log("A.prototype.propA_proto:", A.prototype.propA_proto); // 输出: A.prototype.propA_proto:Prototype A
 
// 删除实例属性后,会再次访问原型上的属性
delete instanceB.propA_proto;
console.log("instanceB.propA_proto after delete:", instanceB.propA_proto); // 输出: Prototype A (重新访问了原型)

解释:

  • 当访问 instanceB.propB_instance 时,属性直接在 instanceB 实例上找到,查找结束。
  • 当访问 instanceB.propB_proto 时,instanceB 没有,引擎沿着 __proto__ 链接到 B.prototype,在那里找到了 propB_proto,查找结束。
  • 当访问 instanceB.propA_proto 时,instanceB 没有,B.prototype 没有,引擎沿着 __proto__ 链接到 B.prototype 的原型(即 A.prototype),在那里找到了 propA_proto,查找结束。
  • 当访问 instanceB.toString 时,沿着链一直向上查找,最终在 Object.prototype 上找到。
  • 如果一个属性在实例本身和原型链上的某个位置都有,实例上的属性会“屏蔽”原型上的属性,查找会先在实例上停止。
  • 删除实例上的属性后,下一次访问同名属性时,查找会继续沿着原型链进行。

概念总结:

  • 原型 (Prototype): 是一个对象,用于实现继承,通过对象的 [[Prototype]] 链接。
  • 函数的 prototype 属性: 是一个特殊属性,用于指定通过该函数作为构造函数创建的实例的原型。
  • 原型链 (Prototype Chain): 是对象通过 [[Prototype]] 链接起来形成的链条,用于属性和方法的查找。
  • 查找规则: 从对象本身开始,沿着原型链向上查找,直到找到或到达 null
  • 原型链终点: 通常是 Object.prototype,它的原型是 null
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值