ちゃんと書いたことあるのが、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";
var BoyBand = function(leader) {
this.leader = leader;
this.greet = function() {
console.log(greeting)
var greeting = "Good morning! ";
let selfIntroduction = "We are TravisJapan!"
{
var greeting = "Good Evening! ";
const selfIntroduction = "We are SixTONES!";
}
console.log(greeting + selfIntroduction);
}
}
var leader;
console.log(leader);
var leader = TRAJA_LEADER_NAME;
var travisJapan = new BoyBand(leader);
console.log(travisJapan.leader);
console.log(travisJapan.KaraokeMovie);
var result = travisJapan.greet();
console.log(result);
データ型
型名 |
データの持ち方 |
値 |
備考 |
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);
let charset = 'utf-8';
console.log(typeof charset);
let finished = false;
console.log(typeof finished);
const bucket = null;
console.log(typeof bucket);
let newSongOfSixtones;
console.log(typeof newSongOfSixtones);
const signalColor = ['red', 'yellow', 'green'];
console.log(signalColor instanceof Array);
const Member = function(){};
member = new Member();
console.log(typeof member);
const bark = () => { console.log("bowbow"); };
console.log(typeof bark);
let x=1;
let y=2;
[x,y]=[y,x];
console.log(`x: ${x}, y: ${y}`);
control structures
条件分岐
等価のチェックには、===
または!==
を使用する、ただnull
とundefined
の両方をキャッチしたいときのみ== undefined
とする
==
はデータ型が違う場合にもJS側が変換して無理やり比較するため想定外の結果が得られることがある
- Javaと違いObject.equalメソッドなど無いため、オブジェクトは単純にアドレスの比較になる
let n = null;
if (n == undefined) {
console.log('Null or Undefined');
n = n === undefined ? 'undefined' : 'null';
}
if (n === 'null') console.log('null');
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 (var i=0;i<10;i++) { }
do {} while (i < 5)
markLoop:
while (true) {
while(true) {
console.log(`i: ${i}`);
if (++i > 11) {
break markLoop;
}
}
}
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]}`);
}
var fruits = ['banana', 'apple', 'melon'];
for (var e of fruits) {
console.log(e);
}
例外処理
- 例外処理はJavaと同じく、
try...catch~finally
- throwで意図的に例外発生させることができるが、Javaと違いExceptionオブジェクトを継承したものでなく、ErrorオブジェクトもしくはXxxxxErrorオブジェクトをthrowすることになる
- 文字列等いろんな型をthrowで投げることが出来てしまう
const price = -1;
try {
if (price < 0) {
throw new RangeError('price must be positive')
}
} catch(e) {
console.log(e);
} finally {
}
主要な組み込みオブジェクト
const str = '夢のHollywood'.substring(2, 11);
console.log(`Type: ${typeof str} Value: ${str}`);
console.log(4.66665.toPrecision(4));
const map = new Map([['hokuto', 'black'], ['kyomo', 'pink'], ['shintaro', 'green']]);
map.set('Jessy', 'red');
console.log(map.size);
const today = new Date();
console.log(today.getFullYear());
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');
console.log(newtext);
関数
- 関数宣言
function
キーワードで宣言するよりも簡潔かつthis
を束縛しないことからarrow関数で書くほうがより好ましい(特にネストした場合には)
- アロー関数でない場合に、
this
は関数の呼び出し方法によって変わる
- コンストラクタであれば新しいオブジェクト
- 「オブジェクトのメソッド」として呼び出された関数ではそのときのオブジェクト
- 通常の関数呼び出しであればグローバルオブジェクト(
strict
のときにはundefined
)
- アロー関数であれば、レキシカルスコープの this 値を使うため、通常の変数検索ルールに従う -> 同じスコープ内になければ外側のスコープを見に行く
const moduleLocalFunc = (numParam, strParam) => numParam + Number(strParam);
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文を使用して変数を宣言することができる。
そして、これらの変数は関数内のいかなる場所で宣言されたとしても、
その関数の先頭で宣言されたのと同じように動作します。
プロトタイプベースのオブジェクト指向
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は下記のようにVSCodeやIntelliJなどのIDEでサポートされているもので非常に有用。
また、node.jsで動くjsdocというツールでドキュメント生成することも可能。アノテーション一覧はこれ。
namespace
いわゆるJavaのpackageのような仕組みが無いため、空のオブジェクトを利用して擬似的に名前空間を作成します。階層を持つことがより望ましいため、下記のような関数を用意すると良いようです。
共通利用する関数などはnamespaceを使うべき。
function namespace(ns) {
let names = ns.split('.');
let parent = window;
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 _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の良し悪しが今回わかってよかった。