LoginSignup
2
1

More than 3 years have passed since last update.

JavaScript Primerを読んで学んだこと(2/3)

Last updated at Posted at 2021-03-08

はじめに

この記事は第1回の記事に続き、第2回になります。
JavaScriptを体系的に学べるサイト「JavaScript Primer」を読み、個人的に重要だと感じたことをまとめました。
JavaScriptを学習する際に参考にしていただければ幸いです。

プロトタイプオブジェクト

JavaScriptにおけるほぼ全てのオブジェクトは、prototypeオブジェクトを継承しています。
prototypeオブジェクトは、全てのオブジェクトから利用できるメソッドなどを提供するベースオブジェクトと言えます。

例えば以下に示すtoStringメソッドはprototypeオブジェクトに定義されたメソッドなので、定義することなく全てのオブジェクトから呼び出すことができます。

const obj = {};
console.log(obj.toString()); // "[object Object]"

このようなprototypeオブジェクトに組み込まれているメソッドは、プロトタイプメソッドと呼ばれます。

配列

配列の作成とアクセス

配列の作成は配列リテラル( [ と ] )の中に要素をカンマ( , )区切りで記述するだけです。
また配列[インデックス]と記述することで、そのインデックスの要素を配列から読み取れます。

const array = ["one", "two", "three"];
console.log(array[0]); // => "one"

存在しないインデックスに対してアクセスした場合は、例外ではなくundefinedを返します。

const array = ["one", "two", "three"];
// `array`にはインデックスが100の要素は定義されていない
console.log(array[100]); // => undefined

要素の追加と削除

配列は可変長であるため、作成後の配列に対して要素を追加、削除できます。
配列の任意のインデックスの要素を追加、削除するにはspliceメソッドを利用できます。

const array = [];
array.splice(インデックス, 削除する要素数);
// 削除と同時に要素の追加もできる
array.splice(インデックス, 削除する要素数, ...追加する要素); // ...(Spread構文)については後述

以下に使用例を示します。

const array = ["a", "b", "c", "d"];
// 1番目から2つの要素("b", "c")を削除
array.splice(1, 2);
console.log(array); // => ["a", "d"]
// 2番目に新しい要素("e")を追加
array.splice(2, 0, "e");
console.log(array); // => ["a", "d", "e"]

結合

concatメソッドを使うことで、配列と配列や新しい要素を結合した新しい配列を作成できます。

const array = ["A", "B", "C"];
const newArray = array.concat(["D", "E"]);
console.log(newArray); // => ["A", "B", "C", "D", "E"]

const newArray = array.concat("新しい要素");
console.log(newArray); // => ["A", "B", "C", "D", "E", "新しい要素"]

また、...(Spread構文)を使うことで、配列リテラル中に既存の配列を展開できます。

const array = ["A", "B", "C"];
const newArray = ["X", ...array, "Z"];
console.log(newArray); // => ["X", "A", "B", "C", "Z"]

Spread構文はconcatメソッドとは異なり、配列リテラル中の任意の位置に配列を展開できます。

配列を反復処理するメソッド

forEachメソッド

forEachメソッドは、配列の情報を先頭から順番にコールバック関数へ渡し、反復処理を行うメソッドです。第1引数に要素、第2引数にインデックス、第3引数に配列そのものを渡します。

const array = ["a", "b", "c"];
array.forEach((currentValue, index, array) => {
    console.log(currentValue, index, array);
});
// コンソールの出力
// "a", 0, ["a", "b", "c"]
// "b", 1, ["a", "b", "c"]
// "c", 2, ["a", "b", "c"]

mapメソッド

mapメソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数が返した値から新しい配列を返すメソッドです。配列の各要素を加工したい場合に利用します。

第1引数に要素、第2引数にインデックス、第3引数に配列そのものを渡し、配列要素の先頭から順番に反復処理します。mapメソッドの返り値は、それぞれのコールバック関数が返した値を集めた新しい配列です。

const array = [1, 2, 3];
// 各要素に10を乗算した新しい配列を作成する
const newArray = array.map((currentValue, index, array) => {
    return currentValue * 10;
});
console.log(newArray); // => [10, 20, 30]

filterメソッド

filterメソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数がtrueを返した要素だけを集めた新しい配列を返すメソッドです。配列から不要な要素を取り除いた配列を作成したい場合に利用します。

第1引数に要素、第2引数にインデックス、第3引数に配列そのものを渡し、配列要素の先頭から順番に反復処理します。filterメソッドの返り値は、コールバック関数がtrueを返した要素だけを集めた新しい配列です。

const array = [1, 2, 3];
// 奇数の値を持つ要素だけを集めた配列を返す
const newArray = array.filter((currentValue, index, array) => {
    return currentValue % 2 === 1;
});
console.log(newArray); // => [1, 3]

文字列

文字列の結合

文字列を結合する簡単な方法は文字列結合演算子(+)を使う方法です。

const str = "a" + "b";
console.log(str); // => "ab"

const name = "JavaScript";
console.log("Hello " + name + "!");// => "Hello JavaScript!"

文字へのアクセス

文字列の特定の位置にある文字にはインデックスを指定してアクセスできます。

const str = "文字列";
// 配列と同じようにインデックスでアクセスできる
console.log(str[0]); // => "文"
console.log(str[1]); // => "字"
console.log(str[2]); // => "列"
// 42番目の要素は存在しない
console.log(str[42]); // => undefined

文字列の長さ

lengthプロパティは文字列の要素数を返します。

console.log("文字列".length); // => 3

エスケープシーケンス

文字列リテラル中にはそのままでは入力できない特殊な文字もあります。
エスケープシーケンスは、\と特定の文字を組み合わせることで、特殊文字を表現します。

  エスケープシーケンス         意味      
\' シングルクォート
\" ダブルクォート
\` バッククォート
\\ バックスラッシュ
\n 改行
\t タブ

ラッパーオブジェクト

JavaScriptのデータ型は、プリミティブ型( NumberStringなどの基本的な値の型 )とオブジェクトに分けられます。
いくつかのプリミティブ型のデータには、それぞれ対応するオブジェクトが存在し、プリミティブ型の値に対してのラッパーオブジェクトと呼びます。

ラッパーオブジェクトとプリミティブ型の対応は次のとおりです。

ラッパーオブジェクト プリミティブ型
Boolean 真偽値 trueやfalse
Number 数値 1や2
String 文字列 "文字列"
Symbol シンボル Symbol("説明")

たとえば文字列に対応するオブジェクトとして、Stringオブジェクトがあります。このStringオブジェクトをnewすることで、Stringオブジェクトのインスタンスを作ることができ、文字列を明示的にオブジェクトとして扱うことができます。

// "input value"の値をラップしたStringのインスタンスを生成
const str = new String("input value");
// StringのインスタンスメソッドであるtoUpperCaseを呼び出す
str.toUpperCase(); // => "INPUT VALUE"

JavaScriptでは、プリミティブ型の値に対してプロパティアクセスするとき、自動で対応するラッパーオブジェクトに変換されます。これにより、プリミティブ型の値がラッパーオブジェクトのインスタンスメソッドを呼び出せるようになります。

const str = "string";
// プリミティブ型の値に対してメソッド呼び出しを行う
str.toUpperCase();
// `str`へアクセスする際に"string"がラッパーオブジェクトへ変換され、
// ラッパーオブジェクトはStringのインスタンスなのでメソッドを呼び出せる
// つまり、上のコードは下のコードと同じ意味である
(new String(str)).toUpperCase();

リテラルを使ったプリミティブ型の文字列とラッパーオブジェクトを使った文字列オブジェクトを明示的に使い分ける利点はないため、常にリテラルを使うことが推奨されています。

スコープ

スコープとは変数の名前や関数などの参照できる範囲を決めるものです。スコープの中で定義された変数はスコープの内側でのみ参照でき、スコープの外側からは参照できません。

関数によるスコープ

次のコードでは、fn関数のブロック( { と } )内で変数xを定義しています。この変数xfn関数のスコープに定義されているため、fn関数の内側では参照できます。一方、fn関数の外側から変数xは参照できないためReferenceErrorが発生します。

function fn() {
    const x = 1;
    // fn関数のスコープ内から`x`は参照できる
    console.log(x); // => 1
}
fn();
// fn関数のスコープ外から`x`は参照できないためエラー
console.log(x); // => ReferenceError: x is not defined

ブロックスコープ

{}で囲んだ範囲をブロックと呼び、スコープを作成します。ブロック内で宣言された変数はスコープ内でのみ参照でき、スコープの外側からは参照できません。
ブロックによるスコープのことをブロックスコープと呼びます。

// ブロック内で定義した変数はスコープ内でのみ参照できる
{
    const x = 1;
    console.log(x); // => 1
}
// スコープの外から`x`を参照できないためエラー
console.log(x); // => ReferenceError: x is not defined

スコープチェーン

以下のように入れ子になったスコープを考えます。

{
    // OUTERブロックスコープ
    const x = "x";
    {
        // INNERブロックスコープからOUTERブロックスコープの変数を参照できる
        console.log(x); // => "x"
    }
}

上記の例では、INNERブロックスコープからOUTERブロックスコープの変数を参照できています。これは、参照したい変数(今回はx)を探索する際に次のようなステップを踏むからです。

  1. INNERブロックスコープに変数xがあるかを確認 => ない
  2. ひとつ外側のOUTERブロックスコープに変数xがあるかを確認 => ある

この内側から外側のスコープへと順番に変数が定義されているか探す仕組みのことをスコープチェーンと呼びます。

グローバルスコープ

プログラム直下( ブロックで囲まれていない一番外側の部分 )には、暗黙的なグローバルスコープと呼ばれるスコープが存在します。

// プログラム直下はグローバルスコープ
const x = "x";
console.log(x); // => "x"

グローバルスコープで定義した変数はグローバル変数と呼ばれ、グローバル変数はあらゆるスコープから参照できる変数となります。 なぜなら、スコープチェーンの仕組みにより、最終的にもっとも外側のグローバルスコープに定義されている変数を参照できるためです。

クロージャー

クロージャーとは、関数内から特定の変数を参照し続けることで、関数が状態を持てる仕組みのことを言います。

クロージャーはJavaScriptが持つ以下の2つの仕組みを利用しています。

  • ある変数がどの値を参照するかはコードを実行する前から決まっている( 静的スコープ )
  • あるデータがどこかから参照されている限りは、そのデータがメモリ解放されることはない( ガベージコレクション )

次の例ではcreateCounter関数が、関数内で定義したincrement関数を返しています。その返されたincrement関数をmyCounter変数に代入しています。このmyCounter変数を実行するたびに1, 2, 3と1ずつ増えた値を返しています。

// `increment`関数を定義して返す関数
function createCounter() {
    let count = 0;
    // `increment`関数は`count`変数を参照
    function increment() {
        count = count + 1;
        return count;
    }
    return increment;
}
// `myCounter`は`createCounter`が返した関数を参照
const myCounter = createCounter();
myCounter(); // => 1
myCounter(); // => 2
// 新しく`newCounter`を定義する
const newCounter = createCounter();
newCounter(); // => 1
newCounter(); // => 2
// `myCounter`と`newCounter`は別々の状態持っている
myCounter(); // => 3
newCounter(); // => 3

このように、まるで関数が状態(ここでは1ずつ増えるcountという値)を持っているように振る舞える仕組みの背景にはクロージャーがあります。

クラス

クラスの定義

クラスを定義するにはclass構文を使います。クラスの定義方法にはクラス宣言文とクラス式があります。

クラス宣言文ではclassキーワードを使い、classクラス名{ }のようにクラスの構造を定義できます。

class MyClass {
    constructor() {
        // コンストラクタ関数の処理
        // インスタンス化されるときに自動的に呼び出される
    }
}

もうひとつの定義方法であるクラス式は、クラスを値として定義する方法です。クラス式ではクラス名を省略できます。

const MyClass = class MyClass {
    constructor() {}
};

const AnonymousClass = class {
    constructor() {}
};

クラスは必ずコンストラクタを持ち、constructorという名前のメソッドとして定義します。コンストラクタとは、そのクラスからインスタンスを作成する際に、インスタンスに関する状態の初期化を行うメソッドです。constructorメソッドに定義した処理は、クラスをインスタンス化したときに自動的に呼び出されます。

クラスのインスタンス化

クラスのインスタンス化には、new演算子を使用します。

class MyClass {
}
// `MyClass`をインスタンス化する
const myClass = new MyClass();

クラスにおけるメソッド

クラスにおけるメソッドは、クラスの直下に定義した場合とコンストラクタ内で定義した場合で挙動が異なります。

クラスの直下に定義したメソッドは、クラスの各インスタンスから共有されるメソッドとなります。このインスタンス間で共有されるメソッドのことをプロトタイプメソッドと呼びます。

一方コンストラクタ内で定義したメソッドは、インスタンス作成時に毎回新しく定義されるため、インスタンスによって参照先が異なります。

class Counter {
    constructor() {
        // `this`は`Counter`のインスタンスを参照する
        this.count = 0;
        this.InnerIncrement(){
            this.count++;
        }
    }
    outerIncrement() {
        this.count++;
    }
}
const counterA = new Counter();
const counterB = new Counter();
// クラスの直下に定義したメソッドは共有されている(同じ関数を参照している)
console.log(counterA.outerIncrement === counterB.outerIncrement); // => true
//コンストラクタ内で定義したメソッドは参照先は異なる
console.log(counterA.innerIncrement === counterB.innerIncrement); // => false

継承

extendsキーワードを使うことで既存のクラスを継承できます。継承とは、クラスの構造や機能を引き継いだ新しいクラスを定義することです。

また、extendsを使って定義した子クラスから親クラスを参照するにはsuperというキーワードを利用します。 もっともシンプルなsuperを使う例としてコンストラクタの処理があります。

// 親クラス
class Parent {
    constructor(...args) {
        console.log("Parentコンストラクタの処理", ...args);
    }
}
// Parentを継承したChildクラスの定義
class Child extends Parent {
    constructor(...args) {
        // Parentのコンストラクタ処理を呼び出す
        super(...args);
        console.log("Childコンストラクタの処理", ...args);
    }
}
const child = new Child("引数1", "引数2");
// "Parentコンストラクタの処理", "引数1", "引数2"
// "Childコンストラクタの処理", "引数1", "引数2"

class構文では必ず親クラスのコンストラクタ処理(super()の呼び出し)を先に行い、その次に子クラスのコンストラクタ処理を行います。

続きについて

この記事はJavaScript Primerを読んで学んだことの第2回目(全3回)になります。
第1回はこちら
第3回についてもいずれ執筆予定です。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1