blog

1.1wワード | 初級中級フロントエンドJavaScript自己診断チェックリスト - 2

はじめに\nジュニアフロントエンド自己診断チェックリスト-1」では、基本的なことを簡単に説明しましたので、見たことのない方は復習してみてください。\n\nこのパートの内容は、主にオブジェクト、大...

Jun 9, 2020 · 5 min. read
シェア

オブジェクト

JavaScriptには8つのデータ量型があり、値には1つの型しか含まない7つのプリミティブ型があり、オブジェクトはキーと値のペアやより複雑なエンティティを保持するために使用されます。オブジェクトは、オプションの属性のリストで括弧を使用して作成することができます**{...}。** 属性はキーと値のペア{"key" : "value"}で、キーは文字列、値は任意の型です。

オブジェクトの作成

新しいオブジェクトを作成するには2つの方法があります:

// 1. コンストラクタからオブジェクトを作成する let user = new Object(); // 2. リテラルからリテラルを作成する let user = {};

オブジェクトのテキストとプロパティ

オブジェクトを作成する際、オブジェクトのプロパティの一部を初期化することができます:

let user = { name : 'leo', age : 18 }

その後、オブジェクトに属性を追加、削除、取得することができます:

// プロパティを追加する user.addr = "Japan"; // user => {name: "leo", age: 18, addr: "Japan"} // プロパティを削除する delete user.addr // user => {name: "leo", age: 18} // プロパティを変更する user.age = 20; // user => {name: "leo", age: 20} // プロパティを見つける user.age; // 20

角括弧の使い方

もちろん、オブジェクトのキーは複数単語の属性でもかまいませんが、引用符で囲み、使用する場合は角括弧で囲んで読み込まなければなりません:

let user = { name : 'leo', "my interest" : ["coding", "football", "cycling"] } user["my interest"]; // ["coding", "football", "cycling"] delete user["my interest"];

また、角括弧内の変数を使用して属性値を取得することもできます:

let key = "name"; let user = { name : "leo", age : 18 } // ok user[key]; // "leo" user[key] = "pingan"; // error user.key; // undefined

計算プロパティ

オブジェクトを作成する際、オブジェクト・リテラル、つまり 計算プロパティに 角括弧を使用することができます:

let key = "name"; let inputKey = prompt("キーを入力する", "age"); let user = { [key] : "leo", [inputKey] : 18 } // ユーザーが"age" ユーザーを作成すると、ユーザーはこのようになる: // {name: "leo", age: 18}

もちろん、計算されたプロパティを式にすることもできます:

let key = "name"; let user = { ["my_" + key] : "leo" } user["my_" + key]; // "leo"

属性名の略称

実際には、同じ属性名と属性値をより短い構文に省略することが可能です:

// もともとの書き方 let getUser = function(name, age){ // ... return { name: name, age: age } } // ToPrimitiveメソッド let getUser = function(name, age){ // ... return { name, age } }

混ぜることもできます:

// もともとの書き方 let getUser = function(name, age){ // ... return { name: name, age: 18 } } // ToPrimitiveメソッド let getUser = function(name, age){ // ... return { name, age: 18 } }

オブジェクト属性の存在検出

inキーワードの使い方

このメソッドは、オブジェクト自身のプロパティと継承されたプロパティが存在するかどうかを判定します。

let user = {name: "leo"}; "name" in user; //true以下は、JavaScriptに存在する属性のリストである。 "age" in user; //false "toString" in user; //trueSymbol.toPrimitiveは継承プロパティである。

オブジェクトの hasOwnProperty() メソッドを使用します。

このメソッドは、自身の属性の存在のみを判定することができ、継承された属性に対してはfalseを返します。

let user = {name: "leo"}; user.hasOwnProperty("name"); //true以下は、Symbol.toPrimitiveの属性の一部である。 user.hasOwnProperty("age"); //false新しいJavaScriptオブジェクトを探している人は、年齢がそれ自身の属性に存在しないことがわかるだろう。 user.hasOwnProperty("toString"); //falseこれは継承されたプロパティだが、自身のプロパティではない。

undefinedを使った判定

このメソッドは、オブジェクトの所有プロパティと継承プロパティを決定します。

let user = {name: "leo"}; user.name !== undefined; // true user.age !== undefined; // false user.toString !== undefined // true

このメソッドの1つの問題は、プロパティの値が未定義の場合、望ましい結果を返さないことです:

let user = {name: undefined}; user.name !== undefined; // falseプロパティは存在するが、値は未定義である。 user.age !== undefined; // false user.toString !== undefined; // true

条件文の直接判断

let user = {}; if(user.name) user.name = "pingan"; //名前が未定義の場合, null, false, " ", 0 であれNaNであれ、それは変わらない。 user; // {}

オブジェクトループのトラバーサル

オブジェクトのすべてのプロパティを繰り返し処理する必要がある場合、for.....inステートメントを使って

for...in

for....in 文は、オブジェクトの列挙可能なプロパティを Symbol 以外の任意の順序で反復処理します。注意 : for.....in は、インデックスの順序が重要な配列には適用しないでください。

let user = { name : "leo", age : 18 } for(let k in user){ console.log(k, user[k]); } // name leo // age 18

ES7 新しいメソッド

ES7の新機能であるObject.values()とObject.entries()は、配列型を返すという点でObject.keys()と似ています。

Object.keys()

引数オブジェクト自身の、すべての走査可能なプロパティのkin 名をメンバとする配列を返します。

let user = { name: "leo", age: 18}; Object.keys(user); // ["name", "age"]

Object.values()

引数オブジェクト自身のすべての走査可能なプロパティのキーをメンバとする配列を返します。

let user = { name: "leo", age: 18}; Object.values(user); // ["leo", 18]

引数がオブジェクトでない場合は空の配列を返します:

Object.values(10); // [] Object.values(true); // []

Object.entries()

引数オブジェクト自身のすべての走査可能なプロパティのキーと値のペアの配列をメンバとする配列を返します。

let user = { name: "leo", age: 18}; Object.entries(user); // [["name","leo"],["age",18]]

Object.entries() メソッドを手動で実装します:

// Generator関数の実装 function* entries(obj){ for (let k of Object.keys(obj)){ yield [k ,obj[k]]; } } // ジェネレーター以外の関数実装 function entries (obj){ let arr = []; for(let k of Object.keys(obj)){ arr.push([k, obj[k]]); } return arr; }

Object.getOwnPropertyNames(Obj)

このメソッドは、オブジェクト Obj が持つすべてのプロパティ(列挙可能かどうか)の名前を含む配列を返します。

let user = { name: "leo", age: 18}; Object.getOwnPropertyNames(user); // ["name", "age"]

オブジェクトのコピー

基本データ型と参照データ型の復習から始めましょう:

  • 基本的な型

概念:基本型の値はメモリ内で固定サイズを占め、スタック・メモリに格納されます。一般的には、undefined、null、Boolean、String、Number、Symbol。

  • 参照型

概念:参照型の値はオブジェクトであり、ヒープ・メモリに格納されます。スタック・メモリがオブジェクトの変数識別子とヒープ・メモリ内のオブジェクトの格納 アドレスを格納するのに対し、参照データ型はヒープ内のエンティティの開始アドレスを指 すポインタをスタックに格納します。インタプリタが参照値を探す場合、まずスタック上のアドレスを取得し、アドレスを取得した後にヒープから実体を取得します。一般的な型としては、Object、Array、Date、Function、RegExp などがあります。

基本データ型の割り当て

スタック・メモリのデータが変更されると、システムは自動的にスタック・メモリの新しい変数に新しい値を割り当てます。

let user = "leo"; let user1 = user; user1 = "pingan"; console.log(user); // "leo" console.log(user1); // "pingan"

参照データ型の割り当て

JavaScriptでは、変数はオブジェクトそのものを格納するのではなく、その「メモリ上のアドレス」、つまりオブジェクトへの「参照」を格納します。言い換えれば、それへの「参照」を格納します。 例えば、以下のleo変数はuserオブジェクトへの参照を保持しているだけです:

let user = { name: "leo", age: 18}; let leo = user;

他の変数もユーザーオブジェクトを参照することができます:

let leo1 = user; let leo2 = user;

ただし、変数は参照として格納されているため、変数leo ⇦ leo1 ⇦ leo2 の値が変更されると、参照されているオブジェクト userも変更さ れますが、user が変更されると、そのオブジェクトを参照している他のすべての変数の値も変更されます:

leo.name = "pingan"; console.log(leo); // {name: "pingan", age: 18} console.log(leo1); // {name: "pingan", age: 18} console.log(leo2); // {name: "pingan", age: 18} console.log(user); // {name: "pingan", age: 18} user.name = "pingan8787"; console.log(leo); // {name: "pingan8787", age: 18} console.log(leo1); // {name: "pingan8787", age: 18} console.log(leo2); // {name: "pingan8787", age: 18} console.log(user); // {name: "pingan8787", age: 18}

このプロセスには、問題に対する可変アドレスのポインタが含まれています。

課題

2つの変数が同じオブジェクトを参照する場合、==でも====でも真を返します。

let user = { name: "leo", age: 18}; let leo = user; let leo1 = user; leo == leo1; // true leo === leo1; // true leo == user; // true leo === user; // true

しかし、2つの変数が空のオブジェクト{}である場合、それらは等しくありません:

let leo1 = {}; let leo2 = {}; leo1 == leo2; // false leo1 === leo2; // false

オブジェクトの比較

コンセプト

概念:新しいオブジェクトは、既存のオブジェクトの非オブジェクト属性の値とオブジェクト属性への参照をコピーします。新しいオブジェクトは、既存のオブジェクトのオブジェクト属性への参照を直接コピーします。

浅いコピーは属性の最初のレベルのみをコピーし、属性の最初のレベルの値が基本データ型の場合、新しいオブジェクトと元のオブジェクトは互いに影響しませんが、属性の最初のレベルの値が複雑なデータ型の場合、同じメモリアドレスを指す新しいオブジェクトと元のオブジェクトの属性の値。

サンプルコードがシーンの浅いコピーを使用していないことを実証してください:

// 例1 オブジェクトのプリミティブコピー let user = { name: "leo", skill: { JavaScript: 90, CSS: 80}}; let leo = user; leo.name = "leo1"; leo.skill.CSS = 90; console.log(leo.name); // "leo1" console.log(user.name); // "leo1" console.log(leo.skill.CSS); // 90 console.log(user.skill.CSS);// 90 // 例2 配列の生のコピー let user = ["leo", "pingan", {name: "pingan8787"}]; let leo = user; leo[0] = "pingan888"; leo[2]["name"] = "pingan999"; console.log(leo[0]); // "pingan888" console.log(user[0]); // "pingan888" console.log(leo[2]["name"]); // "pingan999" console.log(user[2]["name"]); // "pingan999"

上記のサンプルコードから見ることができます:オブジェクトが直接コピーされるので、 参照データ型を コピーするのと同じように、新しいオブジェクトで任意の値を変更するには、ソースデータが変更されます。

次にシャローコピーを実施し、次のように比較します。

シャローコピーの実装

Object.assign()

Object.assign(target, ...sources)構文: オブジェクトをコピーするES6メソッドは、最初の引数にコピーのターゲット、残りの引数にコピーのソース、sourcesを取ります。 詳細については、"MDN Object.assign "を参照してください。

// 例1 オブジェクトの浅いコピー let user = { name: "leo", skill: { JavaScript: 90, CSS: 80}}; let leo = Object.assign({}, user); leo.name = "leo1"; leo.skill.CSS = 90; console.log(leo.name); // "leo1" console.log(user.name); // "leo" console.log(leo.skill.CSS); // 90 console.log(user.skill.CSS);// 90 // 例2 配列の浅いコピー let user = ["leo", "pingan", {name: "pingan8787"}]; let leo = Object.assign({}, user); leo[0] = "pingan888"; leo[2]["name"] = "pingan999"; console.log(leo[0]); // "pingan888" console.log(user[0]); // "leo" console.log(leo[2]["name"]); // "pingan999" console.log(user[2]["name"]); // "pingan999"

プリントアウトからわかるように、シャローコピーはルートプロパティに新しいオブジェクトを作成するだけで、プロパティの値がオブジェクトである場合にのみ、同じメモリアドレスのコピーを作成します。

Object.assign()の使い方に注意:

  • ソース・オブジェクト自身の属性のみをコピーします;
  • オブジェクトの列挙不可能なプロパティをコピーしません;
  • プロパティ名 Symbol の値を持つプロパティは、Object.assign でコピーできます;
  • undefinedとnullはオブジェクトに変換できないので、Object.assignの引数としては使えませんが、ソース・オブジェクトとしては使えます。
Object.assign(undefined); // Object.assign(null); // Object.assign({}, undefined); // {} Object.assign({}, null); // {} let user = {name: "leo"}; Object.assign(user, undefined) === user; // true Object.assign(user, null) === user; // true

Array.prototype.slice()

arr.slice([begin[, end]])構文: slice() メソッドは、begin と end によって決定される、元の配列の浅いコピーである新しい配列オブジェクトを返します。元の配列は変更されません。詳細は"MDN Array slice"を参照ください。

// 例 配列の浅いコピー let user = ["leo", "pingan", {name: "pingan8787"}]; let leo = Array.prototype.slice.call(user); leo[0] = "pingan888"; leo[2]["name"] = "pingan999"; console.log(leo[0]); // "pingan888" console.log(user[0]); // "leo" console.log(leo[2]["name"]); // "pingan999" console.log(user[2]["name"]); // "pingan999"

Array.prototype.concat()

var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])構文: concat() メソッドは、2 つ以上の配列を結合するために使用します。このメソッドは既存の配列を変更せず、新しい配列を返します。詳細はMDN Array concatを参照ください。

let user = [{name: "leo"}, {age: 18}]; let user1 = [{age: 20},{addr: "fujian"}]; let user2 = user.concat(user1); user1[0]["age"] = 25; console.log(user); // [{"name":"leo"},{"age":18}] console.log(user1); // [{"age":25},{"addr":"fujian"}] console.log(user2); // [{"name":"leo"},{"age":18},{"age":25},{"addr":"fujian"}]

Array.prototype.concat これも浅いコピーで、ルート・プロパティに新しいオブジェクトを作成するだけですが、プロパティの値がオブジェクトの場合、同じメモリ・アドレスのコピーは1つだけです。

演算子の拡張

var cloneObj = { ...obj };構文:extension演算子も浅いコピーです。 オブジェクトを値とする属性を2つの異なるオブジェクトに完全にコピーすることはできませんが、属性がすべて基本型の値である場合は、extension演算子を使うのが有利で便利です。

let user = { name: "leo", skill: { JavaScript: 90, CSS: 80}}; let leo = {...user}; leo.name = "leo1"; leo.skill.CSS = 90; console.log(leo.name); // "leo1" console.log(user.name); // "leo" console.log(leo.skill.CSS); // 90 console.log(user.skill.CSS);// 90

手書きシャローコピー

実装の原則:新しいオブジェクトは、既存のオブジェクトの非オブジェクト属性の値とオブジェクト属性への参照をコピーします。

function cloneShallow(source) { let target = {}; for (let key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } return target; }
  • for in

for....in文は、オブジェクト自身の、継承された、列挙可能な、シンボル以外のプロパティを任意の順序で走査します。それぞれの異なるプロパティに対して、ステートメントが実行されます。

  • hasOwnProperty

この関数はブール値を返します。 Object を継承するすべてのオブジェクトは hasOwnProperty メソッドを継承しますが、このメソッドは in 演算子とは異なり、自身のプロパティだけでなくプロトタイプ・チェインから継承したプロパティも無視します。obj.hasOwnProperty(prop)構文:prop は、テストするプロパティの文字列名または Symbol です。

浅いコピー

コンセプト

変数の値、および参照データについては、コピーする前に基本型に再帰的にコピーします。ディープコピーされたオブジェクトは、元のオブジェクトから完全に分離され、お互いに影響を与えません。

ディープコピーの実装

JSON.parse(JSON.stringify())

原則は、オブジェクトをJSON文字列にシリアライズし、オブジェクトの内容を文字列形式にしてディスクに保存し、JSON.parse()を使用してJSON文字列を新しいオブジェクトにデシリアライズすることです。

let user = { name: "leo", skill: { JavaScript: 90, CSS: 80}}; let leo = JSON.parse(JSON.stringify(user)); leo.name = "leo1"; leo.skill.CSS = 90; console.log(leo.name); // "leo1" console.log(user.name); // "leo" console.log(leo.skill.CSS); // 90 console.log(user.skill.CSS);// 80

JSON.stringify() 使用上の注意:

  • コピーされたオブジェクトの値に関数、undefined、シンボルが含まれている場合、JSON.stringify()で`シリアライズ'された後のJSON文字列からキーと値のペアが失われます;
  • 列挙不可能なプロパティをコピーできず、オブジェクトのプロトタイプ・チェインをコピーできません;
  • Date参照型をコピーすると文字列になります;
  • RegExp 参照型をコピーすると、空のオブジェクトになります;
  • NaN、Infinity、-Infinity を含むオブジェクトは null にシリアライズされます;
  • オブジェクトをコピーできないループアプリケーション。

サードパーティライブラリ

手書きディープコピー

コアとなるアイデアは再帰的で、オブジェクトや配列の内部が基本的なデータ型になるまでトラバースし、コピーに移動し、コピーの深さです。 実装コード:

const isObject = obj => typeof obj === 'object' && obj != null; function cloneDeep(source) { if (!isObject(source)) return source; // 非オブジェクトは自分自身に戻る const target = Array.isArray(source) ? [] : {}; for(var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (isObject(source[key])) { target[key] = cloneDeep(source[key]); // ここに注目 } else { target[key] = source[key]; } } } return target; }

この方法の欠点は、循環参照に遭遇することで、再帰ループに陥り、スタックが爆発する可能性があることです。他の書き方については、.

ディープコピー

浅いコピー:オブジェクトの各プロパティは順番にコピーされますが、オブジェクトのプロパティの値が参照型の場合、コピーされるのは参照であり、参照が指す値が変更されると、その値も一緒に変更されます。

ディープコピー:変数の値をコピーし、参照データについては、基本型に再帰的に、コピーします。ディープコピー後のオブジェクトは、元のオブジェクトから完全に分離され、お互いに影響を与えません、一方のオブジェクトへの変更は、他のオブジェクトに影響を与えません。

ディープコピーとシャローコピーは複雑なデータ型のためのもので、シャローコピーは1つのレイヤーのみをコピーし、ディープコピーはレイヤーをコピーします。

III.ガベージコレクション機構

、自動メモリ管理メカニズムです。プログラムによって占有されていたメモリ空間の一部がそのプログラムによってアクセスできなくなると、そのプログラムは、ごみ収集アルゴリズムの助けを借りて、そのメモリ空間の一部をオペレーティング・システムに返します。ごみ収集器は、プログラマーの負担を軽減し、プログラムのエラー数を減らします。ごみ収集は、最初はLISP言語で生まれました。現在では、Smalltalk、Java、C#、Dなど多くの言語がゴミ収集器をサポートしており、有名なJavaScriptには自動ゴミ収集メカニズムがあります。

JavaScriptでは、プリミティブ型のデータはスタック空間に、参照型のデータはヒープ空間に割り当てられます。

スタック空間のガベージコレクション

関数showNameが呼び出されると、 MDN 配列スライス下に移動してshowName関数を破棄し、その後に他の関数が呼び出されると、古いメモリを上書きして別の関数の実行コンテキストを格納することで、ゴミ回収を実現しています。イメージは「Browser Working Principles and Practices」から引用しています。

ヒープ空間でのガベージコレクション

このゴミ山のデータ収集戦略は、MDN Array concatいます。つまり

  1. ほとんどのオブジェクトがメモリ上に存在する時間は非常に短く、その多くはすぐにアクセスできなくなります。
  2. 不滅のものは長生きします。

この2つの特徴はJavaScriptだけでなく、JavaやPythonなどほとんどの動的言語にも当てはまります。V8エンジンはヒープ空間を新世代と旧世代の領域に分け、異なるガベージコレクタを使用します。V8エンジンはヒープ空間を新世代と旧世代の2つの領域に分け、異なるガベージコレクタを使用します。

  • 主に新世代のゴミ収集を担当する二次ゴミ収集業者。
  • 主に旧世代のゴミ収集を担当するゴミ収集業者。

どのようなゴミコレクターであっても、それらはすべて同じゴミ収集プロセスを使用します:アクティブなオブジェクトと非アクティブなオブジェクトをマークし、非アクティブなオブジェクトからメモリを取り戻し、最後にメモリマーシャリング。**

セカンダリ・ガベージ・コレクタ

Scavengeアルゴリズムを使用して処理され、新世代空間はオブジェクト領域とフリー領域の2つの領域に半分に分割されます。イメージはBrowser Working Principles and Practicesより

プロセスを実行します:

  • オブジェクト・エリアには新しいオブジェクトが存在し、オブジェクト・エリアが上書きされそうになると、ゴミ収集が行われます;
  • ゴミ回収では、まず対象エリア内のゴミに目印を付け、生き残ったゴミを副ゴミ回収員がコピーして空きエリアに整然と並べることで、メモリの仕分けを完了させることに相当します。
  • コピーが完了すると、オブジェクト領域と空き領域が反転され、ゴミ収集操作が完了します。

もちろん、コピー操作のデータが大きいとクリーニング効率に影響するという問題もあります。JavaScriptエンジンの解決策は、新しい生成領域を小さく設定し、新しい生成領域が小さいことに起因する、生きているオブジェクトで領域全体が埋まってしまう問題を避けるために、オブジェクトプロモーション戦略を使用することです。

主なガベージコレクタ

マーククリアとマークソートの2種類のアルゴリズムがあります。

a) マーク・パージ・アルゴリズムの処理:

  • マーキングプロセス:ルート要素のセットから開始して要素全体をトラバースし、到達可能な要素はアクティブオブジェクトであり、その逆はゴミデータです;
  • クリア処理:タグ付けされたデータをクリーンアップし、断片化されたメモリを大量に生成します。

イメージは「How Browsers Work and Practice」より引用

b) タグ付け - 照合アルゴリズムプロセス:

  • マーキングプロセス:ルート要素のセットから開始して要素全体をトラバースし、到達可能な要素はアクティブオブジェクトであり、その逆はゴミデータです;
  • 片付けプロセス:残っているオブジェクトをすべて取り出し、セグメントに向かって移動させ、終了の境界を越えて内容を消去します。

イメージは「How Browsers Work and Practice」より引用

多読

1.図解Javaガベージコレクションの仕組み2.MDNメモリ管理

IV.オブジェクトメソッドと this

オブジェクトメソッド

詳しくは ゴミ収集 ご覧ください。オブジェクトのプロパティであるメソッドは "オブジェクトメソッド "と呼ばれます:

let user = {}; let say = function(){console.log("hello!")}; user.say = say; // オブジェクトに値を代入する user.say(); // "hello!"

もっと簡潔なアプローチを使うこともできます:

let user = { say: function(){} // say (){console.log("hello!")} // ES8 async async say (){/.../} } user.say();

もちろん、オブジェクトのメソッド名や計算プロパティの名前もメソッド名としてサポートされています:

const hello = "Hello"; let user = { ['say' + hello](){console.log("hello!")} } user['say' + hello](); // "hello!"

また、すべてのメソッド定義はコンストラクタではないので、インスタンス化しようとすると世代仮説スローされることにも注意してください。

let user = { say(){}; } new user.say; // TypeError: user.say is not a constructor

this

this

オブジェクトのメソッドがオブジェクトのプロパティを使用する必要がある場合、thisキーワードを使用することができます:

let user = { name : 'leo', say(){ console.log(`hello ${this.name}`)} } user.say(); // "hello leo"

user.say()が実行されると、thisはユーザ・オブジェクトを指します。もちろん、変数名 user を使用して say() メソッドを直接参照することもできます:

let user = { name : 'leo', say(){ console.log(`hello ${user.name}`)} } user.say(); // "hello leo"

しかし、これは安全ではありません。ユーザー・オブジェクトが別の変数に代入されている可能性があり、ユーザー・オブジェクトに別の値を代入するとエラーになる可能性があるからです:

let user = { name : 'leo', say(){ console.log(`hello ${user.name}`)} } let leo = user; user = null; leo.say(); // Uncaught TypeError: Cannot read property 'name' of null

しかし、user.nameをthis.nameに変更すると、コードは正常に動作します。

this

this""の値は 実行 時に 計算さ れ、コードのコンテキストに依存します:

let user = { name: "leo"}; let admin = {name: "pingan"}; let say = function (){ console.log(`hello ${this.name}`) }; user.fun = say; admin.fun = say; // 関数内部では、これは「ドットの前」にあるオブジェクトである。 user.fun(); // "hello leo" admin.fun(); // "hello pingan" admin['fun'](); // "hello pingan"

ルール: obj.fun()が呼び出された場合、thisはfun関数呼び出し中のobjであるため、上記のthisは最初のユーザー、次に管理者。""は最初のユーザーで、次に管理者です。

しかし、グローバル環境では、this は、ストリクト・モードが有効かどうかにかかわらず、グローバル・オブジェクトを指します。

console.log(this == window); // true let a = 10; this.b = 10; a === this.b; // true

thisを持たないアロー関数

アロー関数は、独自の this を持たないという点で特殊であり、this への参照がある場合、それは外部の通常の関数を指しています。「以下の例では、thisはuser.say()メソッドを指しています:

let user = { name : 'leo', say : () => { console.log(`hello ${this.name}`); }, hello(){ let fun = () => console.log(`hello ${this.name}`); fun(); } } user.say(); // hello => say() 外部関数はウィンドウ user.hello(); // hello leo => fun() 外部関数はhello

call / apply / bind

詳しくはMDN メソッドの定義 」をお読みください。この""値を別の環境にバインドしたい場合は、call / apply / bind メソッドを使用します:

var user = { name: 'leo' }; var name = 'pingan'; function fun(){ return console.log(this.name); // this Symbol.toPrimitiveの値は、関数がどのように呼び出されるかに依存する } fun(); // "pingan" fun.call(user); // "leo" fun.apply(user); // "leo"

注意: var name = 'pingan';は、varを使用して宣言する必要があります。 letを使用した場合、ウィンドウ上にname変数は存在しません。

3つの文法は以下の通り:

fun.call(thisArg, param1, param2, ...) fun.apply(thisArg, [param1,param2,...]) fun.bind(thisArg, param1, param2, ...)

オプションの連鎖"?."

コンストラクタ

コンストラクタの役割は、 再利用可能なオブジェクト生成コードを実装 することです。一般的に、コンストラクタには2つの規約があります:

  • 命名の際は頭文字を大文字にします;
  • はnew演算子でしか実行できません。

new 演算子は、ユーザー定義オブジェクト・タイプのインスタンス、またはコンストラクタを持つ組込みオブジェクトのインスタンスを作成します。構文は以下のとおりです:

new constructor[([arguments])]

パラメータは以下の通り:

  • コンストラクタ オブジェクトのインスタンスの型を指定するクラスまたは関数。
  • argumentsコンストラクタの呼び出しで使用する引数のリスト。

簡単な例

簡単な例です:

function User (name){ this.name = name; this.isAdmin = false; } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false

new 演算子の処理

関数がnew演算子を使って実行される場合、次のようなステップを踏みます:

  1. 新しい空のオブジェクトが作成され、thisに割り当てられます。
  2. 関数本体が実行されます。通常はthisに新しいプロパティを追加して変更します。
  3. この値を返します。

先ほどのUserメソッドを例にとってみましょう:

function User(name) { // this = {}; // これにプロパティを追加する this.name = name; this.isAdmin = false; // return this; } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false

new User('leo')が実行されると、次のようになります:

  1. User.prototypeを継承した新しいオブジェクトが作成されます;
  2. 指定されたパラメータでコンストラクタUserを呼び出し、新しく生成されたオブジェクトにthisをバインドします;
  3. コンストラクタが返すオブジェクトは、new 式の結果です。コンストラクタが明示的にオブジェクトを返さない場合は、手順 1 で作成したオブジェクトが使用されます。

注意が必要

  1. 一般的に、コンストラクタは値を返しませんが、開発者はオブジェクトを積極的に返すことで、通常のオブジェクト生成ステップをオーバーライドすることができます;
  2. new Userはnew User()と同じですが、引数のリストを指定しない、つまりUserは引数を取りません;
let user = new User; // <-- パラメーターなし // let user = new User();
  1. どの関数もコンストラクタとして動作させることができます。

コンストラクタのメソッド

コンストラクタ内でメソッドをthisにバインドすることも可能です:

function User (name){ this.name = name; this.isAdmin = false; this.sayHello = function(){ console.log("hello " + this.name); } } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false leo.sayHello(); // "hello leo"

Symbol

詳しくは TypeError ご覧ください。

背景

実際の開発では、以下のようなエラーがよく発生します:

// 1. オブジェクトに存在しないプロパティを指定する const leo = {}; console.log(leo.name.toString()); // Uncaught TypeError: Cannot read property 'toString' of undefined // 2. 存在しないDOMノードのプロパティを使う const dom = document.getElementById("dom").innerHTML; // Uncaught TypeError: Cannot read property 'innerHTML' of null

オプションのチェーン? が現れた場合、短絡演算 && 演算子を使用して問題を解決します:

const leo = {}; console.log(leo && leo.name && leo.name.toString()); // undefined

この書き方の欠点は、 面倒くさい ことです。

オプショナルチェーン入門

オプショナル・チェイニング ? は、 入れ子になったオブジェクトのプロパティにアクセスするためのエラー防止メソッド です。介在するプロパティが存在しなくてもエラーは発生しません。オプショナル連鎖の最初の部分 ? の前に undefined または null がある場合は、演算を停止して undefined を返します。

文法:

obj?.prop obj?.[expr] arr?.[index] func?.(args)

** 前の例のコードを修正してください:

// 1. オブジェクトに存在しないプロパティを指定する const leo = {}; console.log(leo?.name?.toString()); // undefined // 2. 存在しないDOMノードのプロパティを使う const dom = document?.getElementById("dom")?.innerHTML; // undefined

使用上の注意

オプションのチェーンがあるのは良いことですが、いくつか注意すべき点があります:

  1. オプショナル・チェーンの使いすぎは禁物です;

のみを使用すべきです。 が使われるべきなのは、上のサンプル・コードに示されているように、いくつかの属性やメソッドが存在しない可能性がある場所だけです:

const leo = {}; console.log(leo.name?.toString());

なぜなら、leoオブジェクトは存在する必要がありますが、name属性は存在しない可能性があるからです。

  1. オプションのチェーン? 変数が宣言されている必要があります;

オプショナル・チェーンの変数 ? 変数は let/const/var を使って宣言しなければなりません:

leo?.name; // Uncaught ReferenceError: leo is not defined
  1. オプションのチェーンは割り当てに使用できません
let object = {}; object?.property = 1; // Uncaught SyntaxError: Invalid left-hand side in assignment
  1. オプションの連鎖を使用した配列要素へのアクセス方法
let arrayItem = arr?.[42];

その他?.() ?.[]

注意すべき点は、? は特殊な構文構造であり、演算子ではありません:

オプションの連鎖と関数呼び出し ?()

?.() は、例えば、存在しないかもしれない関数を呼び出すために使われます:

let user1 = { admin() { alert("I am admin"); } } let user2 = {}; user1.admin?.(); // I am admin user2.admin?.();

?.() はその左の部分をチェックします。もし管理関数が存在すれば、呼び出しはそれを実行します。そうでなければ、操作は停止し、エラーは発生しません。

オプションのチェーンと式?.[]

?.[] 存在しないかもしれないオブジェクトからプロパティを安全に読み取れるようにします。

let user1 = { firstName: "John" }; let user2 = null; // 仮に、このユーザーが認証できないと仮定する let key = "firstName"; alert( user1?.[key] ); // John alert( user2?.[key] ); // undefined alert( user1?.[key]?.something?.not?.existing); // undefined

オプションのチェーン?. 構文まとめ

オプションのチェーン ? 構文には3つの形式があります:

見てわかるように、これらの構文形式は単純で使いやすい。 左の部分がNULL/未定義かどうかをチェックし、そうでなければ処理を続行します。?. 連鎖により、入れ子になったプロパティに安全にアクセスできます。

VIII.生の値の変換

この仕様では、JavaScriptのオブジェクトのプロパティは String型か Symbol 型しか 使えないことになっています。

コンセプト

ES6では、主にプロパティ名の衝突を防ぐために、ユニークな値を表す新しいプリミティブなデータ型としてSymbolが導入されました。ES6以降、JavaScriptにはSymbol、undefined、null、Boolean、String、Number、Objectの合計1つのデータ型があります。使い方は簡単です:

let leo = Symbol(); typeof leo; // "symbol"

Symbolは、コードのデバッグを容易にするために、パラメータをSymbol名として渡すことができます。

let leo = Symbol("leo");

備考**

  • シンボル関数はnewを使用できません。

Symbolはオブジェクトではなくプリミティブ型なので、属性を追加することはできません。

let leo = new Symbol() // Uncaught TypeError: Symbol is not leo constructor
  • パラメータが同じであっても、シンボルは同じではありません。
// パラメーターなし let leo1 = Symbol(); let leo2 = Symbol(); leo1 === leo2; // false // let leo1 = Symbol('leo'); let leo2 = Symbol('leo'); leo1 === leo2; // false
  • 記号は他の型の値では計算できず、エラーが報告されます。
let leo = Symbol('hello'); leo + " world!"; // `${leo} world!`; //
  • 記号は自動的に文字列に変換することはできません。
let leo = Symbol('hello'); alert(leo); // Uncaught TypeError: Cannot convert a Symbol value to a string String(leo); // "Symbol(hello)" leo.toString(); // "Symbol(hello)"
  • 記号はブーリアン値には変換できますが、数値には変換できません:
let a1 = Symbol(); Boolean(a1); !a1; // false Number(a1); // TypeError a1 + 1 ; // TypeError
  • Symbol属性はfor.ループには参加しません。
let id = Symbol("id"); let user = { name: "Leo", age: 30, [id]: 123 }; for (let key in user) console.log(key); // name, age (no symbols) // Symbolタスクを使って直接アクセスする console.log( "Direct: " + user[id] );

リテラルの属性名としてシンボルを使用します。

オブジェクトリテラルのプロパティ名としてSymbolを使用する場合は、 角括弧を 使用する必要があります。利点:同じ名前の属性を防ぐことができ、キーの書き換えや上書きを防ぐことができます。

let leo = Symbol(); // let user = {}; user[leo] = 'leo'; // let user = { [leo] : 'leo' } // let user = {}; Object.defineProperty(user, leo, {value : 'leo' }); // 3結果は同じである user[leo]; // 'leo'

注意 :オブジェクト属性名としてのシンボルはドット演算子を使用できず、角括弧で囲む必要があります。

let leo = Symbol(); let user = {}; // ポイント演算は使えない user.leo = 'leo'; user[leo] ; // undefined user['leo'] ; // 'leo' // 必ず角括弧で囲むこと let user = { [leo] : function (text){ console.log(text); } } user[leo]('leo'); // 'leo' // 上記はMore conciseに相当する。 let user = { [leo](text){ console.log(text); } }

また、すべての値が不等であることが保証された定数のセットを作成するためにもよく使われます

let user = {}; user.list = { AAA: Symbol('Leo'), BBB: Symbol('Robin'), CCC: Symbol('Pingan') }

アプリケーション:マジックストリングの排除

マジック・ストリング(Magic String):コード中に何度も出現する、強く結合された文字列や値。

function fun(name){ if(name == 'leo') { console.log('hello'); } } fun('leo'); // 'hello' 魔法の文字列へ

魔法の文字列を排除するために頻繁に使用される変数:

let obj = { name: 'leo' }; function fun(name){ if(name == obj.name){ console.log('hello'); } } fun(obj.name); // 'hello'

シンボルを使用することで、強いカップリングがなくなり、特定の値を関連付ける必要がなくなります。

let obj = { name: Symbol() }; function fun (name){ if(name == obj.name){ console.log('hello'); } } fun(obj.name); // 'hello'

プロパティ名 イテレーション

シンボルはプロパティ名としてトラバースされ、for...には表示されません。ループのObject.getOwnPropertyNames()ループのfor...内には現れず、Object.keys()、JSON.stringify()でも返されません。

let leo = Symbol('leo'), robin = Symbol('robin'); let user = { [leo]:'18', [robin]:'28' } for(let k of Object.values(user)){console.log(k)} // let user = {}; let leo = Symbol('leo'); Object.defineProperty(user, leo, {value: 'hi'}); for(let k in user){ console.log(k); // } Object.getOwnPropertyNames(user); // [] Object.getOwnPropertySymbols(user); // [Symbol(leo)]

Object.getOwnPropertySymbolsメソッドは、現在のオブジェクトのプロパティ名として使用されているすべてのシンボル値の配列を返します。

let user = {}; let leo = Symbol('leo'); let pingan = Symbol('pingan'); user[leo] = 'hi leo'; user[pingan] = 'hi pingan'; let obj = Object.getOwnPropertySymbols(user); obj; // [Symbol(leo), Symbol(pingan)]

また、Reflect.ownKeys メソッドを使用して、通常のキー名と Symbol キー名を含むすべてのタイプのキー名を返すこともできます。

let user = { [Symbol('leo')]: 1, age : 2, address : 3, } Reflect.ownKeys(user); // ['age', 'address',Symbol('leo')]

名前のプロパティとしてのSymbol値は、通常のメソッドでは走査されないため、オブジェクトの内部で使用されるプライベートでないメソッドを定義するためによく使用されます。

Symbol.for()Symbol.keyFor()

Symbol.for()

Symbol値を再利用するために使用します。パラメータとして文字列を受け取り、このパラメータを名前とするSymbol値が存在する場合はこのSymbolを返し、そうでない場合は新しいSymbolを作成し、このパラメータを名前とするSymbol値を返します。

let leo = Symbol.for('leo'); let pingan = Symbol.for('leo'); leo === pingan; // true

Symbol()とSymbol.for()の違い:

Symbol.for('leo') === Symbol.for('leo'); // true Symbol('leo') === Symbol('leo'); // false

Symbol.keyFor()

使用されたSymbol型のキーを返すために使用します

let leo = Symbol.for('leo'); Symbol.keyFor(leo); // 'leo' let leo = Symbol('leo'); Symbol.keyFor(leo); // undefined

組み込みのシンボル値

ES6には、言語内で使用されるメソッドを指す11の組み込みSymbol値があります:

Symbol.hasInstance

このメソッドは、他のオブジェクトが instanceof 演算子を使用してそのオブジェクトのインスタンスかどうかを判断するときに呼び出されます。Foo[Symbol.hasInstance](foo)例えば、foo instanceof Fooは実際には.

class P { [Symbol.hasInstance](a){ return a instanceof Array; } } [1, 2, 3] instanceof new P(); // true

Pはクラスであり、new P()はそのインスタンスを返します。Symbol.hasInstanceメソッドは、instanceof操作が実行されたときに自動的に呼び出され、左側の演算子がArrayのインスタンスであるかどうかを判定します。

Symbol.isConcatSpreadable

Array.prototype.concat()この値は、.NET で使用する際にオブジェクトを展開できるかどうかを示すブール値です。

let a = ['aa','bb']; ['cc','dd'].concat(a, 'ee'); // ['cc', 'dd', 'aa', 'bb', 'ee'] a[Symbol.isConcatSpreadable]; // undefined let b = ['aa','bb']; b[Symbol.isConcatSpreadable] = false; ['cc','dd'].concat(b, 'ee'); // ['cc', 'dd',[ 'aa', 'bb'], 'ee']

記号

get フェッチャーを使用して派生オブジェクトを作成するときに使用されるコンストラクタを指します。

class P extends Array { static get [Symbol.species](){ return this; } }

次の問題を解いてください:

// 質問:bはArrayのインスタンスのはずだが、実際にはPのインスタンスである。 class P extends Array{} let a = new P(1,2,3); let b = a.map(x => x); b instanceof Array; // true b instanceof P; // true // 解答:Symbolを使う.species class P extends Array { static get [Symbol.species]() { return Array; } } let a = new P(); let b = a.map(x => x); b instanceof P; // false b instanceof Array; // true

Symbol.match

str.match(myObject)が実行され、渡された属性が存在する場合に呼び出され、メソッドの戻り値を返します。

class P { [Symbol.match](string){ return 'hello world'.indexOf(string); } } 'h'.match(new P()); // 0

Symbol.replace

String.prototype.replaceこのオブジェクトがメソッドから呼び出されると、そのメソッドの戻り値が返されます。

let a = {}; a[Symbol.replace] = (...s) => console.log(s); 'Hello'.replace(a , 'World') // ["Hello", "World"]

Symbol.hasInstance

String.prototype.searchこのオブジェクトがメソッドから呼び出されると、そのメソッドの戻り値が返されます。

class P { constructor(val) { this.val = val; } [Symbol.search](s){ return s.indexOf(this.val); } } 'hileo'.search(new P('leo')); // 2

Symbol.split

String.prototype.splitこのオブジェクトがメソッドから呼び出されると、そのメソッドの戻り値が返されます。

// 文字列オブジェクトに対するsplitメソッドの振る舞いを再定義する class P { constructor(val) { this.val = val; } [Symbol.split](s) { let i = s.indexOf(this.val); if(i == -1) return s; return [ s.substr(0, i), s.substr(i + this.val.length) ] } } 'helloworld'.split(new P('hello')); // ["hello", ""] 'helloworld'.split(new P('world')); // ["", "world"] 'helloworld'.split(new P('leo')); // "helloworld"

Symbol.iterator

オブジェクトがfor.....ループを通過するとき、Symbol.iteratorメソッドが呼び出され、オブジェクトのデフォルトのトラバーサーを返します。

class P { *[Symbol.interator]() { let i = 0; while(this[i] !== undefined ) { yield this[i]; ++i; } } } let a = new P(); a[0] = 1; a[1] = 2; for (let k of a){ console.log(k); }

Symbol.toPrimitive

このメソッドは、オブジェクトがプリミティブ型の値に変換されるときに呼び出され、オブジェクトの対応するプリミティブ型の値を返します。この呼び出しは、現在の操作モードを示す文字列引数を取ります:

  • Number : 数値に変換する必要があります。
  • 文字列:文字列に変換する必要があります。
  • デフォルト : 数値または文字列に変換できます。
let obj = { [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return 123; case 'string': return 'str'; case 'default': return 'default'; default: throw new Error(); } } }; 2 * obj // 246 3 + obj // '3default' obj == 'default' // true String(obj) // 'str'

Symbol.toStringTag

Object.prototype.toStringこのオブジェクトの上でメソッドが呼び出されると、この属性が存在する場合、その戻り値は toString メソッドによって返される文字列に表示され、オブジェクトの型を示します。つまり、このプロパティは、[object Object] または [object Array] のオブジェクトに続く文字列をカスタマイズするために使用できます。

// ({[Symbol.toStringTag]: 'Foo'}.toString()) // "[object Foo]" // class Collection { get [Symbol.toStringTag]() { return 'xxx'; } } let x = new Collection(); Object.prototype.toString.call(x) // "[object xxx]"

Symbol.unscopables

このオブジェクトは、with キーワードが使用されたときに、どのプロパティが with 環境から除外されるかを指定します。

// unscopablesがない場合 class MyClass { foo() { return 1; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 1 } // スコープ不能がある場合 class MyClass { foo() { return 1; } get [Symbol.unscopables]() { return { foo: true }; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 2 }

上記のコードでは、Symbol.unscopables属性を指定することで、with構文ブロックが現在のスコープでfoo属性を探さないようにしています。

まとめ

文字列、数値、ブール値などの変換に前のレビューが、オブジェクトの変換ルールについて話していない、一緒に見てのこの部分:。いくつかのルールを覚えておく必要があります:

  1. すべてのオブジェクトはブーリアンコンテキストで真となり、ブーリアンへの変換は行われず、文字列と数値の変換のみが行われます。
  2. 数値変換は、オブジェクトが減算されるときや、数学関数が適用されるときに発生します。例えば、Date オブジェクトは、例えば date1 - date2 のように、2 つの時刻の差を引き算することができます。
  3. 文字列変換の場合、これは通常alert(obj)という形で起こります。

特殊なオブジェクト・メソッドを使って文字列や数値の変換を微調整することは確かに可能です。以下に3つの型変換について説明します:

object to string

alert "のような文字列を期待するオブジェクトに対して操作を実行する場合、オブジェクトから文字列への変換:

// alert(obj); // オブジェクトをプロパティ・キーとして使う anotherObj[obj] = 123;

object to number

数学的演算を行う際など、オブジェクトを数値に変換:

// 明示的な変換 let num = Number(obj); // 数学の演算 let n = +obj; // 単項足し算 let delta = date1 - date2; // /大小比較 let greater = user1 > user2;

object to default

演算子が期待する値の型について「不確か」な場合がいくつかあります。例えば、二項加算 + は数値だけでなく文字列にも使用できるため、文字列型と数値型の両方が可能です。そのため、2進加算にオブジェクト型の引数が与えられると、「デフォルト」に従って変換します。また、オブジェクトが文字列、数値、シンボルとの == 比較に使用される場合、どの変換が実行されるべきかは明確ではないため、"default "が使用されます。

// デフォルトのヒントを使った2進加算 let total = obj1 + obj2; // obj == number デフォルトのヒントを使う if (user == 1) { ... };

型変換アルゴリズム

変換を実行するために、JavaScriptは3つのオブジェクトメソッドを見つけて呼び出そうとします:

  1. obj[Symbol.toPrimitive](hint) このメソッドが存在する場合、シンボルキー Symbol.toPrimitive で -- 呼び出します。
  2. 文字列」のヒントにtoString -> valueOf

Symbol.toPrimitive

詳細は、MDN|Symbol.toPrimitiveを 参照してください。Symbol.toPrimitiveは組み込みのSymbol値で、オブジェクトの関数値プロパティとして存在し、オブジェクトが対応するプリミティブ値に変換されるときに呼び出されます。簡単な例を示します:

let user = { name: "Leo", money: 9999, [Symbol.toPrimitive](hint) { console.log(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; alert(user); // コンソール:ヒント: string {name: "John"} alert(+user); // コンソール:ヒント: number ポップアップボックス:9999 alert(user + 1); // コンソール:ヒント: default ポップアップボックス:10000

toString/valueOf

toString / valueOf は変換の比較的初期の実装です。Symbol.toPrimitiveがない場合、JavaScriptはそれらを見つけ、以下の順序で試そうとします:

  • その他valueOf -> toString
  • "[object Object]"それ以外の場合は、.

どちらのメソッドも生の値を返す必要があります。toString や valueOf がオブジェクトを返す場合、その戻り値は無視されます。デフォルトでは、通常のオブジェクトは toString と valueOf メソッドを持っています:

  • toString メソッドは文字列 "[object Object]" を返します。
  • valueOf メソッドはオブジェクト自身を返します。

簡単な導入例

const user = {name: "Leo"}; alert(user); // [object Object] alert(user.valueOf() === user); // true

また、toStringとvalueOfを組み合わせて、上記5で説明したユーザーオブジェクトを実装することも可能です:

let user = { name: "Leo", money: 9999, // ヒント="string" toString() { return `{name: "${this.name}"}`; }, // ヒント="number" "default" valueOf() { return this.money; } }; alert(user); // コンソール:ヒント: string {name: "John"} alert(+user); // コンソール:ヒント: number ポップアップボックス:9999 alert(user + 1); // コンソール:ヒント: default ポップアップボックス:10000

概要

今回は、「ジュニアフロントエンドJavaScript自己診断チェックリスト」の第2弾として、JavaScriptのオブジェクトをベースにした内容を紹介し、Symbol.toPrimitiveメソッドなど、気になる知識ポイントもありました。また、このチェックリストが、自分のJavaScriptレベルの自己診断や、ギャップの確認、過去の学習などに役立てば幸いです。

Read next