理解原型对象(Prototype Object)与 `__proto__` 属性

嘿,JavaScript原型链大冒险:揭秘藏在__proto__和原型对象背后的宝藏!

大家好,我是你们的编程老友,今天我们要一起开启一场激动人心的冒险,探索JavaScript中神秘而强大的原型链!准备好了吗?系好安全带,我们即将进入一个充满魔法和惊喜的世界!🚀

1. 故事的开端:万物皆对象,对象皆有源

在JavaScript这片神奇的土地上,万物皆对象。就像亚里士多德说的,任何事物都有它的“形式”和“质料”。在JS里,对象就是“质料”,而对象的“形式”则与原型有关。这句话听起来玄乎,但用大白话来说就是:每个对象都从某个地方“继承”了一些东西,就像我们继承了父母的基因一样。

那么,这个“地方”究竟在哪里呢?这就是我们今天的主角之一:原型对象(Prototype Object)

你可以把原型对象想象成一个充满宝藏的密室,里面存放着一些共享的属性和方法。这些属性和方法就像家族传承的秘方,所有的后代对象都可以拿来使用。

2. 原型对象:对象的“爸爸”或“妈妈”

每个函数(包括构造函数)都有一个特殊的属性,叫做 prototype。注意,是函数才有的哦!这个 prototype 指向的就是一个对象,也就是我们说的原型对象。

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

console.log(Person.prototype); // 输出:一个对象,包含constructor属性

这个原型对象默认自带一个属性 constructor,它指回构造函数本身,也就是 Person。这就像一个回家的路标,告诉我们这个原型对象是属于哪个构造函数的。

那么,原型对象里面有些什么呢?默认情况下,它是一个空对象,但我们可以往里面添加任何我们想要的属性和方法。这些属性和方法将会被所有通过该构造函数创建的对象所共享。

Person.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name);
};

const person1 = new Person("Alice");
person1.sayHello(); // 输出:Hello, my name is Alice

const person2 = new Person("Bob");
person2.sayHello(); // 输出:Hello, my name is Bob

瞧,我们往 Person.prototype 添加了一个 sayHello 方法,然后 person1person2 都可以调用这个方法了!它们就像从同一个模子里刻出来的,拥有相同的技能。

3. __proto__:连接对象的秘密通道

接下来,我们要介绍另一位主角:__proto__。这个属性有点神秘,因为它不是标准的属性,但在所有现代浏览器中都支持(但官方推荐使用 Object.getPrototypeOf()Object.setPrototypeOf() 来代替)。

__proto__ 属性存在于对象上,而不是函数上。它指向的是创建该对象的构造函数的原型对象

这句话有点绕,我们来拆解一下:

  • person1 是通过 new Person("Alice") 创建的。
  • Person 是一个构造函数。
  • Person.prototypePerson 的原型对象。

所以,person1.__proto__ 指向的就是 Person.prototype

console.log(person1.__proto__ === Person.prototype); // 输出:true

你可以把 __proto__ 想象成一条秘密通道,连接着对象和它的“父母”——原型对象。通过这条通道,对象可以访问原型对象中的属性和方法。

4. 原型链:寻宝之旅的路线图

现在,我们将两个主角连接起来,就形成了一条链条,这就是传说中的 原型链(Prototype Chain)

原型链就像一条寻宝路线图,它从对象自身开始,沿着 __proto__ 一路向上,直到找到目标属性或方法,或者到达终点—— null

让我们再来梳理一下:

  1. 当我们试图访问一个对象的属性或方法时,JavaScript引擎会首先在对象自身查找。
  2. 如果对象自身没有这个属性或方法,引擎会沿着 __proto__ 找到对象的原型对象。
  3. 如果在原型对象中找到了,就返回该属性或方法。
  4. 如果原型对象中仍然没有找到,引擎会继续沿着原型对象的 __proto__ 向上查找,也就是查找原型对象的原型对象。
  5. 这个过程会一直持续下去,直到找到目标属性或方法,或者到达原型链的顶端—— null
console.log(person1.toString()); // 输出:[object Object]

咦?person1 并没有 toString 方法啊,为什么可以调用呢?

秘密就在原型链上!person1.__proto__ 指向 Person.prototype,而 Person.prototype.__proto__ 指向 Object.prototypeObject.prototype 是所有对象的最终原型,它包含了 toStringvalueOf 等常用的方法。

所以,当 JavaScript 引擎在 person1 自身和 Person.prototype 中都找不到 toString 方法时,它会继续向上查找,最终在 Object.prototype 中找到了!

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

这就是原型链的魔力!它让对象可以继承和共享属性和方法,极大地提高了代码的复用性和灵活性。

5. 图解原型链:一图胜千言

为了更好地理解原型链,我们来看一张图:

  +-------------------+     +-------------------+     +-------------------+
  |     person1       | --> |  Person.prototype | --> |  Object.prototype | --> null
  +-------------------+     +-------------------+     +-------------------+
  | name: "Alice"     |     | constructor: Person|     | toString: ...      |
  | __proto__: ...    |     | sayHello: ...      |     | valueOf: ...       |
  +-------------------+     +-------------------+     +-------------------+
                           | __proto__: ...    |     | __proto__: null    |
                           +-------------------+     +-------------------+

这张图清晰地展示了 person1Person.prototypeObject.prototype 之间的关系。person1 通过 __proto__ 指向 Person.prototype,而 Person.prototype 又通过 __proto__ 指向 Object.prototype,最终 Object.prototype__proto__ 指向 null,形成了完整的原型链。

6. 构造函数、原型对象和实例之间的三角恋

为了更好地理解原型链,我们需要理清构造函数、原型对象和实例之间的关系。它们就像一个三角恋,互相影响,互相依赖。

  • 构造函数(Constructor Function): 负责创建对象,并初始化对象的属性。
  • 原型对象(Prototype Object): 存放共享的属性和方法,供所有通过该构造函数创建的对象使用。
  • 实例(Instance): 通过构造函数创建的具体对象,拥有自身的属性,并通过原型链访问原型对象的属性和方法。

它们之间的关系可以用一句话概括:构造函数创建实例,实例通过 __proto__ 访问原型对象,原型对象通过 constructor 指回构造函数。

就像一个家庭,构造函数是家长,原型对象是家族的共同财产,实例是孩子。孩子继承了家族的财产,并通过父母找到自己的家族。

7. 动手实践:用代码验证你的理解

理论讲了一大堆,不如动手实践一下!让我们用代码来验证一下我们对原型链的理解。

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

Animal.prototype.sayName = function() {
  console.log("My name is " + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name); // 调用父构造函数,继承属性
  this.breed = breed;
}

// 设置Dog的原型对象为Animal的实例,实现继承
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修正constructor指向

Dog.prototype.bark = function() {
  console.log("Woof!");
};

const dog1 = new Dog("Buddy", "Golden Retriever");
dog1.sayName(); // 输出:My name is Buddy (继承自Animal)
dog1.bark(); // 输出:Woof! (Dog自身的方法)

console.log(dog1 instanceof Animal); // 输出:true
console.log(dog1 instanceof Dog); // 输出:true

这段代码展示了如何使用原型链来实现继承。Dog 构造函数继承了 Animal 构造函数的属性和方法,并且添加了自身特有的方法。

  • Animal.call(this, name):调用 Animal 构造函数,将 this 指向 Dog 的实例,从而继承 Animal 的属性。
  • Dog.prototype = new Animal():将 Dog 的原型对象设置为 Animal 的一个实例,从而继承 Animal 的方法。
  • Dog.prototype.constructor = Dog:修正 Dog 的原型对象的 constructor 属性,使其指向 Dog 构造函数。

通过这种方式,dog1 既可以访问 AnimalsayName 方法,也可以访问 Dogbark 方法。

8. 原型链的注意事项:避免踩坑

原型链虽然强大,但也需要谨慎使用,避免踩坑。

  • 不要直接修改 __proto__ 属性: 虽然 __proto__ 属性在浏览器中可用,但它不是标准的属性,官方推荐使用 Object.getPrototypeOf()Object.setPrototypeOf() 来代替。直接修改 __proto__ 可能会导致性能问题和兼容性问题。
  • 注意 constructor 属性的指向: 当你修改原型对象时,一定要记得修正 constructor 属性的指向,否则可能会导致类型判断错误。
  • 避免原型链过长: 原型链过长会影响性能,因为 JavaScript 引擎需要花费更多的时间来查找属性和方法。尽量保持原型链的简洁。
  • 理解 hasOwnProperty() 方法: hasOwnProperty() 方法用于判断一个属性是否是对象自身的属性,而不是继承自原型链的属性。这在遍历对象属性时非常有用。
const obj = { name: "Alice" };

console.log(obj.hasOwnProperty("name")); // 输出:true
console.log(obj.hasOwnProperty("toString")); // 输出:false (继承自Object.prototype)

9. 总结:原型链的价值

恭喜你,完成了这次原型链的冒险!🎉 现在,你应该对原型对象和 __proto__ 属性有了更深入的理解。

原型链是 JavaScript 中最重要的概念之一,它实现了继承和代码复用,是 JavaScript 面向对象编程的基础。掌握原型链,你才能真正理解 JavaScript 的精髓,写出更优雅、更高效的代码。

记住,原型链就像一条寻宝路线图,它连接着对象和它的“祖先”,让对象可以继承和共享属性和方法。理解原型链,你就能在 JavaScript 的世界里自由探索,发现更多的宝藏!

希望这篇文章能够帮助你理解 JavaScript 原型链。如果你有任何问题,欢迎在评论区留言,我们一起讨论!下次再见!👋

发表回复

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