1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

JSをさらっと学んでみた

ちゃんと書いたことあるのが、GoとJava(とC#)だけの自分がJavaScriptを速習しました。というのも、業務で急に必要になってしまったからです。
以前少し触ったことがあるものの中途半端な状態で触るのも無責任と思い、下記を読んで、気をつけようと思った点をまとめてみました。
クライアントサイドJS前提ですが、FetchAPIとかクライアントサイドに特化した部分はここではメモしていない。ES5をメインで使わなくてはいけないシチュエーションですが、今後ES6を使っていけると信じてES6も併記していきます。

gihyo.jp

google.github.io

Variables

変数はデフォルトでundifinedという値を割り当たる

  • ある変数が宣言済みであるものの値を与えられていない場合
  • 未定義のプロパティを参照しようとした場合
  • 関数で値が返されなかった場合

変数の命名ルールはJavaと同じっぽい

  • 定数: SNAKE_CASE
  • 変数: camelCase
  • クラス: PascalCase

[ES6] ローカル変数は基本constを使って、再代入をする場合のみletを使う!varは癖がるので使わない!

  • varは宣言のスコープがブロックでなく関数
  • 変数の巻き上げによって、関数の最初に宣言されるような挙動となる
    • 宣言される前に既に変数は割り当てられているが、代入は宣言された行までされない
    • そのため、YAGNI原則に背き関数の一番最初に使用する変数を全て宣言する書き方がされる
    • 同じ変数名で再宣言できてしまう(Rustのshadowingに似ているというと聞こえはいい)
  • 「関数内でグローバル変数を書き換える」ような用途を除いては、原則としてvar命令を省略すべきではない。グローバル変数を宣言する場合には省略してもしなくても挙動は同じだが、混乱を防ぐためにvarで宣言すべき
// 定数は大文字+スネークケース
var TRAJA_LEADER_NAME = "chaka";

// クラス名はPascalCase
var BoyBand = function(leader) {
    this.leader = leader;
    this.greet = function() {
        // 変数の巻き上げによって、関数の最初に宣言されるような挙動となる(が変数の代入は行われないため「undefined」となる)
        console.log(greeting) // undefined
        var greeting = "Good morning! ";
        let selfIntroduction = "We are TravisJapan!"
        {
            var greeting = "Good Evening! ";
            const selfIntroduction = "We are SixTONES!";
        }
        // varのスコープはブロックではなく、関数なので後から宣言した方が優先される
        // let,const はブロックスコープ
        console.log(greeting + selfIntroduction);
    }
}

var leader;
console.log(leader); // 初期化していない変数のため「undefined」となる
var leader = TRAJA_LEADER_NAME;  // なんと同じ変数で再宣言できてしまう

// 変数名はCamelCase
var travisJapan = new BoyBand(leader); 
console.log(travisJapan.leader); // 「chaka」
console.log(travisJapan.KaraokeMovie); // 未定義のプロパティのため「undefined」となる

var result = travisJapan.greet();  // Good Evening! We are TravisJapan!
console.log(result); // 戻り値のない関数のため「undefined」となる

データ型

型名 データの持ち方 備考
number プリミティブ 64ビットの浮動小数点数 数値型はこの他に任意の精度を持つ整数型のBigIntが存在する(※1)
string プリミティブ 文字の集合、内部では0 個以上の 16 bit 符号なし整数の列として扱われる GoやJavaと違い参照ではない(※2)が、必要に応じて暗黙的にboxingされてStringオブジェクトとなる(※3)
文字列リテラルはダブルクォートよりもシングルクォートを優先して使う(※4)
[ES6] 文字列や複数行になるときや複雑な結合になる場合には、テンプレート文字列を利用する
boolean プリミティブ true/falseの真偽値 暗黙的なfalseを取る値がある
・ 空文字列
・ 数値の0, NaN
・ null, undefined
特殊値 プリミティブ null/undefined nullは明示的に空を表すときに使用する
array 参照 各要素に対してインデックス番号でアクセス可能なデータの集合 要素の追加/削除/反復などのメソッドを持つ
[ES6] 分割代入が便利(※5)
object 参照 各要素に対してkeyとなる名前でアクセス可能なデータ集合 JSにおいて連想配列=オブジェクト
[ES6] オブジェクト内の個々のデータをプロパティ、中でも関数が格納されたプロパティはメソッドと呼称する
function 参照 一連の処理をまとめたもの 宣言ではfunction命令よりもアロー関数が好ましいとされる(※6)

※1: JavaScript のデータ型とデータ構造 - JavaScript | MDN
※2: Goはstructだが実質byteスライスのwrapperという認識です(https://go101.org/article/string.html
※3: String - JavaScript | MDN
※4: https://google.github.io/styleguide/jsguide.html#features-strings-use-single-quotes
※5: 分割代入 - JavaScript | MDN
※6: https://google.github.io/styleguide/jsguide.html#features-functions-arrow-functions

// 初期化方法
const PI = 3.14;
console.log(typeof PI); // number
let charset = 'utf-8';
console.log(typeof charset); // string
let finished = false;
console.log(typeof finished); // boolean
const bucket = null;
console.log(typeof bucket); // object
let newSongOfSixtones;
console.log(typeof newSongOfSixtones); // undefined
const signalColor = ['red', 'yellow', 'green'];
console.log(signalColor instanceof Array);  // true
const Member = function(){};
member = new Member();
console.log(typeof member);   // object
const bark = () => { console.log("bowbow"); };
console.log(typeof bark);   // function

// [ES6]分割代入を使った典型的なswap
let x=1;
let y=2;
[x,y]=[y,x];
// [ES6]テンプレート文字列で直感的に変数を埋め込める
console.log(`x: ${x}, y: ${y}`); // x: 2, y: 1

control structures

条件分岐

等価のチェックには、===または!==を使用する、ただnullundefinedの両方をキャッチしたいときのみ== undefinedとする

  • ==はデータ型が違う場合にもJS側が変換して無理やり比較するため想定外の結果が得られることがある
  • Javaと違いObject.equalメソッドなど無いため、オブジェクトは単純にアドレスの比較になる
let n = null;
// nullとundefinedどちらも引っ掛けたいときのみ ==,!= を利用する 
if (n == undefined) {
    console.log('Null or Undefined'); // 出力される
    n = n === undefined ? 'undefined' : 'null'; // 3項演算子アリ!個人的には嬉しい!
}
// 基本は ===, !== で比較
if (n === 'null')  console.log('null'); // 1行であれば中括弧を省ける

// breakの後の1行開けると見通しが良い
const signal = 'red';
switch(signal) {
    case 'green':
        console.log('Go!');
        break;

    case 'yellow':
        console.log('Pay Attention!');
        break;
        
    default:
        console.log('Stop');
}

ループ

ループの制御構文としては、while, do ~ while, forに加えて、連想配列を順に処理するfor...in、配列やArrayなどEumerableなオブジェクトを順に処理するfor...ofがある
また、Go, Javaと同じようにラベル付きbreak/continueがあるのでネストが深くなったときには利用しても良さそう

// for, do while, whileの構文はJavaとほとんど変わらない
for (var i=0;i<10;i++) { }
do {} while (i < 5)
markLoop:
while (true) {
    while(true) {
        console.log(`i: ${i}`);  // iはvarで宣言されているのでアクセス出来てしまう(ES6以降はlet使う)
        if (++i > 11) {
            // ラベル付きBreakが可能で、外側のループに付与されたラベルをもとにbreakするので
            // 外側ループの無限ループに巻き込まれない
            break markLoop;
        }
    }
}
/** 上記の処理の出力
i: 10
i: 11 
**/

// for...inはオブジェクトのプロパティに対して反復する
var ExamScore = function(japanese, english) {
    this.japanese = japanese;
    this.english = english;
    this.getSum = function() {
        return this.japanese + this.english;
    }
}
var secondRegularExam = new ExamScore(87, 64);
for (var key in secondRegularExam) {
    console.log(`${key}: ${secondRegularExam[key]}`);
}
/** 上記の処理の出力
japanese: 87
english: 64
getSum: function() {
        return this.japanese + this.english;
    }
**/

// array, Map(ES6以降)などに関してはfor...ofを使用する
// for...inを使用するとユーザ定義の関数なども合わせて列挙されてしまうため
var fruits = ['banana', 'apple', 'melon'];
for (var e of fruits) {
    console.log(e);
}
/** 上記の処理の出力
banana
apple
melon
**/

例外処理

  • 例外処理はJavaと同じく、try...catch~finally
  • throwで意図的に例外発生させることができるが、Javaと違いExceptionオブジェクトを継承したものでなく、ErrorオブジェクトもしくはXxxxxErrorオブジェクトをthrowすることになる
    • 文字列等いろんな型をthrowで投げることが出来てしまう
const price = -1;
try {
    if (price < 0) {
        // 組み込みのError型がいくつか存在する
        throw new RangeError('price must be positive')
    }
} catch(e) {
    console.log(e);
} finally {
    // コネクションのクローズ処理など
}

オブジェクト指向プログラミング

主要な組み込みオブジェクト

// Stringオブジェクト
const str = '夢のHollywood'.substring(2, 11); // 内部ではboxing -> メソッド呼び出し -> unboxingが行われている?
console.log(`Type: ${typeof str}  Value: ${str}`); // Type: string  Value: Hollywood

// Numberオブジェクト
console.log(4.66665.toPrecision(4)); // 4.667

// Mapオブジェクト
const map = new Map([['hokuto', 'black'], ['kyomo', 'pink'], ['shintaro', 'green']]);
map.set('Jessy', 'red');
console.log(map.size);  // 4

// Dateオブジェクト
const today = new Date();
console.log(today.getFullYear()); // 2020

// RegExpオブジェクト
var re = new RegExp('[0-9]{4}-[a-zA-Z]+'); // オブジェクト生成時に評価
var re = /([0-9]{4})-([a-zA-Z]+)/; // 正規表現が不変のときはリテラル
var text = '2020-olynpic';
var newtext = text.replace(re, '$2_$1'); // olynpic_2020
console.log(newtext);
  • Object

    • すべてのオブジェクトの雛形
    • preventExtensions/seal/freezeといったメソッドを利用することで、Immutableにできる
  • Globalオブジェクト

    • グローバル変数やグローバル関数を管理するために、JavaScriptが自動的に生成する便宜的なオブジェクト
    • encodeURI()のようによく使われる機能などをグローバルオブジェクトに紐づく関数としてJSがデフォルトで提供している
    • ブラウザにおいてはwindowオブジェクトがグローバルオブジェクトにあたる、そのためwindow.location.hrefなどはlocation.hrefでアクセスできるし、グローバル空間において宣言したvar nwindow.nでアクセスできる
  • JSON.stringify(obj)でobjectをJSONとして吐くことができる

関数

  • 関数宣言
    • functionキーワードで宣言するよりも簡潔かつthisを束縛しないことからarrow関数で書くほうがより好ましい(特にネストした場合には)
      • アロー関数でない場合に、thisは関数の呼び出し方法によって変わる
        • コンストラクタであれば新しいオブジェクト
        • 「オブジェクトのメソッド」として呼び出された関数ではそのときのオブジェクト
        • 通常の関数呼び出しであればグローバルオブジェクト(strictのときにはundefined
      • アロー関数であれば、レキシカルスコープの this 値を使うため、通常の変数検索ルールに従う -> 同じスコープ内になければ外側のスコープを見に行く
// arrow関数
const moduleLocalFunc = (numParam, strParam) => numParam + Number(strParam);

// function命令
function moduleLocalFunction(numParam, strParam) {
  return numParam + Number(strParam);
}
  • ES2015より前はJavaScriptは引数の数をチェックしない
    • argumentsオブジェクトというのが関数配下でのみ使える特別な変数になっており、それを使って引数のチェックなどを行う
    • 可変長引数もargumentsオブジェクトを利用する
  • ES2015以降はデフォルト引数や可変長引数、名前付き引数が言語の機能として提供されるようになった
    • functiongetTriangle(base=1,height=1){...}
    • functionsum(...nums){}
    • getTriangle({base:5,height:4}))
  • ES2015以降は分割代入を利用してGoのように多値返却も可能に
functiongetMaxMin(...nums){
   return[Math.max(...nums),Math.min(...nums)];
}
let[max,min]=getMaxMin(10,35,5,78,0);
  • ES2015以降で、タグ付きテンプレート文字列を使えば、文字列を加工する関数を用意しておいて文字列定義と一緒に利用することが可能
  • Callオブジェクトとは、「関数呼び出しの都度、内部的に自動生成されるオブジェクト」で関数内で定義されたローカル変数を管理するための便宜的なオブジェクトであり、argumentsもこれで管理されている
    • 内部のCallオブジェクト、外部のCallオブジェクト、グローバルオブジェクトという順でスコープチェーンがある場合、内部から順番にオブジェクトに紐づくプロパティを参照していき、変数を解決する
  • クロージャも機能として提供されており、自分が定義されたスコープの変数をキャプチャすることができる

関数内のどこにでもvar文を使用して変数を宣言することができる。 そして、これらの変数は関数内のいかなる場所で宣言されたとしても、 その関数の先頭で宣言されたのと同じように動作します。

ES2015より前のオブジェクト指向

プロトタイプベースのオブジェクト指向
Functionにクラスとしての役割を与えている。
プロパティの定義は、自分自身を指し示すthisキーワードを使って変数を宣言する。
インスタンス化した後に動的にメソッドを追加することができるが、個人的には混乱を招きそうなのでsealメソッドを利用したほうが良さそう。

function Car(make, model) {
  this.make = make;  // プロパティの定義
  this.model = model;
  this.getName()=function(){
    return this.make + ' ' + this.model
  }  // メソッドの宣言
  Object.seal(this);  // 後からプロパティと削除/追加されないようにする
}

var car1 = new Car('Eagle', 'Talon TSi');

↓ 個人的な所感

インスタンスを生成するたびに、それぞれのインスタンスのためにメモリを確保する -> 無駄
prototypeプロパティに格納されたメンバーは、インスタンス化された先のオブジェクトに引き継がれる

JSで保守性を上げる

JSDoc

メソッド引数などに型の宣言が無い以上、ドキュメンテーションによる判断が重要になってくる。JSDocは下記のようにVSCodeIntelliJなどのIDEでサポートされているもので非常に有用。
また、node.jsで動くjsdocというツールでドキュメント生成することも可能。アノテーション一覧はこれ

f:id:jrywm121:20200510005657p:plain

namespace

いわゆるJavaのpackageのような仕組みが無いため、空のオブジェクトを利用して擬似的に名前空間を作成します。階層を持つことがより望ましいため、下記のような関数を用意すると良いようです。
共通利用する関数などはnamespaceを使うべき。

function namespace(ns) {
    let names = ns.split('.');
    let parent = window; // 最上位のwindowオブジェクト
    
// 'parent[v] || {}' とすることによって、parent[v]が定義されていないときのみ新たなオブジェクトを生成するという形
    names.forEach(v => {
        parent[v] = parent[v] || {};
        parent = parent[v];
    });
    return parent;
}

var common = namespace('com.example.app.common');
common.Util = function() { ... };

ファイル分割

以前までだと、別のファイルの関数を読み込むためには、html内でのスクリプトファイルの読み込みの順番を気をつける必要がありました。呼び出される側のファイルを先に定義しておかなければいけないためです。
ですが、ES2015以降ではモジュールという機能が登場したおかげでその状況は変わったようです。
本格入門では「執筆時点でモジュールをサポートしているブラウザーはありません」と記載がありますが、現在ではIE以外の主要なブラウザではサポートされているよう。
詳しい情報はここ参照。

private

JSではプライベートなメンバを定義する機能は無いが、クロージャを利用することで擬似的に再現できる

var Member = function() {
    // プライベートメンバはvarで定義する
    var _firstName;
    var _lastName;

    var _checkArgs = function(val) {
        return val.length > 0;
    }

    this.setFirstName = function(firstName) {
        if (_checkArgs(firstName)) {
            _firstName = firstName
        }
    }
    // 以下省略
}

感想

如何にグローバル空間を汚さないか、型の制約が無い状態で期待通りの動きをさせるかが肝なのだなと思った。
JS特有の表現で真偽値分かれるのは(例えば0であれば偽)、他の言語畑から来た人からすると分かりづらくてしんどいですね。
以前、軽くAngularを触ったときにはTypeScriptだったので、pureJSの良し悪しが今回わかってよかった。