この章は、ECMAScript のオブジェクト指向実装に関する第 2 部です。第 1 部では、CEMAScript の概要と比較について説明しました。まだ第 1 部を読んでいない場合は、この章に進む前に読むことを強くお勧めします。この記事は長すぎるため (35 ページ)、最初の部分を読んでください。
最も基本的なデータ型から分析していきます。最初に理解すべきことは、ECMAScript はエンティティを区別するためにプリミティブな値とオブジェクトを使用するということです。したがって、一部の記事で「JavaScript ではすべてがオブジェクトである」ということは間違いです (完全に間違っているわけではありません)。右)、プリミティブ値は、ここで説明するデータ型の一部です。
他の 3 つのタイプは実装レベルでのみアクセスでき (ECMAScript オブジェクトはこれらのタイプを使用できません)、一部の操作動作を説明し、中間値を保存するために仕様で使用されます。これらの 3 つのタイプは、参照、リスト、および補完です。
したがって、Reference は delete、typeof、this などの演算子を説明するために使用され、基本オブジェクトとプロパティ名が含まれます。List はパラメーター リストの動作を説明します (新しい式と関数呼び出しで)。 Break、Continue、Return、および throw ステートメントの動作を説明します。
おじさんのメモ: これらのネイティブ値は、私たちが通常使用する値 (Boolean、String、Number、Object) と名前が似ていますが、同じものではありません。したがって、typeof(true) と typeof(Boolean) の結果は異なります。typeof(Boolean) の結果は関数であるため、関数 Boolean、String、および Number にはプロトタイプがあります (以下の属性の読み取りと書き込みの章でも説明されています)。 。
データの型を知りたい場合は、typeof を使用するのが最適です。null の型を決定するために typeof を使用すると、結果がオブジェクトになることに注意する必要があります。なぜ? null の型が Null として定義されているためです。
仕様ではこれを説明することは想定されていませんが、Brendan Eich (JavaScript の発明者) は、オブジェクトを null 参照に設定するなど、未定義ではなく、オブジェクトが出現する場所で null が主に使用されることに気づきました。しかし、一部の文書では、これをバグのせいだと考えている人もおり、そのバグをバグ リストに追加し、Brendan Eich も議論に参加しました。その結果、(262-3 の記述にもかかわらず) typeof null の結果がオブジェクトに設定されました。標準では、null の型は Null であると定義されており、262-5 では、null の型がオブジェクトであると標準が修正されました)。
次に、Object 型 (Object コンストラクターと混同しないでください。ここでは抽象型についてのみ説明します) は、ECMAScript オブジェクトを記述する唯一のデータ型です。
第 17 章で指摘したように、ES のオブジェクトは完全に動的です。これは、プログラムの実行中にオブジェクトのプロパティを自由に追加、変更、削除できることを意味します。
一部のプロパティは変更できません (読み取り専用プロパティ、削除されたプロパティ、または構成不可能なプロパティ)。 これについては、属性のプロパティで後ほど説明します。
さらに、ES5 仕様では、静的オブジェクトを新しいプロパティで拡張することはできず、そのプロパティ ページを削除または変更することはできないと規定されています。これらはいわゆる凍結オブジェクトであり、Object.freeze(o) メソッドを適用することで取得できます。
ES5 仕様では、拡張機能を防ぐために Object.preventExtensions(o) メソッドも使用され、プロパティを定義するために Object.defineProperty(o) メソッドも使用されます。
var foo = {x : 10};
Object.defineProperty(foo, "y", {
値: 20、
書き込み可能: false, // 読み取り専用
configurable: false // 構成不可
});
// 変更できません
foo.y = 200;
//
は削除できません
foo.y を削除します。 // false
// 予防と制御の拡張
Object.preventExtensions(foo);
console.log(Object.isExtensible(foo)); // false
//新しい属性を追加できません
foo.z = 30;
console.log(foo); {x: 10, y: 20}
組み込みオブジェクト、ネイティブ オブジェクト、ホスト オブジェクト
仕様では、組み込みオブジェクト、要素オブジェクト、ホスト オブジェクトも区別されていることに注意する必要があります。
組み込みオブジェクトと要素オブジェクトは ECMAScript 仕様によって定義および実装されており、この 2 つの違いは重要ではありません。 ECMAScript によって実装されるすべてのオブジェクトはネイティブ オブジェクトです (組み込みオブジェクトもあれば、ユーザー定義オブジェクトなど、プログラムの実行時に作成されるオブジェクトもあります)。組み込みオブジェクトは、プログラムの開始前に ECMAScript に組み込まれるネイティブ オブジェクトのサブセットです (たとえば、parseInt、Match など)。すべてのホスト オブジェクトはホスト環境 (通常はブラウザ) によって提供され、ウィンドウやアラートなどが含まれる場合があります。
ホスト オブジェクトは、仕様のセマンティクスに完全に準拠して、ES 自体によって実装される場合があることに注意してください。この観点から、これらは (理論的にはできるだけ早く) 「ネイティブ ホスト」オブジェクトと呼ぶことができますが、仕様では「ネイティブ ホスト」オブジェクトの概念は定義されていません。
ブール値、文字列、数値オブジェクト
さらに、仕様ではいくつかのネイティブの特殊なパッケージング クラスも定義されています。これらのオブジェクトは次のとおりです。
1. ブールオブジェクト
2. 文字列オブジェクト
3. デジタルオブジェクト
これらのオブジェクトは、対応する組み込みコンストラクターを通じて作成され、内部プロパティとしてネイティブ値を含みます。これらのオブジェクトは、プリミティブ値を変換したり、その逆を行うことができます。
var c = new Boolean(true);
var d = new String('test');
var e = 新しい数値(10);
//元の値に変換
// new キーワードなしで関数を使用します
с = ブール値(c);
d = 文字列(d);
e = 数値(e);
// オブジェクト
に再変換します
с = オブジェクト(c);
d = オブジェクト(d);
e = オブジェクト(e);
さらに、特殊な組み込みコンストラクターによって作成されたオブジェクトもあります: Function (関数オブジェクト コンストラクター)、Array (配列コンストラクター) RegExp (正規表現コンストラクター)、Math (数学モジュール)、Date (日付コンストラクター) (コンテナー)これらのオブジェクトは、Object オブジェクト タイプの値でもあり、それらの相互の違いは、以下で説明する内部プロパティによって管理されます。
文字通り
オブジェクト、配列、正規表現の 3 つのオブジェクトの値には、オブジェクト初期化子、配列初期化子、正規表現という省略形の識別子があります:
// new Array(1, 2, 3);
と同等
// または、配列 = new Array();
// 配列[0] = 1;
// 配列[1] = 2;
// 配列[2] = 3;
var 配列 = [1, 2, 3];
//
と同等
// var object = new Object();
// object.a = 1;
// object.b = 2;
// object.c = 3;
var オブジェクト = {a: 1, b: 2, c: 3};
// new RegExp("^\d $", "g")
と同等
var re = /^d $/g;
上記の 3 つのオブジェクトが新しいタイプに再割り当てされた場合、その後の実装セマンティクスは、新しく割り当てられたタイプに従って使用されることに注意してください。たとえば、Rhino の現在の実装と SpiderMonkey 1.7 の古いバージョンでは、次のようになります。オブジェクトは new キーワードのコンストラクターを使用して正常に作成されますが、一部の実装 (現在 Spider/TraceMonkey) では、型が変更された後にリテラルのセマンティクスが必ずしも変更されるわけではありません。
var getClass = Object.prototype.toString;
オブジェクト = 数値;
var foo = 新しいオブジェクト;
alert([foo, getClass.call(foo)]); // 0, "[オブジェクト番号]"
var bar = {};
// Rhino、SpiderMonkey 1.7 - 0、"[オブジェクト番号]"
// その他: まだ "[object Object]"、"[object Object]"
alert([bar, getClass.call(bar)]);
//配列でも同じ効果があります
配列 = 数値;
foo = 新しい配列;
alert([foo, getClass.call(foo)]); // 0, "[オブジェクト番号]"
bar = [];
// Rhino、SpiderMonkey 1.7 - 0、"[オブジェクト番号]"
// その他: まだ ""、"[object Object]"
alert([bar, getClass.call(bar)]);
// ただし、RegExp の場合、リテラルのセマンティクスは変更されません。リテラル
の意味論
// テストされたすべての実装で変更されていない
RegExp = 数値;
foo = 新しい正規表現;
alert([foo, getClass.call(foo)]); // 0, "[オブジェクト番号]"
bar = /(?!)/g;
alert([bar, getClass.call(bar)]); // /(?!)/g, "[object RegExp]"
正規表現リテラルと RegExp オブジェクト
次の 2 つの例では、正規表現のセマンティクスが仕様の第 3 版と同等であることに注意してください。正規表現リテラルは 1 つの文にのみ存在し、解析段階で作成されますが、正規表現リテラルは RegExp コンストラクターによって作成されます。これは新しいオブジェクトであるため、テスト中に lastIndex の値が間違っているなど、いくつかの問題が発生する可能性があります。
for (var k = 0; k
var re = /ecma/g;
アラート(re.lastIndex) // 0, 4, 0, 4
alert(re.test("ecmascript")); // true、false、true、false
}
// 比較
for (var k = 0; k
var re = new RegExp("ecma", "g");
アラート(re.lastIndex) // 0, 0, 0, 0
alert(re.test("ecmascript")); // true、true、true
}
注: ただし、これらの問題は ES 仕様の第 5 版で修正されており、リテラルに基づいているかコンストラクターに基づいているかに関係なく、通常のルールによって新しいオブジェクトが作成されます。
連想配列
さまざまなテキストの静的ディスカッション、JavaScript オブジェクト (オブジェクト初期化子 {} を使用して作成されることが多い) は、ハッシュ テーブル、ハッシュ テーブル、またはその他の単純な名前と呼ばれます: ハッシュ (Ruby または Perl の概念)、管理配列 (PHP の概念) 、辞書(Pythonの概念)など。
このような用語だけが存在するのは、主にそれらの構造が類似しているためです。つまり、「キーと値」のペアを使用してオブジェクトを格納しており、「連想配列」または「ハッシュ」の理論によって定義されたデータ構造と完全に一致しています。テーブル"。 さらに、ハッシュ テーブル抽象データ型は通常、実装レベルで使用されます。
ただし、この概念は用語で説明されていますが、ECMAScript の観点から見ると、実際には間違いです。ECMAScript にはオブジェクト、タイプ、およびそのサブタイプが 1 つだけあり、これは「キーと値」ペアのストレージと何ら変わりません。これに関しては特別な概念はありません。 あらゆるオブジェクトの内部プロパティはキーと値のペアとして保存できるため、
var a = {x: 10};
a['y'] = 20;
a.z = 30;
var b = 新しい数値(1);
b.x = 10;
b.y = 20;
b['z'] = 30;
var c = 新しい関数('');
c.x = 10;
c.y = 20;
c['z'] = 30;
// 待って、オブジェクトのサブタイプ "subtype"
また、ECMAScript ではオブジェクトが空になる可能性があるため、ここでは「ハッシュ」の概念も正しくありません。
Object.prototype.x = 10;
var a = {} // 空の「ハッシュ」を作成します
alert(a["x"]); // 10 ですが空ではありません
alert(a.toString); // 関数
a["y"] = 20 // 新しいキーと値のペアを "hash"
に追加します。
アラート(a["y"]) // 20
Object.prototype.y = 20 // プロトタイプ属性を追加します
delete a["y"] // 削除
;
alert(a["y"]); // ただし、ここのキーと値には値がまだ含まれています - 20
ES5 標準では、プロトタイプ (Object.create(null) メソッドを使用して実装) なしでオブジェクトを作成できることに注意してください。この観点から、そのようなオブジェクトはハッシュ テーブルと呼ばれます。
var aHashTable = Object.create(null);
console.log(aHashTable.toString); // 未定義
さらに、一部のプロパティには特定の getter/setter メソッドがあるため、この概念についての混乱を招く可能性もあります:
var a = new String("foo");
a['長さ'] = 10;
アラート(a['length']) // 3
ただし、「ハッシュ」には「プロトタイプ」(Ruby や Python でハッシュ オブジェクトを委任するクラスなど) があると考えられる場合でも、ECMAScript では、この 2 つの用語の間にはギャップがあるため、この用語は正しくありません。表現に意味上の違いはありません (つまり、ドット表記 a.b と a["b"] 表記を使用します)。
ECMAScript の「プロパティ属性」の概念とセマンティクスは、「キー」、配列インデックス、およびメソッドから分離されていません。ここでのすべてのオブジェクトのプロパティの読み取りと書き込みは、同じルールに従う必要があります。プロトタイプ チェーンを確認してください。
次の Ruby の例では、セマンティクスの違いがわかります。
a = {}
a.class # ハッシュ
a.長さ # 0
# 新しい「キーと値」のペア
a['長さ'] = 10;
# 意味的には、キー
ではなく、プロパティまたはメソッドにアクセスするためにドットが使用されます。
a.長さ # 1
#インデクサーはハッシュ内のキーにアクセスします
a['長さ'] # 10
# 既存のオブジェクトで Hash クラスを動的に宣言するのと似ています
# 次に、新しいプロパティまたはメソッドを宣言します
クラスハッシュ
デフz
100
終了
終了
# 新しい属性にアクセスできます
a.z #100
# ただし「鍵」ではありません
a['z'] # nil
ECMA-262-3 標準では、「ハッシュ」(および同様のもの) の概念は定義されていません。しかし、そのような構造理論があれば、それにちなんで物体に名前を付けることは可能です。
オブジェクト変換
オブジェクトをプリミティブ値に変換するには、関数のコンストラクターが関数として呼び出される場合 (一部の型の場合)、valueOf メソッドを使用できますが、 new キーワードが使用されない場合は、オブジェクトは、暗黙的な valueOf メソッド呼び出しと同等のプリミティブ値に変換されます:
var a = 新しい数値(1);
var primitiveA = Number(a) // 暗黙的な「valueOf」呼び出し
;
var alsoPrimitiveA = a.valueOf() // 明示的な呼び出し
;
アラート([
typeof a, // "オブジェクト"
typeof primitiveA, // "数値"
typeof alsoPrimitiveA // "数値"
]);
このアプローチにより、オブジェクトは次のようなさまざまな操作に参加できるようになります。
var a = 新しい数値(1);
var b = 新しい数値(2);
アラート(a b) // 3
//
でも
var c = {
x: 10、
y: 20、
valueOf: function () {
this.x this.y を返します;
}
};
var d = {
x: 30、
y: 40、
// c
の valueOf 関数と同じ
valueOf: c.valueOf
};
アラート(c d); // 100
valueOf のデフォルト値は、オブジェクトのタイプに応じて変更されます (オーバーライドされていない場合)。たとえば、Object.prototype.valueOf() および計算された値が返されます。 .valueOf() は日付と時刻を返します:
var a = {};
alert(a.valueOf() === a); // true、「valueOf」はこれを返します
var d = 新しい日付();
アラート(d.valueOf()) // 時間
alert(d.valueOf() === d.getTime()) // true
さらに、オブジェクトには文字列表現というより原始的な表現があります。 この toString メソッドは信頼性が高く、特定の操作に自動的に使用されます:
var a = {
valueOf: function () {
100 を返します;
}、
toString: function () {
'__test' を返します;
}
};
// この操作では、toString メソッドが自動的に呼び出されます
アラート(a); // "__テスト"
// ただし、ここでは valueOf() メソッドが呼び出されます
アラート(a 10); // 110
// ただし、valueOf が削除されると
// toString は自動的に再度呼び出すことができます
a.valueOf;
を削除します
アラート(a 10); // "_test10"
Object.prototype で定義された toString メソッドには、以下で説明する内部 [[Class]] 属性値が返されます。
プリミティブ値への変換(ToPrimitive)と比較して、値をオブジェクト型への変換には変換仕様(ToObject)もあります。
明示的な方法は、組み込みの Object コンストラクターを関数として使用して ToObject を呼び出すことです (new キーワードに似ています)。
var n = Object(1) // [オブジェクト番号]
;
var s = Object('test') // [オブジェクト文字列]
;
//同様に、new 演算子
も使用できます。
var b = new Object(true) // [オブジェクト ブール値]
;
// パラメーター new Object が使用される場合、単純なオブジェクトが作成されます
var o = new Object() // [オブジェクト オブジェクト]
// パラメータが既存のオブジェクトの場合
// 作成の結果は単にオブジェクトを返すだけです
var a = [];
alert(a === new Object(a)) // true
alert(a === Object(a)) // true
組み込みコンストラクターの呼び出しに関する一般的な規則はなく、new 演算子を使用するかどうかはコンストラクターによって異なります。 たとえば、配列または関数は、new 演算子を使用するコンストラクターまたは new 演算子を使用しない単純な関数として使用すると、同じ結果を生成します:
var a = Array(1, 2, 3) // [オブジェクト配列]
;
var b = new Array(1, 2, 3) // [オブジェクト配列]
;
var c = [1, 2, 3] // [オブジェクト配列]
;
var d = Function('') // [オブジェクト関数]
;
var e = new Function('') // [オブジェクト関数]
;
一部の演算子が使用される場合、明示的および暗黙的な変換も行われます:
var a = 1;
var b = 2;
// 暗黙的
var c = a b // 3, 数値
var d = a b '5' // "35", string
// 明示的
var e = '10' // "10", 文字列
;
var f = e // 10、数値
var g = parseInt(e, 10) // 10, 数値
;
// 待ってください
属性の特徴
すべてのプロパティは多くの属性を持つことができます。
1.{ReadOnly} - プロパティに値を割り当てる書き込み操作を無視しますが、読み取り専用プロパティはホスト環境の動作によって変更される可能性があります。つまり、それは「定数値」ではありません。
2.{DontEnum}——for..in ループ
で属性を列挙することはできません
3.{DontDelete}——削除演算子の動作は無視されます (つまり、削除できません);
4. {Internal} - 内部属性、名前なし (実装レベルでのみ使用される)。このような属性は ECMAScript ではアクセスできません。
ES5 では、{ReadOnly}、{DontEnum}、{DontDelete} の名前が [[Writable]]、[[Enumerable]]、および [[Configurable]] に変更されることに注意してください。これらは Object.defineProperty などを介して手動で渡すことができます。これらのプロパティを管理するメソッド。
var foo = {};
Object.defineProperty(foo, "x", {
値: 10、
writable: true, // つまり、{ReadOnly} = false
enumerable: false, // つまり、{DontEnum} = true
構成可能: true // つまり、{DontDelete} = false
});
console.log(foo.x); // 10
// 記述子を通じて機能セット属性
を取得します
var desc = Object.getOwnPropertyDescriptor(foo, "x");
console.log(desc.enumerable); // false
console.log(desc.writable); // true
// 待ってください
内部プロパティとメソッド
オブジェクトには、ECMAScript プログラムから直接アクセスできない内部プロパティ (実装レベルの一部) を持つこともできます (ただし、以下で説明するように、一部の実装ではそのようなプロパティへのアクセスが許可されています)。 これらのプロパティには、ネストされた角括弧 [[ ]] を介してアクセスします。これらのプロパティの説明は仕様に記載されていますので、いくつか見てみましょう。
すべてのオブジェクトは、次の内部プロパティとメソッドを実装する必要があります:
1.[[プロトタイプ]] - オブジェクトのプロトタイプ (以下で詳しく紹介します)
2.[[クラス]] - オブジェクトを区別するために使用される文字列オブジェクトの表現 (たとえば、オブジェクト配列、関数オブジェクト、関数など)。
3.[[Get]]——属性値を取得するメソッド
4.[[Put]]——属性値を設定するメソッド
5.[[CanPut]]——属性が書き込み可能かどうかを確認します
6.[[HasProperty]]——オブジェクトがすでにこのプロパティを持っているかどうかを確認します
7.[[削除]]——オブジェクトから属性を削除します
8.[[DefaultValue]] はオブジェクトの元の値を返します (valueOf メソッドを呼び出すと、一部のオブジェクトは TypeError 例外をスローする場合があります)。
内部プロパティ [[Class]] の値は、 Object.prototype.toString() メソッドを通じて間接的に取得できます。このメソッドは文字列 "[object " [[Class]] "]" を返します。例:
var getClass = Object.prototype.toString;
getClass.call({}); // [オブジェクト オブジェクト]
getClass.call([]); // [オブジェクト配列]
getClass.call(new Number(1)) // [オブジェクト番号]
;
// 待ってください
この関数は通常、オブジェクトをチェックするために使用されますが、仕様では、ホスト オブジェクトの [[Class]] は、組み込みオブジェクトの [[Class]] 属性の値を含む任意の値にできると規定されているため、理論的には、正確であることを 100% 保証することはできません。たとえば、document.childNodes.item(...) メソッドの [[Class]] 属性は IE では「String」を返しますが、他の実装では「Function」を返します。
// IE の場合 - "文字列"、その他の場合 - "関数"
alert(getClass.call(document.childNodes.item));
コンストラクター
上で述べたように、ECMAScript のオブジェクトは、いわゆるコンストラクターを通じて作成されます。
コンストラクターは、新しく作成されたオブジェクトを作成して初期化する関数です。
コンストラクターは、新しく作成されたオブジェクトを作成して初期化する関数です。
オブジェクトの作成 (メモリ割り当て) は、コンストラクターの内部メソッド [[Construct]] によって処理されます。この内部メソッドの動作は明確に定義されており、すべてのコンストラクターはこのメソッドを使用して新しいオブジェクトにメモリを割り当てます。
初期化は、コンストラクターの内部メソッド [[Call]] を担当する新しいオブジェクトに対してこの関数を呼び出すことによって管理されます。
ユーザー コードは初期化フェーズ中にのみアクセスできることに注意してください。ただし、初期化フェーズ中には別のオブジェクトを返すことができます (最初のフェーズで作成されたオブジェクトは無視します)。
関数 A() {
// 新しく作成したオブジェクトを更新します
this.x = 10;
// ただし、別のオブジェクトが返されます
return [1, 2, 3];
}
var a = new A();
console.log(a.x, a); 未定義、[1, 2, 3]
第 15 章「関数 - 関数を作成するためのアルゴリズム」を参照すると、関数が [[Construct]] ] 属性と [[Call]] ] 属性、および表示されるプロトタイプのプロトタイプ属性を含むネイティブ オブジェクトであることがわかります。 future オブジェクトのプロトタイプ (注: NativeObject はネイティブ オブジェクトの規則であり、以下の疑似コードで使用されます)。
F = 新しい NativeObject();
F.[[クラス]] = "関数"
.... // その他の属性
F.[[Call]] = // 関数自体
F.[[Construct]] = InternalConstructor // 通常の内部コンストラクター
.... // その他の属性
// F コンストラクターによって作成されたオブジェクト プロトタイプ
__objectPrototype = {};
__objectPrototype.constructor = F // {DontEnum}
F.prototype = __objectPrototype
[[Call]] ] は、[[Class]] 属性 (ここでは「関数」に相当) 以外のオブジェクトを区別する主な方法であるため、オブジェクトの内部 [[Call]] 属性は関数。 このようなオブジェクトに対して typeof 演算子を使用すると、「function」が返されます。ただし、場合によっては、値を取得するための typeof の使用方法が異なります。例: IE の window.alert (...) の効果:
// IE ブラウザの場合 - "Object"、"object"、その他のブラウザの場合 - "Function"、"function"
alert(Object.prototype.toString.call(window.alert));
alert(typeof window.alert); // "オブジェクト"
内部メソッド [[Construct]] は、new 演算子を指定したコンストラクターを使用してアクティブ化されます。前述したように、このメソッドはメモリの割り当てとオブジェクトの作成を担当します。パラメーターがない場合は、コンストラクターを呼び出すためのかっこも省略できます:
function A(x) { // コンストラクター А
this.x = x || 10;
}
// パラメーターが渡されない場合、括弧は省略できます
var a = new A; // または new A();
アラート(a.x); // 10
//パラメータ x
を明示的に渡します
var b = 新しい A(20);
アラート(b.x); // 20
また、コンストラクター (初期化フェーズ) の shis が新しく作成されたオブジェクトに設定されていることもわかります。
オブジェクト作成アルゴリズムを勉強しましょう。
オブジェクト作成のアルゴリズム
内部メソッド [[Construct]] の動作は次のように説明できます:
F.[[Construct]](initialParameters):
O = 新しい NativeObject();
// プロパティ [[Class]] は "Object" に設定されます
O.[[クラス]] = "オブジェクト"
// F.prototype を参照するときにオブジェクト g
を取得します
var __objectPrototype = F.prototype;
// __objectPrototype がオブジェクトの場合:
O.[[プロトタイプ]] = __objectPrototype
// それ以外の場合:
O.[[プロトタイプ]] = Object.prototype;
// ここで、O.[[Prototype]] は Object オブジェクトのプロトタイプです
// F.[[Call]]
は、新しく作成されたオブジェクトを初期化するときに適用されます。
// これを新しく作成したオブジェクト O
に設定します
//パラメータはF
のinitialParametersと同じです
R = F.[[Call]](initialParameters); this === O;
// ここで R は [[Call]]
の戻り値です
// 次のように JS で表示します:
// R = F.apply(O,InitialParameters);
// R がオブジェクトの場合
R
を返す
// それ以外の場合
Oを返します
2 つの主な機能に注意してください:
1. まず、新しく作成されたオブジェクトのプロトタイプは、現時点での関数のプロトタイプ属性から取得されます (これは、同じコンストラクターによって作成された 2 つのオブジェクトのプロトタイプが異なる可能性があることを意味します。関数は異なる場合もあります)。
2. 次に、上で述べたように、オブジェクトの初期化時に [[Call]] がオブジェクトを返す場合、これはまさに新しい演算子全体に使用される結果です:
関数 A() {}
A.prototype.x = 10;
var a = new A();
alert(a.x); // 10 – プロトタイプから
を取得します
// .prototype プロパティを新しいオブジェクトに設定します
// .constructor プロパティを明示的に宣言する理由については以下で説明します
A.プロトタイプ = {
コンストラクター: A,
y: 100
};
var b = 新しい A();
// オブジェクト "b" には新しいプロパティがあります
アラート(b.x); // 未定義
alert(b.y); // 100 – プロトタイプから
を取得します
// ただし、オブジェクトのプロトタイプは元の結果を取得できます
alert(a.x); // 10 - プロトタイプから
を取得します
関数 B() {
this.x = 10;
return new Array();
}
// "B" コンストラクターが返さない場合 (またはこれを返す場合)
// この場合、このオブジェクトは使用できますが、次の場合は array
が返されます。
var b = new B();
アラート(b.x); // 未定義
alert(Object.prototype.toString.call(b)); // [オブジェクト配列]
プロトタイプを詳しく見てみましょう
プロトタイプ
すべてのオブジェクトにはプロトタイプがあります (一部のシステム オブジェクトを除く)。プロトタイプ通信は、内部の暗黙的で直接アクセスできない [[Prototype]] プロトタイプ プロパティを通じて実行されます。プロトタイプはオブジェクトまたは null 値にすることができます。
プロパティ コンストラクター
上記の例には 2 つの重要な知識ポイントがあります。1 つ目は、関数のコンストラクター属性のプロトタイプ属性に関するもので、関数作成アルゴリズムでは、コンストラクター属性が関数のプロトタイプ属性に設定されることがわかります。関数作成フェーズ中の関数の場合、コンストラクター属性の値は関数自体への重要な参照です:
関数 A() {}
var a = new A();
alert(a.constructor); // 関数 A() {}、委任による
alert(a.constructor === A) // true
この場合、通常、誤解があります。コンストラクターのコンストラクター プロパティは、新しく作成されたオブジェクト自体のプロパティとしては間違っていますが、見てわかるように、このプロパティはプロトタイプに属し、継承を通じてアクセスされます。
コンストラクター属性のインスタンスを継承することにより、プロトタイプ オブジェクトへの参照を間接的に取得できます。
関数 A() {}
A.prototype.x = 新しい数値(10);
var a = new A();
alert(a.constructor.prototype); // [オブジェクト オブジェクト]
alert(a.x); // 10、プロトタイプ
経由
// a.[[Prototype]].x
と同じ効果
アラート(a.constructor.prototype.x); // 10
alert(a.constructor.prototype.x === a.x) // true
ただし、関数のコンストラクター属性とプロトタイプ属性は、オブジェクトの作成後に再定義できることに注意してください。この場合、オブジェクトは上記のメカニズムを失います。関数のプロトタイプ属性を使用して要素のプロトタイプを編集すると (新しいオブジェクトの追加または既存のオブジェクトの変更)、インスタンスに新しく追加された属性が表示されます。
ただし、(新しいオブジェクトを割り当てることで) 関数のプロトタイプ プロパティを完全に変更すると、作成するオブジェクトにはコンストラクター プロパティが含まれないため、元のコンストラクターへの参照が失われます。
関数 A() {}
A.プロトタイプ = {
×:10
};
var a = new A();
アラート(a.x); // 10
alert(a.constructor === A); // false!
したがって、関数へのプロトタイプ参照を手動で復元する必要があります:
関数 A() {}
A.プロトタイプ = {
コンストラクター: A,
×:10
};
var a = new A();
アラート(a.x); // 10
alert(a.constructor === A) // true
コンストラクター属性は手動で復元されましたが、元の失われたプロトタイプと比較すると、{DontEnum} 機能は利用できなくなっていることに注意してください。これは、A.prototype の for..in ループ ステートメントがサポートされていないことを意味します。仕様の第 5 版では、 [[Enumerable]] 属性を通じて列挙可能な状態を制御する機能が提供されています。
var foo = {x: 10};
Object.defineProperty(foo, "y", {
値: 20、
enumerable: false // 別名 {DontEnum} = true
});
console.log(foo.x, foo.y); // 10, 20
for (foo の var k) {
console.log(k); // "x" のみ
}
var xDesc = Object.getOwnPropertyDescriptor(foo, "x");
var yDesc = Object.getOwnPropertyDescriptor(foo, "y");
console.log(
xDesc.enumerable, // true
yDesc.enumerable // false
);
明示的なプロトタイプと暗黙的な [[Prototype]] 属性
一般に、関数のプロトタイプ属性を通じてオブジェクトのプロトタイプを明示的に参照することは正しくありません。これは、同じオブジェクト、つまりオブジェクトの [[Prototype]] 属性を参照します:
a.[[プロトタイプ]] ----> プロトタイプ <---- A.プロトタイプ
さらに、インスタンスの [[Prototype]] 値は実際にコンストラクターのプロトタイプ属性から取得されます。
ただし、プロトタイプ属性を送信しても、すでに作成されているオブジェクトのプロトタイプには影響しません (コンストラクターのプロトタイプ属性が変更された場合にのみ影響を受けます)。つまり、新しく作成されたオブジェクトのみが新しいプロトタイプを持ちます。すでに作成されたオブジェクトには、元の古いプロトタイプへの参照が引き続き残ります (このプロトタイプは変更できなくなります)。
// A.prototype プロトタイプを修正する前の状態
a.[[プロトタイプ]] ----> プロトタイプ <---- A.プロトタイプ
// 変更後
A.prototype ----> 新しいプロトタイプ // 新しいオブジェクトにはこのプロトタイプが含まれます
a.[[プロトタイプ]] ----> プロトタイプ // 元のプロトタイプを起動します
例:
関数 A() {}
A.prototype.x = 10;
var a = new A();
アラート(a.x); // 10
A.プロトタイプ = {
コンストラクター: A,
×:20
y: 30
};
// オブジェクト a は、暗黙的な [[Prototype]] 参照を通じて原油のプロトタイプから取得された値です
アラート(a.x); // 10
alert(a.y) // 未定義
var b = 新しい A();
// ただし、新しいオブジェクトは新しいプロトタイプから取得された値です
アラート(b.x); // 20
アラート(by.y) // 30
したがって、一部の記事では、「プロトタイプを動的に変更するとすべてのオブジェクトに影響があり、すべてのオブジェクトに新しいプロトタイプが作成されます」と書かれていますが、これは誤りです。新しいプロトタイプは、プロトタイプが変更された後に新しく作成されたオブジェクトに対してのみ有効になります。
ここでの主なルールは次のとおりです。オブジェクトのプロトタイプはオブジェクトの作成時に作成され、その後は新しいオブジェクトに変更することはできません。同じオブジェクトを参照している場合は、明示的なプロトタイプを介して参照できます。オブジェクトの作成後は、プロトタイプのプロパティのみを追加または変更できます。
非標準の __proto__ 属性
ただし、SpiderMonkey などの一部の実装では、オブジェクトのプロトタイプを参照するための非標準の __proto__ 明示的属性が提供されます。
関数 A() {}
A.prototype.x = 10;
var a = new A();
アラート(a.x); // 10
var __newPrototype = {
コンストラクター: A,
x: 20、
y: 30
};
//新しいオブジェクトへの参照
A.prototype = __newPrototype;
var b = 新しい A();
アラート(b.x); // 20
アラート(by); // 30
// "a" オブジェクトは依然として古いプロトタイプを使用します
アラート(a.x); // 10
アラート(a.y); // 未定義
// プロトタイプを明示的に変更します
a.__proto__ = __newPrototype;
// これで、「а」オブジェクトは新しいオブジェクトを参照します
アラート(a.x); // 20
アラート(a.y); // 30
ES5 には、オブジェクトの [[Prototype]] プロパティ (インスタンスの初期プロトタイプ) を直接返す Object.getPrototypeOf(O) メソッドが用意されていることに注意してください。 ただし、__proto__ と比較すると、これはゲッターのみであり、値の設定は許可されません。
var foo = {};
Object.getPrototypeOf(foo) == Object.prototype // true
コンストラクターから独立したオブジェクト
インスタンスのプロトタイプはコンストラクターおよびコンストラクターのプロトタイプ属性から独立しているため、コンストラクターはその主要な作業 (オブジェクトの作成) の完了後に削除できます。プロトタイプ オブジェクトは、[[Prototype]] 属性を参照することで存在し続けます:
関数 A() {}
A.prototype.x = 10;
var a = new A();
アラート(a.x); // 10
// A を null に設定 - 参照コンストラクターを表示
A = null;
// ただし、.constructor プロパティが変更されていない場合、
// 引き続きオブジェクトを作成できます
var b = new a.constructor();
アラート(b.x); // 10
// 暗黙的な参照も削除されます
a.constructor.prototype.constructor を削除します;
b.constructor.prototype.constructor;
を削除します
// A
のコンストラクターを通じてオブジェクトを作成できなくなりました
// しかし、これら 2 つのオブジェクトにはまだ独自のプロトタイプがあります
アラート(a.x); // 10
アラート(b.x); // 10
instanceof 演算子の特徴
instanceof 演算子に関連するコンストラクターのプロトタイプ属性を通じて参照プロトタイプを表示します。この演算子はコンストラクターではなくプロトタイプ チェーンで動作します。これを念頭に置くと、オブジェクトを検出するときに誤解が生じることがよくあります。
if (foo instanceof Foo) {
...
}
これは、オブジェクト foo が Foo コンストラクターを使用して作成されたかどうかを検出するためには使用されません。すべての instanceof 演算子は 1 つのオブジェクト プロパティ (foo.[[Prototype]]) のみを必要とし、プロトタイプ チェーン内の Foo.prototype からその存在を確認します。 instanceof 演算子は、コンストラクターの内部メソッド [[HasInstance]] を通じてアクティブ化されます。
この例を見てみましょう:
関数 A() {}
A.prototype.x = 10;
var a = new A();
アラート(a.x); // 10
alert(A のインスタンス) // true
;
// プロトタイプが null に設定されている場合
A.prototype = null;
// ...「a」は引き続き a.[[Prototype]]
を通じてプロトタイプにアクセスできます。
アラート(a.x); // 10
// ただし、instanceof 演算子は通常は使用できなくなりました
// コンストラクターのprototype属性から実装されるため
alert(a instanceof A); // エラー、A.prototype はオブジェクトではありません
一方、オブジェクトはコンストラクターによって作成できますが、オブジェクトの [[Prototype]] 属性とコンストラクターのプロトタイプ属性の値が同じ値に設定されている場合、instanceof は true を返します。チェックした場合:
関数 B() {}
var b = new B();
アラート(b インスタンスオブ B) // true
関数 C() {}
var __proto = {
コンストラクター: C
};
C.prototype = __proto;
b.__proto__ = __proto;
アラート(b インスタンスオブ C) // true
アラート(b インスタンスオブ B) // false
プロトタイプはメソッドを保存し、プロパティを共有できます
プロトタイプは、オブジェクトのメソッド、デフォルトの状態、共有オブジェクトのプロパティを保存するためにほとんどのプログラムで使用されます。
実際、オブジェクトは独自の状態を持つことができますが、メソッドは通常同じです。 したがって、メソッドは通常、メモリ最適化のプロトタイプで定義されます。 これは、このコンストラクターによって作成されたすべてのインスタンスがこのメソッドを共有できることを意味します。
コードをコピー コードは次のとおりです:
関数 A(x) {
this.x = x || 100;
}
A.prototype = (function () {
// コンテキストを初期化します
// 追加のオブジェクトを使用します
var _someSharedVar = 500;
function _someHelper() {
alert('内部ヘルパー: ' _someSharedVar);
}
関数メソッド1() {
alert('method1: ' this.x);
}
関数メソッド2() {
alert('method2: ' this.x);
_someHelper();
}
// プロトタイプ自体
戻り値 {
コンストラクター: A,
メソッド 1: メソッド 1,
メソッド 2: メソッド 2
};
})();
var a = 新しい A(10);
var b = 新しい A(20);
a.method1(); // メソッド1: 10
a.method2(); // メソッド 2: 10、内部ヘルパー: 500
b.method1(); // メソッド1: 20
b.method2(); // メソッド 2: 20、内部ヘルパー: 500
// 2 つのオブジェクトはプロトタイプで同じメソッドを使用します
alert(a.method1 === b.method1) // true
alert(a.method2 === b.method2) // true
属性の読み取りおよび書き込み
前述したように、プロパティ値の読み取りと書き込みは、内部の [[Get]] メソッドと [[Put]] メソッドを通じて行われます。これらの内部メソッドは、プロパティ アクセサー: ドット表記またはインデックス表記:
を通じてアクティブ化されます。
//
と書きます
foo.bar = 10; // [[Put]]
と呼ばれます
console.log(foo.bar); // 10、[[Get]]
と呼ばれます
console.log(foo['bar']); // 同じ効果
これらのメソッドが疑似コードでどのように機能するかを見てみましょう:
[[Get]] メソッド
[[Get]] はプロトタイプ チェーンのプロパティもクエリするため、プロトタイプ内のプロパティにはオブジェクトを通じてアクセスすることもできます。
O.[[Get]](P):
// 独自の属性の場合は
を返します
if (O.hasOwnProperty(P)) {
O.P を返します;
}
// それ以外の場合は、プロトタイプの分析を続行します
var __proto = O.[[プロトタイプ]];
// プロトタイプが null の場合、unknown
を返します。
// これは可能です: 最上位の Object.prototype.[[Prototype]] は null
if (__proto === null) {
未定義を返します;
}
// それ以外の場合は、プロトタイプ チェーンで [[Get]] を再帰的に呼び出し、各レイヤーのプロトタイプで属性を検索します
// プロトタイプが null になるまで
return __proto.[[Get]](P)
[[Get]] は、次の状況でも未定義を返すことに注意してください:
if (window.someObject) {
...
}
ここで、someObject プロパティがウィンドウ内で見つからない場合は、プロトタイプ内で検索され、次にプロトタイプのプロトタイプ内で検索され、見つからない場合は定義に従って unknown が返されます。
注: in 演算子はプロパティの検索も行うことができます (プロトタイプ チェーンも検索します):
if (ウィンドウ内の「someObject」) {
...
}
これは、いくつかの特別な問題を回避するのに役立ちます。たとえば、someObject が存在する場合でも、someObject が false に等しい場合、最初の検出ラウンドは失敗します。
[[Put]] メソッド
[[Put]] メソッドは、オブジェクト自体のプロパティを作成および更新し、プロトタイプ内の同じ名前のプロパティをマスクできます。
O.[[Put]](P, V):
// 値を属性に書き込めない場合は、
を終了します。
if (!O.[[CanPut]](P)) {
戻る;
}
// オブジェクトに独自のプロパティがない場合は、作成します
// すべての属性は false
if (!O.hasOwnProperty(P)) {
createNewProperty(O, P, 属性: {
読み取り専用: false、
DontEnum: false、
DontDelete: false、
内部: false
});
}
// 属性が存在する場合は値を設定しますが、属性プロパティは変更しないでください
O.P = V
戻る;
例:
Object.prototype.x = 100;
var foo = {};
console.log(foo.x); // 100、継承されたプロパティ
foo.x = 10 // [[Put]]
console.log(foo.x); // 10、独自の属性
foo.x を削除;
console.log(foo.x); // 100 にリセットし、プロパティを継承します
プロトタイプ内の読み取り専用プロパティはマスクできず、割り当て結果は内部メソッド [[CanPut]] によって制御されることに注意してください。
// たとえば、属性の長さは読み取り専用なので、長さをマスクしてみます
関数 SuperString() {
/* 何も */
}
SuperString.prototype = new String("abc");
var foo = new SuperString();
console.log(foo.length); // 3、「abc」の長さ
// マスクを試みます
foo.length = 5;
console.log(foo.length); // まだ 3
ただし、ES5 の厳密モードでは、読み取り専用属性がマスクされている場合、TypeError が保存されます。
プロパティ アクセサー
内部メソッド [[Get]] および [[Put]] は、ECMAScript のドット表記またはインデックス作成を通じてアクティブ化され、属性識別子が正当な名前である場合、「.」を通じてアクセスでき、パーティのインデックス作成が動的に実行されます。定義された名前。
var a = {testProperty: 10};
alert(a.testProperty); // 10、
をクリックします。
alert(a['testProperty']); // 10, インデックス
var propertyName = 'プロパティ';
alert(a['test' propertyName]); // 10、動的プロパティにインデックスが付けられます
ここには非常に重要な機能があります。プロパティ アクセサーは常に ToObject 仕様を使用して、「.」の左側の値を処理します。この暗黙的な変換は、「JavaScript のすべての値はオブジェクトである」ということと関連しています (ただし、すでにご存知のとおり、JavaScript のすべての値がオブジェクトであるわけではありません)。
属性アクセサーを使用して元の値にアクセスする場合、元の値はアクセスする前にオブジェクト (元の値を含む) によってラップされ、属性にアクセスした後、ラップされたオブジェクトを通じて属性にアクセスされます。 、ラップされたオブジェクトは削除されます。
例:
var a = 10 // 元の値
// ただし、メソッドには (オブジェクトと同様に) アクセスできます
alert(a.toString()); // "10"
// さらに、
にハート属性を作成できます。
a.test = 100 // 問題ないようです
// ただし、[[Get]] メソッドはプロパティの値を返さず、unknown
を返します。
アラート(a.test); // 未定義
では、なぜ例全体の元の値は toString メソッドにアクセスできるのに、新しく作成されたテスト プロパティにはアクセスできないのでしょうか?
答えは簡単です:
まず最初に、前述したように、プロパティ アクセサーを使用した後は、元の値ではなく、ラップされた中間オブジェクト (例全体で new Number(a) が使用されています) になり、ここで toString メソッドが渡されます。プロトタイプチェーンで見つかった時間:
// a.toString():
の実行原理
1. ラッパー = new Number(a);
2.wrapper.toString() // "10"
;
3. ラッパーを削除します;
次に、[[Put]] メソッドが新しい属性を作成するときも、パッケージ化されたオブジェクトを通じて行われます:
// a.test = 100 を実行する原則:
1. ラッパー = new Number(a);
2.wrapper.test = 100;
3. ラッパーを削除します;
ステップ 3 で、ラップされたオブジェクトが削除され、新しく作成されたプロパティ ページが削除されることがわかります。つまり、ラッピング オブジェクト自体が削除されます。
[[Get]] を使用してテスト値を取得すると、パッケージング オブジェクトが再度作成されますが、今回はラップされたオブジェクトに test 属性がなくなるため、unknown が返されます。
// a.test の実行原則:
1. ラッパー = new Number(a);
2.wrapper.test; // 未定義
このメソッドは、元の値がどのように読み取られるかを説明します。また、元の値が属性にアクセスするために頻繁に使用される場合、逆に、頻繁にアクセスされない場合、または単に For に使用される場合は、時間効率を考慮してその値がオブジェクトに直接置き換えられます。計算目的であれば、この形式を保持できます。
継承
ECMAScript がプロトタイプベースの委任継承を使用していることはわかっています。チェーンとプロトタイプについては、プロトタイプ チェーンですでに説明しました。実際、委任の実装とプロトタイプ チェーンの検索と分析はすべて [[Get]] メソッドに凝縮されています。
[[Get]] メソッドを完全に理解していれば、JavaScript における継承の問題は自明でしょう。
フォーラムで JavaScript の継承について話すときは、常に 1 行のコードを使用して説明します。実際、コードはすでに継承に基づいているため、オブジェクトや関数を作成する必要はありません。は次のとおりです:
alert(1..toString()); // "1"
[[Get]] メソッドとプロパティ アクセサーがどのように機能するかはすでにわかっています。何が起こるかを見てみましょう:
1. まず、元の値 1 から新しい Number(1) までのパッケージ化オブジェクトを作成します
2. 次に、このパッケージ化オブジェクト
から toString メソッドが継承されます。
なぜ遺伝するのでしょうか? ECMAScript のオブジェクトは独自のプロパティを持つことができるため、この場合のラッパー オブジェクトには toString メソッドがありません。 つまり、Number.prototype という原則を継承しています。
微妙な点があることに注意してください。上の例の 2 つのドットはエラーではありません。最初のポイントは小数部分を表し、2 番目のポイントは属性アクセサーです:
1.toString(); // 構文エラーです。
(1).toString() // OK
1..toString() // OK
1['toString']() // OK
プロトタイプチェーン
ユーザー定義オブジェクトのプロトタイプ チェーンを作成する方法を見てみましょう。それは非常に簡単です。
関数 A() {
alert('A.[[通話]] がアクティブになりました');
this.x = 10;
}
A.prototype.y = 20;
var a = new A();
alert([a.x, a.y]); // 10 (自身)、20 (継承)
関数 B() {}
// 最新のプロトタイプチェーン方法は、オブジェクトのプロトタイプを別の新しいオブジェクトに設定することです
B.prototype = new A();
// プロトタイプのコンストラクター プロパティを修復します。そうでない場合は A
になります。
B.prototype.constructor = B;
var b = new B();
alert([b.x, b.y]); // 10、20、2 は継承されます
// [[Get]] b.x:
// b.x (いいえ) -->
// b.[[プロトタイプ]].x (はい) - 10
// [[Get]] by.y
// b.y (いいえ) -->
// b.[[プロトタイプ]].y (いいえ) -->
// b.[[プロトタイプ]].[[プロトタイプ]].y (はい) - 20
// ここで b.[[プロトタイプ]] === B.prototype,
// そして b.[[プロトタイプ]].[[プロトタイプ]] === A.prototype
このメソッドには 2 つの特徴があります:
まず、B.prototype には x 属性が含まれます。一見すると、これは正しくないように思えるかもしれません。x プロパティが A で定義されており、B コンストラクターも同様にそれを期待していると考えるかもしれません。通常の状況ではプロトタイプの継承は問題ありませんが、B コンストラクターでは、クラスベースの継承と比較して、すべての属性が子孫のサブクラスにコピーされるため、x 属性が必要ない場合があります。
ただし、(クラスベースの継承をシミュレートするために) B コンストラクターによって作成されたオブジェクトに x 属性を割り当てる必要がある場合は、いくつかの方法があります。そのうちの 1 つを後で示します。
2 番目に、これは機能ではなく欠点です。サブクラスのプロトタイプが作成されると、コンストラクターのコードも実行され、「A.[[Call]] がアクティブ化されました」というメッセージが 2 回表示されることがわかります。 - A コンストラクターを使用してオブジェクトを作成し、それを B.prototype プロパティに割り当てる場合、もう 1 つのシーンはオブジェクト自体が作成されるときです。
次の例はより重要で、親クラスのコンストラクターによってスローされる例外です。実際のオブジェクトが作成されるときにチェックする必要があるかもしれませんが、明らかに同じケースです。つまり、これらの親オブジェクトを次のオブジェクトとして使用する場合です。プロトタイプ 何かがうまくいかないでしょう。
関数 A(パラメータ) {
if (!param) {
throw 'パラメータが必要です';
}
this.param = param;
}
A.prototype.x = 10;
var a = 新しい A(20);
alert([a.x, a.param]); // 10, 20
関数 B() {}
B.prototype = new A() // エラー
;
さらに、親クラスのコンストラクターにコードが多すぎることも欠点です。
これらの「関数」と問題を解決するために、プログラマはプロトタイプ チェーンの標準パターン (以下を参照) を使用します。主な目的は、これらのラッピング コンストラクターのチェーンに必要なプロトタイプを含めることです。
関数 A() {
alert('A.[[通話]] がアクティブになりました');
this.x = 10;
}
A.prototype.y = 20;
var a = new A();
alert([a.x, a.y]); // 10 (自身)、20 (統合)
関数 B() {
// または A.apply(this, argument)
を使用します
B.superproto.constructor.apply(this, argument);
}
// 継承: 空の中間コンストラクターを介してプロトタイプを接続します
var F = function () {};
F.プロト