JavaScriptオブジェクトなど(私的メモ)

JavaScriptオブジェクトなど(私的メモ)

  • JavaScriptは、あらゆるものがオブジェクトです
  • JavaScriptでは、どの変数にもあらゆる「データ型」の値を代入できます(動的型付け)
  • データ型には大きくプリミティブとオブジェクトがあります
目次
  1. プリミティブとオブジェクトについて
    1. イミュータブルとミュータブル
    2. 変数をコピー(代入)した時の挙動
  2. thisについて
    1. その前に
    2. thisの参照先
    3. thisを固定する
  3. コンストラクタ関数
    1. プロトタイプ
    2. プロトタイプ継承
    3. クラス
    4. クラス継承
  4. defineProperty()
  5. for in と for of
    1. MapとSet

プリミティブとオブジェクトについて

イミュータブルとミュータブル

  • プリミティブはイミュータブル(値の変更はできません)
  • オブジェクトはミュータブル(値の変更ができる)

プリミティブ
プリミティブ値はイミュータブルです(値の変更はできません)

オブジェクト以外のデータのことです(文字列・数値・BigInt・真偽値・undefined・Symbol ・null)
プリミティブ値は代入(=)することで新しい値になります(変更されたわけではない)

// リテラル
const str = "文字列";
// ラッパーオブジェクト
const stringWrapper = new String("文字列");

let a = "hoge";
console.log(a); // hoge
a.toUpperCase();
console.log(a) // hoge
//代入する
a = a.toUpperCase();
console.log(a) // HOGE

*プリミティブ値(nullとundefined以外)に対してプロパティでアクセスすると自動で対応するプリミティブ型の値を包んだラッパーオブジェクト(例ではStringオブジェクト)に変換されるので、メソッドの呼び出しができます

オブジェクト

関連のあるデータと機能の集合
オブジェクトリテラル({})の中にプロパティ名(キー:文字列またはシンボル)とそれに関連付けられたオブジェクトの値(バリュー)のリストです
プロパティの値を関数にできます(プロパティに関数を定義した場合、関数はメソッドと呼びます)
また配列はArrayオブジェクト(0から整数を添字として値にアクセス)ですし、関数もまた実行可能なオブジェクトです

const a = {
    prop: 'hoge',
    method: function(){
     console.log(this.prop)
   }
}
a.method() //hoge

オブジェクトはミュータブル(値の変更ができる)
プロパティやメソッドの値は変更できます(変数には値ではなく参照が保存されているため)
*オブジェクト自体ではなくプロパティの変更なので、constで宣言できます

let a = {
    prop: 'hoge',
}
console.log(a.prop)//hoge
a.prop = 'hoge2'
console.log(a.prop)//hoge2

変数をコピー(代入)した時の挙動

  • プリミティブ型:値そのものが複製されます
  • オブジェクト型:参照がコピーされますが、オブジェクト自体は複製されません(変更がコピー元にも影響します)
let a = 'hoge';
let b = a; //bにコピー
b = 'hoge2';
console.log(a, b); 
// hoge hoge2 元の値に影響なし

//オブジェクトの場合
let a = {
    prop: 'hoge'
}
let b = a;
b.prop = 'hoge2';
console.log(a.prop, b.prop)
// hoge hoge2 元のオブジェクトも変更される
//オブジェクトを新しく作成
let ary = []
lel obj = {}

スプレッド演算子を使う(オブジェクト自体を複製します)
*ただし、ネストされたオブジェクトまで複製される訳ではありません
オブジェクトをパラメータのリストに展開して新しいオブジェクトに結果を返します

let ary = [1, 2];
let newAry = [...ary]
let newAry2 = [...ary, 3]; //複製したオブジェクトに3を追加
console.log(ary, newAry2) //  [1, 2]  [1, 2, 3]

let obj = { a: 1 };
let newObj = { ...obj, b:2 };
console.log(obj, newObj) //{a: 1} {a: 1, b: 2}

ネストされたオブジェクトの複製

hoge = {
    a: {
      aa: {
        aaa: 1
      },
      bb: {
        bbb: 2
      },
      cc: {
        ccc: 3
      },
    }
  }

//bbのbbbだけを更新したオブジェクトを複製してreturn
const key = "bb";
  return {
    ...hoge, // hogeをコピー
    a: {
      ...hoge.a, // aをコピー
      [key]: { //bbだけコピー
        ...hoge.a[key],
        bbb: hoge.a[key].bbb + 3   // 3プラス
      }
    }
  }
}

余談:「Immer」はオブジェクトを簡単にイミュータブルに扱えるライブラリです(ネストされたオブジェクトもイミュータブルに扱えます)

引数の扱い(関数に中では、引数のコピーを取ります)
引数がプリミティブの場合とオブジェクトの場合の違い

//引数がプリミティブ
let a = 0;
function fn1(arg) {
    arg = 1;
    console.log(a, arg);
}
fn1(a);
//結果 0, 1 元の値に影響なし

//引数がオブジェクト
const b = {
    prop: 0
}
function fn2(arg) {
    arg.prop = 1;
    console.log(b.prop, arg.prop);
}
fn2(b);// 1 1 元のオブジェクトも変更される

//分割代入
const b = {
    prop: 0
}
function fn2({prop}) {
    prop = 1;
    console.log(b.prop);
}
fn2(b);// 0 元のオブジェクトに影響なし

//レストパラメーター
const b = {
    prop: 0
}
function fn2(...arg) {
    arg.prop = 1;
    console.log(b.prop, arg.prop);
}
fn2(b);// 0 1 元のオブジェクトに影響なし

*余談:レストパラメーターは通常引数の数が可変の時に、「残りの引数」という意味合いで使います

function myFun(a, ...manyArgs) {
  console.log(a)//1
  console.log(manyArgs)//[2, 3, 4, 5]
}
myFun(1,2,3,4,5)

分割代入
分割代入はオブジェクトから特定のプロパティを選んで宣言しているので、
そのオブジェクトとは別の新しい変数からの参照になります(値の変更は元のオブジェクトに影響しません)

const a = {
    prop: 0
}
//分割代入
let { prop } = a;
prop = 1;

console.log(a.prop, prop);
// 0 1

thisについて

  • 「this」は関数内で使える呼び出し元のオブジェクトへの参照です
  • 関数が実行された状況で「this」が参照するオブジェクトは変わります
  • 基本的にメソッドの中で利用しますが、どこにでも書けます

その前に

  • スコープは変数や関数の引数などを参照できる範囲です(関数スコープ・ブロックスコープなど)
  • トップレベルのスコープ(通常グローバルスコープ)に定義した変数やビルトインオブジェクトはどこからでも参照できます
  • 自分のスコープより外側のスコープは参照できます
  • レキシカルスコープはコードを書く場所で、参照できる変数や関数が変わるスコープです(コードを書いた時点で決まる自分のスコープより外側のスコープ)

コールバック関数とは、他の関数の引数に渡す関数です
コールバック関数として一部の処理を切り出して関数の中で実行します

クロージャーとは、関数がレキシカルスコープの変数を使用している状態のことです
関数を返す関数を利用すると、外側の関数の引数を内側の関数で利用できるようなります

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

余談(varについて)

  • 「var」で宣言された変数はブロックスコープ(if文やループ文など)は無視して一番近い外側のスコープに変数をひもづけます
  • 「var」は宣言の前に使用しても「undefined」となりエラーになりません(ホイスティングがあるためです)
  • ちなみ「関数宣言」もホイスティングがあり実行より後でもエラーになりませんが、「関数式」は実行前に宣言する必要があります
func(){
//関数スコープ
}
if(省略){
//ブロックスコープ
}
for(省略){
//ブロックスコープ
}
//関数式
const func = function() {};
//関数宣言
function func() {}

thisの参照先

  • オブジェクト.メソッド()で実行した場合は、「this」の参照先はそのオブジェクト
  • 関数名()で実行した場合は
    • strictモードでは「this」の参照先は「undefined」
    • strictモードでない場合は「this」の参照先はグローバルオブジェクト(ブラウザの場合はWindowオブジェクト)
"use strict"
const obj = {
    prop: 'hoge',
    func: function() {
        console.log(this);
    }
}
obj.func()//結果 hoge

//obj.func()を変数に代入
const test = obj.func
//関数名()で実行
test()//結果undefined

メソッドをコールバック関数として渡した場合、関数が呼ばれた場所で「this」が参照するオブジェクトは変わります

window.prop = 'グローバル';
const obj = {
    prop: 'obj',
    func: function() {
        console.log(this.prop);
    }
}
function fn(ref) {
    ref();
}
fn(obj.func);//グローバル

入れ子になった関数の「this」の参照先はグローバルオブジェクトです

const func = {
    func1: function(){
        console.log(this);
        const func2 = function(){
            console.log(this);
        }()
    }
}
func.func1()//{func1: ƒ} Window

ポイント
「メソッドを変数に代入して実行」したり、「メソッドをコールバック関数として引数に渡して実行」したりした場合は、基本的には「this」の参照先はグローバルオブジェクトです
また入れ子になった関数の「this」の参照先はグローバルオブジェクトです

thisを固定する

bind() メソッドは「thisや引数」を固定した新しい関数を生成します(使用時点で実行はされない)
bind(強制したいオブジェクト、第二引数以降は引数として引き継ぐ)

window.prop = 'グローバル';
const obj = {
    prop: 'obj',
    func: function() {
        console.log(this.prop);
    }
}
const test1 = obj.func
test1() //グローバル
//bindでオブジェクトを固定
const test2 = obj.func.bind(obj);
test2()  //obj

//fn1関数の引数にbindした新たな関数を渡す
function fn1(ref) {
    ref();
}
fn1(test2);//obj

//fn2関数の引数を強制
function fn2(arg) {
    console.log(arg);
}
const test3 = fn2.bind(null, 'aaa');
test3()//aaa

ちなみに「callメソッドとapplyメソッド」は「thisや引数」を固定した新しい関数の実行まで行います
*applyは第二引数は配列になり展開されて引数に渡されます

アロー関数では「this」はセットされません
なのでアロー関数内に「this」があれば、レキシカルスコープ(外側のスコープ)を探すことになります
アロー関数は入れ子にしても、参照されている 「this」が指すものは変わりません

window.prop = 'グローバル';
const obj = {
    prop: 'obj',
    method : function() {
        const a = () => console.log(this.prop);
        a();
    }
}
obj.method(); //obj

thisをあらかじめ(_this)に代入しておく

window.prop = 'グローバル';
const obj = {
    prop: 'obj',
    method : function() {
        const _this = this;
        const a = function(){console.log( _this.prop)}
        a();
    }
}
obj.method(); //obj

コンストラクタ関数

  • コンストラクタ関数は新しくオブジェクトを作るための関数(通常の関数と同じ)です
  • 慣例的に変数名の先頭は大文字です
  • 再利用可能なオブジェクトの作成に利用します
  • newキーワードでオブジェクトを作成します(インスタンス化)
  • newで作成したオブジェクトをインスタンスと呼びます
  • コンストラクタ関数内での「this」はこれから新しく作るインスタンスを指します
function User(name) {
  // this = {};  (暗黙で書く必要はない)
  // this へプロパティを追加
  this.name = name;
  // return this;  (暗黙で書く必要はない)
}
let user1 = new User("花子")
console.log(user1) //User {name: '花子'}

「return」が「ない」か「プリミティブ値」の場合は、暗黙的に「return this」されますが
「return」でオブジェクトを返す場合はインスタンスは「returnされたオブジェクト」です

function User(name) {
  this.name = name;
  return {}
}
let user1 = new User("花子")
console.log(user1) // {}

プロトタイプ

メソッド定義には「prototype」を使います
*コンストラクタ内で定義するとインスタンスを作成するたびにメソッドが追加され余分なメモリを使うことになります

function User(name) {
    this.name = name;
//コンストラクタ内で定義
    // this.func = function() {
    //  console.log( this.name);
    // }
}
User.prototype.func = function() {
    console.log(this.name);
}
const user1 = new User('花子');
user1.func()//花子
  • 「prototype」は特別なプロパティです
  • 「prototype」にメソッド追加すると__proto__に参照がコピーされます
  • オブジェクトのプロパティにアクセスすると、自身のオブジェクトに一致する名前のプロパティがなければ、オブジェクトの「prototype」をプロトタイプチェーンの最終(Object.prototype)まで探します

instanceof演算子は、あるコンストラクターの「prototype」が、プロトタイプチェーンの中のどこかに現れるか判定します(型の判定などに使います)
hasOwnProperty() メソッドは、オブジェクト自身がそのプロパティを持っているかを判定します(プロトタイプチェーンのプロパティは無視したいときなどに使います)

const obj = {
 prop : 'hoge'
};

console.log(obj instanceof Object); //true

console.log(obj.hasOwnProperty('prop')) //true

プロトタイプ継承

Userオブジェクトの一部を利用(継承)してAdminオブジェクトを作る

function User(name) {
    this.name = name;
}
User.prototype.func = function() {
    console.log(this.name);
}
//call()
function Admin(name, group) {
    User.call(this, name);
    this.group = group
}
// Object.create()
Admin.prototype = Object.create(User.prototype);
Admin.prototype.func2 = function() {
    console.log(this.group + this.name);
}

let user1 = new User("花子")
let admin1 = new Admin("太郎", "Aグループ")

console.log(user1)//User {name: '花子'}
console.log(admin1)//Admin {name: '太郎', group: 'Aグループ'}
user1.func() //花子
admin1.func() //太郎
admin1.func2() //Aグループ太郎

クラス

クラスは、コンストラクタ関数のシンタックスシュガー(簡単に記述できるようにしたもの)です
*constructorメソッドはインスタンス化されたときに最初に実行されます(インスタンスの初期化)

class User{
    constructor(name) {
        this.name = name;
    }
   func() {
        console.log(this.name);
    }
}
let user1 = new User("花子")

クラス継承

Userクラスを継承したAdminクラスを作る
「superキーワード」はオブジェクトの親を呼び出すために使用でき、コンストラクターで使用する場合「this」が使われる前に使用する必要があります

class User{
    constructor(name) {
        this.name = name;
    }
   func() {
        console.log(this.name);
    }
}

class Admin extends User {
    constructor(name, group) {
        super(name);
        this.group =group;
    }
   func2 = function() {
    console.log(this.group + this.name);
  }
}

let user1 = new User("花子")
let admin1 = new Admin("太郎", "Aグループ")

console.log(user1)//User {name: '花子'}
console.log(admin1)//Admin {name: '太郎', group: 'Aグループ'}
user1.func() //花子
admin1.func() //太郎
admin1.func2() //Aグループ太郎

ちなみに、メソッドは「return this」 でインスタンスを返すことでメソッドチェーンで繋ぐことができます

defineProperty()

オブジェクトのプロパティは「value」以外のプロパティの設定値を保持しています(ディスクリプター)
これらの値を変更することで挙動を変更できます

  • configurable :設定変更の可否
  • enumarable :列挙の可否
  • writable :値の変更の可否
  • setとgetはオプションなので、設定されない場合はundefinedになります

Object.defineProperty()はオブジェクトに新しいプロパティを直接定義できます
getOwnPropertyDescriptor(obj, 'prop')はオブジェクトのプロパティのディスクリプターを返します

const obj = {};

Object.defineProperty(obj, 'prop', {
       value: 0,
       configurable: true,
       enumarable: false,
       writable: true
});

console.log(obj.prop); //0

const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop');

console.log(descriptor);
//{value: 0, writable: true, enumerable: false, configurable: true}

SetterとGetterを追加設定(値の書き換え)

//definePropertyを使った場合
let user = {
  name: "太郎",
};
Object.defineProperty(user, "newName", {
  get: function () {
    return user.name;
  },
  set: function (value) {
    this. name = value;
  },
});

//オブジェクトに書いた場合
// let user = {
//   name: "太郎",
//   get newName() {
//     return this.name;
//   },
//   set newName(value) {
//     this.name = value;
//   },
// };

user.name = "花子";

console.log(user.newName);//花子
console.log(user.name)//花子
console.log(user)//{name: '花子'}

for in と for of

「for in」

  • キーが文字列であるオブジェクトの列挙可能プロパティすべてに対してループ処理をします
  • 「for in」のループで出力されるのはキーです
  • prototypeで拡張されたメソッドまで引っ張ってきます
    *基本的にビルドインメソッドでは列挙可能性はfalseなので引っ張っられることはないのですが、自分のプロパティーに限定する場合はif(obj.hasOwnProperty(key))で条件を指定します
  • 処理の順序が保証されてい真田
const obj = {
	prop1: 'v1',
	prop2: 'v2',
	prop3: 'v3',
}

for(let key in obj) {
	// if(obj.hasOwnProperty(key)) {
		console.log(key, obj[key]);
	// }
}
//prop1 v1
//prop2 v2
//prop3 v3

「for of」

  • 反復可能(iterable)オブジェクト(String・Array ・NodeList・ Map・Setなど)に対してループ処理をします(Symbol.iterator が実装されています)
  • 値が出力されます
const arry = ['v1', 'v2', 'v3'];

for(let v of arry) {
	console.log(v)
}
//v1
//v2
//v3

MapとSet

ObjectとMapの違い

  • キー
    • Objectのキーは文字列またはシンボル
    • Mapのキーは関数やオブジェクトなどもキーにできます
  • 「for in 」と「for of」
    • Objectは「 for in」
    • Mapは「for of」

Mapのメソッド

//new Map() 新しいmapを作ります
const map = new Map();
const key1 = {};
// map.set(key, value)キーで、値を格納します.
map.set(key1, 'value1');
// map.get(key)指定されたキーの値を返却
console.log(map.get(key1));//value1
//map.has(key) – キーが存在する場合にtrue
console.lg(map.has(key1)); //true
//map.delete(key) – キーで値を削除します.
//map.clear() – mapをクリアします.
//map.size – 要素の数

Mapのループ処理

let myMap = new Map()
myMap.set('key1', 'v1')
myMap.set('key2', 'v2')
myMap.set('key3', 'v3')
myMap.entries()

//上はこれと同じ
// let myMap = new Map([
//   ['key1', 'v1'],
//   ['key2', 'v2'],
//   ['key3', 'v3']
// ]);

// キーの反復
for (let key of myMap.keys()) {
  console.log(key); // key1 key2 key3
}
//値の反復
for (let v of myMap.values()){
console.log(v);//v1 v2 v3
}
//[key, value]の反復
for (let entry of myMap){
   console.log(entry);// ['key1', 'v1'] ['key2', 'v2']['key3', 'v3']
}
//forEach
myMap.forEach( (value, key)=>console.log(`${key} ${value}`));
//key1 v1, key2 v2 ,key3 v3

ArrayとSetの違い

  • 重複
    • Setは重複した値は削除されます
  • 「for in 」と「for of」
    • Arraytは「 for in」と「for of」どちらも利用できます
    • Setは「for of」
const mySet = new Set()
mySet.add(1)
mySet.add(5)
mySet.add(5)
console.log(mySet)// Set [ 1, 5 ]
console.log(mySet.has(1)) //true
console.log(mySet.size)//2

for (let item of mySet){
 console.log(item) // 1 5
}

// Setオブジェクトを  Array.fromでArrayに変換
const arry = Array.from(mySet);
console.log(arry); //[1, 5]