ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScriptのプロトタイプと継承を詳しく解説(画像とテキスト)_JavaScriptスキル

JavaScriptのプロトタイプと継承を詳しく解説(画像とテキスト)_JavaScriptスキル

WBOY
WBOYオリジナル
2016-05-16 16:41:461009ブラウズ

これまでにオブジェクト指向について学んだことをすべて一時的に忘れてください。ここではレース状況のみを考慮する必要があります。はい、レースです。

最近、私はフランスで人気のイベントである ル・マン 24 時間レース を見ていました。最も速い車はル・マン・プロトタイプとして知られていました。これらの車は「アウディ」や「プジョー」などのメーカーが作っていますが、街や高速道路で見かけるような車ではありません。これらは、高速耐久イベントに出場するために特別に作られています。

メーカーはこれらのプロトタイプ車両の研究、開発、設計、製造に巨額の資金を投資しており、エンジニアはこのプロジェクトを完璧にするために常に懸命に働いています。彼らは、合金、バイオ燃料、ブレーキ技術、タイヤコンパウンド組成、安全機能などに関するさまざまな実験を実施しました。時間が経つにつれて、これらの実験から得られた技術の一部は洗練され、主流の車両ラインに導入されました。あなたが運転している車のテクノロジーの一部が、レーシングプロトタイプで初めて登場した可能性があります。

これらの主流車両 は、レーシング プロトタイプ から テクノロジーを継承しているとも言えます。

これで、JavaScript のプロトタイプと継承の問題について議論するための基礎が整いました。これは、C、Java、または C# で知られている古典的な継承パターンとはまったく異なりますが、同じくらい強力で、潜在的により柔軟です。

オブジェクトとクラスについて

JavaScript はオブジェクトがすべてです。オブジェクトとは、伝統的な意味でのオブジェクト、つまり「状態と動作を含む単一のエンティティ」を指します。たとえば、JavaScript の配列は、複数の値を含むオブジェクトであり、プッシュ、リバース、およびポップのメソッドが含まれます。

var myArray = [1, 2];
myArray.push(3);
myArray.reverse();
myArray.pop();
var length = myArray.length;

ここで問題は、プッシュ メソッドがどこから来たのかということです。先ほど述べた静的言語は「クラス構文」を使ってオブジェクトの構造を定義しますが、JavaScriptは「クラス構文」を持たない言語であり、配列の「クラス」構文を使って各配列オブジェクトを定義することができません。また、JavaScript は動的言語であるため、必要に応じてオブジェクトにメソッドを任意に配置できます。たとえば、次のコードは、2 次元空間内の点を表すために使用される point オブジェクトを定義し、add メソッドも定義します。

var point = {
  x : 10,
  y : 5,
  add: function(otherPoint) {
    this.x += otherPoint.x;
    this.y += otherPoint.y;
  }
};

しかし、上記のアプローチのスケーラビリティは良くありません。各ポイント オブジェクトに add メソッドが含まれていることを確認する必要があります。また、このメソッドを各ポイント オブジェクトに手動で追加するのではなく、すべてのポイント オブジェクトが同じ add メソッドの実装を共有することを望んでいます。ここでプロトタイピングが登場します。

プロトタイプについて

JavaScript では、各オブジェクトは隠された状態の一部、つまりプロトタイプとも呼ばれる別のオブジェクトへの参照を維持します。以前に作成した配列は、自分で作成したポイント オブジェクトと同様に、プロトタイプ オブジェクトを参照します。プロトタイプ参照は隠蔽されていると上で述べましたが、オブジェクトの __proto__ 属性を通じてこのプロトタイプ参照にアクセスできる ECMAScript (JavaScript の正式名) の実装もあります (Google Chrome など)。概念的には、オブジェクトは、図 1 に示されているオブジェクトとプロトタイプの関係に似ていると考えることができます。

写真 1

将来的には、開発者は __proto__ 属性の代わりに Object.getPrototypeOf 関数を使用して、オブジェクト プロトタイプへの参照を取得できるようになります。この記事の執筆時点では、Object.getPrototypeOf 関数は Google Chrome、FIrefox、IE9 ブラウザーですでに利用可能です。この機能はすでに ECMAScript 標準の一部であるため、今後さらに多くのブラウザーがこの機能を実装するでしょう。次のコードを使用して、作成した myArray オブジェクトと point オブジェクトが 2 つの異なるプロトタイプ オブジェクトを参照していることを証明できます。

  1. Object.getPrototypeOf(point) != Object.getPrototypeOf(myArray);

この記事の残りの部分では、__proto__ 関数と Object.getPrototypeOf 関数を併用します。これは主に、図や文章で __proto__ の方が認識しやすいためです。覚えておく必要があるのは、これ (__proto__) は標準ではなく、オブジェクトのプロトタイプを表示するには Object.getPrototypeOf 関数が推奨される方法であるということです。

プロトタイプの何が特別なのでしょうか?

この質問にはまだ答えていません: 配列のプッシュ メソッドはどこから来たのですか?答えは、myArray プロトタイプ オブジェクトから得られるものです。図 2 は、Chrome のスクリプト デバッガーのスクリーンショットです。 Object.getPrototypeOf メソッドを呼び出して、myArray のプロトタイプ オブジェクトを表示しました。

写真 2

  注意 myArray 的原型对象中有许多方法,包括那些在代码示例中调用的 push、pop 和 reverse 方法。因此,原型对象中的确包括 push 方法,但是 myArray 方法如何引用到呢?

myArray.push(3);

  了解其工作原理的第一步,是要认识到原型并不是特别的。原型只是普通的对象。可以给原型添加方法,属性,并把他们当作其他 JavaScript 对象一样看待。然而,套用乔治·奥威尔的小说《动物农场》中“猪”的说法 —— 所有的对象应当是平等的,但有些对象(遵守规则的)比其他人更加平等。

  JavaScript 中的原型对象的确是特殊的,因为他们遵从以下规则。当我们告诉 JavaScript 我们要调用一个对象的 push 方法,或读取对象的 x 属性时,运行时会首先查找对象本身。如果运行时找不到想要的东西,它就会循着 __proto__ 引用和对象原型寻找该成员。当我们  调用 myArray 的 push 方法时,JavaScript 并没有在 myArray 对象上发现 push 方法,而是在 myArray 的原型对象上找到了,于是 JavaScript 调用此方法(见图 3)。

3

  上面所描述的行为是指一个对象本身继承了原型上的任何方法或属性。JavaScript 中其实不需要使用类语法也能实现继承。就像从赛车原型上继承了相应的技术的车,一个 JavaScript 对象也可以从原型对象上继承功能特性。

  图 3 还展示了每个数组对象同时也可以维护自身的状态和成员。在请求得到 myArray 的 length 属性的情况下,JavaScript 会取得 myArray 中 length 属性的值,而不会去读取原型中的对应值。我们可以通过向对象上添加 push 这样的方法来“重写”push 方法。这样就会有效地隐藏原型中的 push 方法实现。

 共享原型

  JavaScript 中原型的真正神奇之处是多个对象如何维持对同一个原型对象的引用。例如,如果我们创建了这样的两个数组:

var myArray = [1, 2];
var yourArray = [4, 5, 6];

  那么这两个数组将共享同一个原型对象,而下面的代码计算结果为 true:

Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);

  如果我们引用两个数组对象上的 push 方法,JavaScript 会去寻找原型上共享的 push 方法。

4

  JavaScript 中的原型对象提供继承功能,同时也就实现了该方法实现的共享。原型也是链式的。换句话说,因为原型对象只是一个对象,所以一个原型对象可以维持到另一个原型对象的引用。如果你重新审视图 2 便可以看到,原型的 __proto__ 属性是一个指向另一个原型的非空值。当 JavaScript 查找像 push 方法这样的成员时,它会循着原型引用链检查每一个对象,直到找到该成员,或者抵达原型链的末端。原型链为继承和共享开辟了一条灵活的途径。

  你可能会问的下一个问题是:我该如何设置那些自定义对象的原型引用呢?例如前面所使用的点对象,如何才能将 add 方法添加到原型对象中,并从多个点对象中继承方法呢?在回答这个问题之前,我们需要看看函数。

 有关函数

  JavaScript 中的函数也是对象。这样的表述带来了几个重要的结果,而我们并不会在本文中涉及所有的事项。这其中,能将一个函数赋值给一个变量,并且将一个函数作为参数传递给另一个函数的能力构成了现代 JavaScript 编程表达的基本范式。

  我们需要关注的是,函数本身就是对象,因此函数可以有自身的方法,属性,并且引用一个原型对象。让我们来讨论下面的代码的含义。

// 这将返回 true:
typeof (Array) === "function"
// 这样的表达式也是:
Object.getPrototypeOf(Array) === Object.getPrototypeOf(function () { })
// 这样的表达式同样:
Array.prototype != null

  代码中的第一行证明, JavaScript 中的数组是函数。稍后我们将看到如何调用 Array 函数创建一个新的数组对象。下一行代码,证明了 Array 对象使用与任何其他函数对象相同的原型,就像我们看到数组对象间共享相同的原型一样。最后一行代码证明了 Array 函数都有一个 prototype 属性,而这个 prototype 属性指向一个有效的对象。这个 prototype 属性十分重要。

  JavaScript 中的每一个函数对象都有 prototype 属性。千万不要混淆这个 prototype 属性的 __proto__ 属性。他们用途并不相同,也不是指向同一个对象。

// 返回 true
Object.getPrototypeOf(Array) != Array.prototype

  Array.__proto__ 提供的是 数组原型 – 请把它当作 Array 函数所继承的对象。

  而 Array.protoype,提供的的是 所有数组的原型对象。也就是说,它提供的是像 myArray 这样数组对象的原型对象,也包含了所有数组将会继承的方法。我们可以写一些代码来证明这个事实。

// true
Array.prototype == Object.getPrototypeOf(myArray)
// 也是 true
Array.prototype == Object.getPrototypeOf(yourArray);

  我们也可以使用这项新知识重绘之前的示意图。

5

  基于所知道的知识,请想象创建一个新的对象,并让新对象表现地像数组的过程。一种方法是使用下面的代码。

// 创建一个新的空对象
var o = {};
// 继承自同一个原型,一个数组对象
o.__proto__ = Array.prototype;
// 现在我们可以调用数组的任何方法...
o.push(3);

  虽然这段代码很有趣,也能工作,可问题在于,并不是每一个 JavaScript 环境都支持可写的 __proto__ 对象属性。幸运的是,JavaScript 确实有一个创建对象内建的标准机制,只需要一个操作符,就可以创建新对象,并且设置新对象的 __proto__ 引用 – 那就是“new”操作符。

var o = new Array();
o.push(3);

  JavaScript 中的 new 操作符有三个基本任务。首先,它创建新的空对象。接下来,它将设置新对象的 __proto__ 属性,以匹配所调用函数的原型属性。最后,操作符调用函数,将新对象作为“this”引用传递。如果要扩展最后两行代码,就会变成如下情况:

var o = {};
o.__proto__ = Array.prototype;
Array.call(o);
o.push(3);

  函数的 call 方法允许你在调用函数的情况下在函数内部指定“this”所引用的对象。当然,函数的作者在这种情况下需要实现这样的函数。一旦作者创建了这样的函数,就可以将其称之为构造函数。

  构造函数

  构造函数和普通的函数一样,但是具有以下两个特殊性质。

  1. 通常构造函数的首字母是大写的(让识别构造函数变得更容易)。
  2. 构造函数通常要和 new 操作符结合,用来构造新对象。

  Array 就是一个构造函数的例子。Array 函数需要和 new 操作符一起使用,而且 Array 的首字母是大写的。JavaScript 将 Array 作为内置函数包括在内,而任何人都可以写出自己的构造函数。事实上,我们最后可以为先前创建的点对象编写出构造函数。

var Point = function (x, y) {
  this.x = x;
  this.y = y;
  this.add = function (otherPoint) {
    this.x += otherPoint.x;
    this.y += otherPoint.y;
  }
}
var p1 = new Point(3, 4);
var p2 = new Point(8, 6);
p1.add(p2);

  在上面的代码中,我们使用了 new 操作符和 Point 函数来构造点对象,这个对象带有 x 属性和 y 属性和一个 add 方法。你可以将最后的结果想象成图 6 的样子。

6

  现在的问题是我们的每个点对象中仍然有单独的 add 方法。使用我们学到的原型和继承的知识,我们更希望将点对象的 add 方法从每个点实例中转移到 Point.prototype 中。要达到继承 add 方法的效果,我们所需要做的,就是修改 Point.prototype 对象。

var Point = function (x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.add = function (otherPoint) {
  this.x += otherPoint.x;
  this.y += otherPoint.y;
}
var p1 = new Point(3, 4);
var p2 = new Point(8, 6);
p1.add(p2);

  大功告成!我们刚刚在 JavaScript 中完成原型式的继承模式!

7

  总结

  我希望这篇文章能够帮助你揭开 JavaScript 原型概念的神秘面纱。开始看到的是原型怎样让一个对象从其他对象中继承功能,然后看到怎样结合 new 操作符和构造函数来构建对象。这里所提到的,只是开启对象原型力量和灵活性的第一步。本文鼓励你自己发现学习有关原型和 JavaScript 语言的新信息。

  同时,请小心驾驶。你永远不会知道这些行驶在路上的车辆会从他们的原型继承到什么(有缺陷)的技术。

  原文链接: Script Junkie   翻译: 伯乐在线 - 埃姆杰

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。