原型继承

原型、原型链


JavaScript 常被描述为一种基于原型的语言 (prototype-based language)。每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

准确地说,这些属性和方法定义在 Object的构造器函数(constructor functions)之上的 prototype 属性上,而非对象实例本身。

继承


我的父母是黑色头发,我也是黑色的头发。可以看出,我从我父母那里遗传了黑头发基因。

在 JavaScript 中 继承是指一个对象基于另一个对象。

假设有一个新建的 car 对象的构造函数,每个 car 对象都具有不同的颜色属性值,例如 红色、蓝色和绿色;对象不仅具有自己的属性,而且与通用对象秘密关联,这种通用对象称之为原型;我们可以向原型中添加 car 对象都能共享的方法 drive方法 ,因此在每次新建一个 car 时,它们可以共享相同的 drive 方法,而不是创建新的 drive 方法。

和其他语言不同,JavaScript 利用原型来管理继承。

添加方法到原型


还记得吗,对象包含数据(即属性)和操纵数据的手段(即方法)。在此之前,我们都是将方法直接添加到构造函数本身:

function Cat() {
this.lives = 9;

this.sayName = function () {
console.log(`Meow! My name is ${this.name}`);
};
}

这样,通过将一个函数保存到新创建的 Cat 对象的 sayName 属性中,可以将 sayName() 方法添加到所有 Cat 对象上。这样做没问题,但是如果我们想用这个构造函数实例化更多的 Cat 对象呢?你每一次都要为这个 Cat 对象的 sayName 创建一个新的函数!更重要的是:如果你想对方法进行更改,则必须逐个更新所有对象。在这种情况下,最好是让同一个 Cat 构造函数所创建的所有对象共享一个 sayName 方法。

为了节省内存并保持简洁,我们可以在构造函数的 prototype 属性中添加方法。原型只是一个对象,构造函数所创建的所有对象都会保持对原型的引用。这些对象甚至可以将 prototype 的属性当作自身属性来使用!

JavaScript 利用对象与其原型之间的这个秘密链接来实现继承。请考虑以下原型链:

原型链

还记得吗,每个函数都有一个 prototype 属性,它其实只是一个对象。当使用 new 运算符将该函数作为构造函数来调用时,它会创建并返回一个新的对象。该对象被秘密地链接到其构造函数的 prototype,而这个秘密链接让该对象可以访问 prototype 的属性和方法,就像它自己的一样!

由于我们知道 prototype 属性仅仅指向一个普通对象,因此这个对象本身也有一个秘密链接指向它的原型。而且,这个原型对象也有引用指向它自己的原型,以此类推。原型链就是这样形成的。

无论你是访问属性(例如 bailey.lives)还是调用方法(即 bailey.meow()),JavaScript 解释器都会按照特定的顺序在原型链中查找它们:

  • 首先,JavaScript 引擎将查看对象自身的属性。这意味着,直接在该对象中定义的任何属性和方法将优先于其他位置的任何同名属性和方法(类似于作用域链中的变量阴影)。
  • 如果找不到目标属性,它将搜索对象的构造函数的原型,以寻找匹配。
  • 如果原型中不存在该属性,则 JavaScript 引擎将沿着该链继续查找。
  • 该链的最顶端是 Object() 对象,也就是顶级父对象。如果 仍然 找不到该属性,则该属性为未定义。

之前,我们都是直接在构造函数中定义方法。让我们来看看,如果我们转而在构造函数的 prototype 中定义方法,情况又会如何!

function Dog(age, weight, name) {
this.age = age;
this.weight = weight;
this.name = name;
this.bark = function () {
console.log(`${this.name} says woof!`);
};
}

这是 Dog 构造函数,我们可以调用它来创建一个具有以下四种属性的对象:age, weight, name 和 bark, 可以在构造器中定义 bark 方法,我们也可以将 bark 方法移到 dog 的原型中,直接使用点记法来定义 bark,像这样:

Dog.prototype.bark = function () {
console.log(`${this.name} says woof!`);
};

函数就变为:

function Dog(age, weight, name) {
this.age = age;
this.weight = weight;
this.name = name;
}

Dog.prototype.bark = function () {
console.log(`${this.name} says woof!`);
};

现在,我们将调用 dog 的构造函数来创建一个新的对象, 并调用这个新的 dog 的 bark方法:

dog1 = new Dog(2, 60, 'Java');

dog2 = new Dog(4, 55, 'Jodi');

dog1.bark(); // Java says woof!

dog2.bark(); // Jodi says woof!

让我们回顾一下,发生了什么?

当我们在新建的 dog 对象中调用 bark 方法时,JavaScript 引擎会查看自己的属性,尝试找到与 bark 方法相匹配的名称,由于 bark 没有直接定义在这个 dog 上,它会看看 bark 方法的原型,最后,我们不需要调用 dog.prototype.bark(),我们只需要调用 dog.bark() 就会起作用,因为这个 dog 对象已经通过它的原型与 bark 方法联系起来了。

小练习


// (A)
function Dalmatian (name) {
this.name = name;
this.bark = function() {
console.log(`${this.name} barks!`);
};
}

// (B)
function Dalmatian (name) {
this.name = name;
}

Dalmatian.prototype.bark = function() {
console.log(`${this.name} barks!`);
};

假设我们想定义一个可以在 Dalmatian 构造函数的实例(对象)上调用的方法(我们将会实例化至少 101 个对象!)。前面两种方式中的哪一种是最佳选择?

参考答案: (B)是最佳选择,因为每次创建 Dalmatian 的实例时,将不需要重新创建 bark 所指向的函数。

替换 prototype 对象


如果完全替换某个函数的 prototype 对象,结果会怎样?这将如何影响该函数所创建的对象?让我们来看一个简单的 Hamster 构造函数,并实例化一些对象:

function Hamster() { 
this.hasFur = true;
};

let waffle = new Hamster();

let pancake = new Hamster();

首先要注意的是,在创建新的对象 waffle 和 pancake 之后,我们仍然可以为 Hamster 的原型添加属性,而且它仍然可以访问这些新的属性。

Hamster.prototype.eat = function () { 
console.log('Chomp chomp chomp!');
};

waffle.eat(); // 'Chomp chomp chomp!'

pancake.eat(); // 'Chomp chomp chomp!'

现在,让我们将 Hamsterprototype 对象完全替换为其他内容:

Hamster.prototype = {
isHungry: false,
color: 'brown'
};

先前的对象无法访问更新后的原型的属性;它们只会保留与旧原型的秘密链接:

console.log(waffle.color); // undefined

waffle.eat(); // 'Chomp chomp chomp!'

console.log(pancake.isHungry); // undefined

事实证明,此后创建的任何新的 Hamster 对象都会使用更新后的原型:

const muffin = new Hamster();

muffin.eat(); // TypeError: muffin.eat is not a function

console.log(muffin.isHungry); // false

console.log(muffin.color); // 'brown'

检查对象的属性


正如我们刚刚所看到的,如果一个对象本身没有某个特定属性,它可以访问原型链中某个这样的属性(当然,假设它是存在的)。由于选择很多,有时可能会不好判断某个特定的属性究竟来自哪里!这里有一些有用的方法可以帮助你进行判断。

hasOwnProperty()


hasOwnProperty() 可以帮助你找到某个特定属性的来源。在向其传入你要查找的属性名称的字符串后,该方法会返回一个布尔值,指示该属性是否属于该对象本身(即该属性 不是 被继承的)。请考虑在函数中直接定义一个属性的 Phone 构造函数,以及它的 prototype 对象的另一个属性:

function Phone() {
this.operatingSystem = 'Android';
}

Phone.prototype.screenSize = 6;

现在,让我们创建一个新的对象 myPhone,并检查 operatingSystem 是否为其本身的属性,也就是说,它不是从该对象的原型(或原型链上的其他地方)继承来的:

const myPhone = new Phone();

const own = myPhone.hasOwnProperty('operatingSystem');

console.log(own);
//true

它确实返回为真!那么,Phone 对象的 prototype 上的 screenSize 属性又如何呢?

const inherited = myPhone.hasOwnProperty('screenSize');

console.log(inherited);
//false

使用 hasOwnProperty(),我们可以洞察某个属性的来源。

isPrototypeOf()


对象还可以访问 isPrototypeOf() 方法,该方法可以检查某个对象是否存在于另一个对象的原型链中。 使用这种方法,你可以确认某个特定的对象是否是另一个对象的原型。请看以下 rodent 对象:

const rodent = {
favoriteFood: 'cheese',
hasTail: true
};

现在,让我们来构建一个 Mouse() 构造函数,并将它的 prototype 赋给 rodent

function Mouse() {
this.favoriteFood = 'cheese';
}

Mouse.prototype = rodent;

如果我们创建一个新的 Mouse 对象,它的原型应该是 rodent 对象。让我们来确认一下:

const ralph = new Mouse();

const result = rodent.isPrototypeOf(ralph)

console.log(result);
//true

太棒了!isPrototypeOf() 是确认某个对象是否存在于另一个对象的原型链中的好办法。

Object.getPrototypeOf()


isPrototypeOf() 很有用处,但要记住,要想使用它,你必须首先掌握原型对象!如果你不确定某个对象的原型是什么呢?Object.getPrototypeOf() 可以帮助你解决这个问题。

使用前面的例子,让我们将 Object.getPrototypeOf() 的返回值存储在变量 myPrototype 中,然后检查它是什么:

const myPrototype = Object.getPrototypeOf(ralph);

console.log(myPrototype);
//{ favoriteFood: "cheese", hasTail: true }

太棒了!ralph 的原型 rodent 对象 与 返回的结果 具有相同的属性,因为它们就是同一个对象。 Object.getPrototypeOf() 很适合检索给定对象的原型。

constructor 属性


每次创建一个对象时,都会有一个特殊的属性被暗中分配给它:constructor

访问一个对象的 constructor 属性会返回一个对创建该对象的构造函数的引用!

以下是一个简单的 Longboard 构造函数。我们还会继续创建一个新的对象,然后将其保存到一个 board 变量中:

function Longboard() {
this.material = 'bamboo';
}

const board = new Longboard();

如果我们访问 boardconstructor 属性,我们应该会看到原来的构造函数本身:

console.log(board.constructor);

// function Longboard() {
// this.material = 'bamboo';
// }

好极了!请记住,如果某个对象是使用字面量表示法创建的,那么它的构造函数就是内置的 Object() 构造函数!

const rodent = {
teeth: 'incisors',
hasTail: true
};

console.log(rodent.constructor);
//function Object() { [native code] }

小练习


以下哪一项有关 hasOwnProperty() 的说法是正确的?请选择所有适用项:

  • 一个对象会作为参数传递给 hasOwnProperty
  • 它会返回一个布尔值,指示对象是否具有所指定的属性作为其本身属性(即该属性不是被继承的)
  • 一个字符串不能作为参数传递给 hasOwnProperty()
  • hasOwnProperty() 作为一个方法被调用到一个对象

参考答案: 2、4

以下哪一项有关 isProtypeOf()getPrototypeOf() 的说法是正确的?请选择所有适用项:

  • isProtypeOf() 可以检查某个对象是否存在于另一个对象的原型链中
  • isProtypeOf() 会接受一个参数:一个原型链将被搜索的对象
  • getProtypeOf() 在构造函数的一个实例(即单个对象本身)上被调用
  • getProtypeOf() 会返回传递给它的对象的原型

参考答案: 2、4

以下哪一项有关 constructor 属性的说法是正确的?请选择所有适用项:

  • 访问一个对象的 constructor 属性会返回一个对创建该对象(实例)的构造函数的引用
  • constructor 属性的值只是构造函数名称的一个字符串,而不是该函数本身
  • 每个对象都有一个 constructor 属性
  • 使用字面量表示法创建的对象是用 Object() 构造函数构建的

参考答案: 1、3、4

假设我们使用常规的对象字面量表示法来创建以下对象 capitals
当 Object.getPrototypeOf(capitals); 被执行时,会返回什么?

const capitals = {
California: 'Sacramento',
Washington: 'Olympia',
Oregon: 'Salem',
Texas: 'Austin'
};

参考答案:Object() 原型的引用

小结


JavaScript 中的继承是指一个对象基于另一个对象。继承让我们可以重用现有的代码,使对象具有其他对象的属性。

当使用 new 运算符将一个函数作为构造函数来调用时,该函数会创建并返回一个新的对象。这个对象会被秘密链接到其构造函数的 prototype,而它只是另一个对象。使用这个秘密链接,可以让一个对象访问原型的属性和方法,就像是它自己的一样。如果 JavaScript 没有在某个对象中找到一个特定属性,它将在原型链上继续查找。如有必要,它会一路查找到 Object()(顶级父对象)。

  • 对象如何查看对象的构造函数 –> obj.constructor 属性
  • 对象如何查看原型 –> 隐式属性 __proto__
  • 修改原型 –> obj.prototype.xxx = xxx
  • (原型链的终端)绝大多数对象的最终原型: Object.prototype

为了更理解 原型 / 构造函数 / 实例 三者的关系,请看下面这张图:

实例.__proto__ === 原型
构造函数.prototype === 原型
原型.constructorr === 构造函数

原型 / 构造函数 / 实例

此外,我们还介绍了几个方法和属性,可以用于检查对象及其原型的来源和引用,即:

  • hasOwnProperty()
  • isPrototypeOf()
  • Object.getPrototypeOf()
  • constructor

在下一部分,我们将从子类化的角度来探索原型继承的另一方面。如果你想从一个对象中只继承几个属性,但是又想让这个对象具有其他专有属性,该怎么办呢?我们将在下一部分对原型继承进行更深入的探讨。

延伸


-------------本文结束 感谢您的阅读-------------
0%