`prototype` 属性与实例对象的原型关系

好的,各位编程界的父老乡亲,兄弟姐妹们,今天老衲要跟大家聊聊JavaScript里一个让人又爱又恨,摸不清头脑,却又非常重要的概念——prototype属性,以及它和实例对象原型之间的那点剪不断理还乱的“爱情故事”。准备好了吗?让我们一起踏上这趟神奇的探索之旅!

第一章:何方妖孽?prototype属性的真面目

各位,提起prototype,是不是感觉眼前一黑,好像回到了当年被高数支配的恐惧?别怕,今天我们就把它扒个精光,让它无所遁形!

首先,我们要明确一点:prototype属性,它不是随便什么对象都有的,它只属于函数对象。记住,是函数对象,不是普通对象!就像只有VIP才能进专属包厢一样,prototype属性就是函数对象的专属特权。

那么,这个prototype属性到底是个啥玩意儿呢?

  • 官方解释: 每个函数都有一个prototype属性,这个属性指向一个对象,这个对象被称为原型对象。
  • 通俗解释: 想象一下,每个函数都是一个模具,而这个prototype属性就是这个模具自带的“说明书”或者“蓝图”。这个“说明书”上写着,用这个模具造出来的东西(也就是实例对象)应该具备哪些“零部件”(属性和方法)。

举个栗子:

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.`);
};

const person1 = new Person("Alice", 30);
person1.sayHello(); // 输出: Hello, my name is Alice, and I am 30 years old.

在这个例子中:

  • Person是一个函数对象。
  • Person.prototype 就是 Person 的原型对象。
  • Person.prototype.sayHello 在原型对象上定义了一个 sayHello 方法。
  • person1Person 函数创建的一个实例对象。

重点来了!person1 这个实例对象,它自己并没有 sayHello 方法,但是它却可以调用 sayHello 方法,这是为什么呢?这就是原型链的神奇之处,我们稍后再细说。

第二章:原型对象:实例对象的“亲爹”

原型对象,顾名思义,就是实例对象的“原型”。但是,这个“原型”不是说实例对象是从原型对象“复制”过来的,而是说实例对象可以通过某种机制,访问到原型对象上的属性和方法。

这种机制,就是传说中的原型链

我们可以把原型链想象成一棵树,实例对象是树上的叶子,原型对象是树枝,而树根就是 Object.prototype,也就是所有对象最终的“祖宗”。

当我们要访问一个对象的属性或方法时,JavaScript引擎会按照以下步骤进行查找:

  1. 首先,在对象自身查找,看有没有这个属性或方法。
  2. 如果没有找到,就沿着原型链向上查找,也就是到该对象的原型对象上查找。
  3. 如果原型对象上还没有找到,就继续向上查找,直到找到 Object.prototype 为止。
  4. 如果 Object.prototype 上也没有找到,那就返回 undefined

用图表示,就像这样:

实例对象 --> 原型对象 --> 原型对象的原型对象 --> ... --> Object.prototype --> null

举个栗子:

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

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

function Dog(name, breed) {
  Animal.call(this, name); // 继承 Animal 的属性
  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.eat(); // 输出: Buddy is eating.  (从 Animal.prototype 继承)
dog1.bark(); // 输出: Woof! (Dog 自身定义)
console.log(dog1.breed); // 输出: Golden Retriever (Dog 自身属性)

在这个例子中:

  • dog1Dog 的实例对象。
  • Dog.prototypeDog 的原型对象,它是一个 Animal 的实例。
  • dog1 可以调用 eat 方法,是因为 eat 方法定义在 Animal.prototype 上,而 Dog.prototype 继承了 Animal.prototype

所以,我们可以说,原型对象是实例对象的“亲爹”,它定义了实例对象可以继承的属性和方法。

第三章:__proto__:连接实例对象和原型对象的桥梁

前面我们说了,实例对象可以通过原型链访问到原型对象上的属性和方法。那么,实例对象是如何知道它的原型对象是谁呢?

答案就是:__proto__ 属性!

每个对象(除了 Object.prototype)都有一个 __proto__ 属性,这个属性指向创建该对象的构造函数的原型对象。

注意:__proto__ 属性不是标准属性,它是由浏览器厂商实现的,不建议在生产环境中使用。但是,它对于理解原型链的概念非常有帮助。

我们可以用 __proto__ 属性来验证一下上面的例子:

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.`);
};

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

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

可以看到,person1.__proto__ 指向了 Person.prototype,也就是说,person1 的原型对象就是 Person.prototype

第四章:constructor:原型对象的“身份证”

每个原型对象都有一个 constructor 属性,这个属性指向创建该原型对象的构造函数。

也就是说,Person.prototype.constructor 指向 Person 函数,Animal.prototype.constructor 指向 Animal 函数。

但是,当我们手动修改原型对象时,可能会导致 constructor 属性丢失,需要手动修正。

比如上面的 Dog 例子,我们修改了 Dog.prototype

Dog.prototype = new Animal();

这会导致 Dog.prototype.constructor 指向 Animal 函数,而不是 Dog 函数。所以,我们需要手动修正:

Dog.prototype.constructor = Dog;

第五章:原型链:JavaScript 继承的基石

原型链是 JavaScript 实现继承的核心机制。通过原型链,我们可以让一个对象继承另一个对象的属性和方法,实现代码的复用。

JavaScript 的继承方式主要有以下几种:

  • 原型链继承: 将子类型的原型对象设置为父类型的实例。
  • 借用构造函数继承: 在子类型的构造函数中调用父类型的构造函数,复制父类型的属性和方法。
  • 组合继承: 结合原型链继承和借用构造函数继承,既可以继承父类型的属性,又可以继承父类型的方法。
  • 原型式继承: 基于一个已有的对象创建新的对象,不需要定义构造函数。
  • 寄生式继承: 在原型式继承的基础上,增强新对象的功能。
  • 寄生组合式继承: 结合原型链继承和寄生式继承,是最常用的继承方式。

这里我们重点介绍一下寄生组合式继承,因为它既解决了原型链继承的属性共享问题,又避免了组合继承调用两次父类型构造函数的性能问题。

function inheritPrototype(subType, superType) {
  // 创建一个父类型原型的副本
  const prototype = Object.create(superType.prototype);
  // 增强副本
  prototype.constructor = subType;
  // 指定对象
  subType.prototype = prototype;
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name); // 借用构造函数继承属性
  this.age = age;
}

inheritPrototype(SubType, SuperType); // 寄生组合式继承

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

const instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // 输出: ["red", "blue", "green", "black"]
instance1.sayName(); // 输出: Nicholas
instance1.sayAge(); // 输出: 29

const instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // 输出: ["red", "blue", "green"]
instance2.sayName(); // 输出: Greg
instance2.sayAge(); // 输出: 27

在这个例子中,inheritPrototype 函数实现了寄生组合式继承的核心逻辑:

  1. 创建一个父类型原型的副本,避免子类型实例共享父类型原型上的属性。
  2. 增强副本,修正 constructor 指向,确保子类型原型对象的 constructor 指向子类型构造函数。
  3. 指定子类型原型对象为父类型原型对象的副本,建立原型链关系。

第六章:Object.create():创建对象的利器

Object.create() 方法可以创建一个新的对象,使用现有的对象来作为新对象的原型对象。

const person = {
  name: "Adam",
  age: 30,
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}, and I am ${this.age} years old.`);
  }
};

const anotherPerson = Object.create(person);
anotherPerson.name = "Eve";
anotherPerson.age = 25;

anotherPerson.sayHello(); // 输出: Hello, my name is Eve, and I am 25 years old.
console.log(person.name); // 输出: Adam

在这个例子中,anotherPerson 的原型对象是 person,所以它可以访问 person 上的 sayHello 方法。

第七章:总结:prototype 和原型链的精髓

说了这么多,我们来总结一下 prototype 和原型链的精髓:

  • prototype 属性是函数对象的专属特权,它指向原型对象。
  • 原型对象是实例对象的“亲爹”,它定义了实例对象可以继承的属性和方法。
  • __proto__ 属性(非标准)是连接实例对象和原型对象的桥梁。
  • constructor 属性是原型对象的“身份证”,它指向创建该原型对象的构造函数。
  • 原型链是 JavaScript 实现继承的核心机制,通过原型链,我们可以让一个对象继承另一个对象的属性和方法,实现代码的复用。
  • Object.create() 方法可以创建一个新的对象,使用现有的对象来作为新对象的原型对象。

掌握了 prototype 和原型链,你就掌握了 JavaScript 继承的精髓,就可以写出更加优雅、高效的代码。

第八章:进阶:prototype 的应用场景

除了实现继承,prototype 还有很多其他的应用场景:

  • 扩展内置对象: 我们可以通过修改内置对象的原型对象,来扩展内置对象的功能。比如,我们可以给 Array.prototype 添加一个 unique 方法,用来去除数组中的重复元素。
Array.prototype.unique = function() {
  const result = [];
  for (let i = 0; i < this.length; i++) {
    if (result.indexOf(this[i]) === -1) {
      result.push(this[i]);
    }
  }
  return result;
};

const arr = [1, 2, 2, 3, 3, 4, 5];
const uniqueArr = arr.unique();
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
  • 实现模块化: 我们可以使用 prototype 来实现模块化,将相关的属性和方法封装在一个对象中,避免全局变量污染。

第九章:注意事项:prototype 的坑

prototype 虽然强大,但是也存在一些坑,需要注意:

  • 不要过度扩展内置对象: 扩展内置对象可能会导致命名冲突,影响代码的可维护性。
  • 注意原型链的长度: 原型链过长会影响性能,尽量避免过长的原型链。
  • 避免属性共享问题: 在原型链继承中,需要注意属性共享问题,避免一个实例修改了原型上的属性,影响到其他实例。

第十章:总结的总结

好了,各位父老乡亲,今天的 prototype 之旅就到这里了。希望通过今天的讲解,大家能够对 prototype 有一个更深入的理解。

记住,prototype 不是洪水猛兽,只要掌握了它的精髓,就能让你的代码更加强大!

最后,送大家一句话:理解 prototype,代码任你行! 😉

发表回复

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