2017年6月7日水曜日

JSなんてもう怖くない!JavaScript/ES2015速習ガイド

はじめに

非フロントエンジニアの方々にとって、JavaScriptは独特でとっつきにくい言語だと思います。最近Reactが流行っていますが、JavaScriptが分からなくて手を出せない人も結構いるのではないでしょうか。
この記事では、普段JavaScriptをあまり触らない人を対象に、モダンなライブラリ等をさわるのに必要なJavaScriptの設計・思想・文法を解説します。関数オブジェクトや関数スコープ、thisに関しては知っておかないとES2015でも確実に躓くので、あえて比重を高くして説明しています。
※厳密ではない箇所があるかもしれませんが、ご容赦ください。

JavaScriptとは

歴史

1995年にNetscape社により開発され、当時流行していたJavaの商標を使って「JavaScript」と名付けられました。文法は比較的似ていますが、JavaとJavaScriptはインドとインドネシア並に違います
ECMA(European Computer Manufacturer's Association)に言語仕様が提出されて標準化されましたが、商標の都合で標準化版には「ECMAScript」という名前がつけられてしまいました。

特徴

関数は第一級オブジェクト

JavaScriptでは関数がオブジェクトとして扱われます。
これは大事なことなのでこの後も何度か言います。

プロトタイプベース

JavaScriptでは、各オブジェクトにプロトタイプとなるオブジェクトがあります。言い換えるとオブジェクト自身が継承関係を持っています。インスタンス自身が継承関係を持っているような感じです。
ES2015ではプロトタイプベースを意識する場面は少ないと思うので、これ以上は触れません。
オブジェクトの継承とは...

parentObj = {
  a: 'hoge',
};

childObj = {
  b: 'fuga'
};

// childObjがparentObjを継承しているとすると...
// childObj.a => 'hoge'

基本文法

変数の宣言・命名規則・文末

  • 変数はvar文で宣言します。変数の宣言時にはvar文を必ず使うようにしましょう
  • 型の束縛はありません。
  • 慣習的にキャメルケースを用います。
  • 文末のセミコロンは省略可能ですが、必ず付けるようにしましょう。
var hogeVar; // 宣言のみ
hogeVar = 1;
var fugaVar = 2; // 宣言と初期化
var fooVar, barVar = 4; // カンマ区切りで複数同時に宣言できます
fooVar = 3;

// 標準出力にはconsole.log()を使います
console.log(hogeVar); // => 1
console.log(fugaVar); // => 2
console.log(fooVar); // => 3
console.log(barVar); // => 4

制御文

制御文・演算子はCやJavaといった主要言語に似ているので、細かい説明は割愛します。例としてFizzBazzを挙げておきます。
for (var i = 1; i <= 15; i++) {
  if (i % 3 == 0 && i % 5 == 0) {
   console.log('FizzBuzz');
  } else if (i % 3 == 0) {
   console.log('Fizz');
  } else if (i % 5 == 0) {
   console.log('Buzz');
  } else {
   console.log(i);
  }
}

JavaScriptの4つの型

基本型

  • 数値: 1 2.535 2E-1
    整数と浮動小数点数の区別はなし
  • 文字列: 'あいうえお' "あいうえお" "\xxxxx"
  • 論理値: truefalse

オブジェクト型

基本型以外はすべてオブジェクト型です。配列や関数もオブジェクト型です。
オブジェクトはkey-valueのプロパティを持ちます。Rubyでいうハッシュが一番近いです。

オブジェクト


var obj0 = {}; // 空オブジェクト
var obj1 = {
  a: 'hoge',
  b: 'fuga'
};
obj0.a = 1; // プロパティを動的に追加できます
console.log(obj0.a);

// オブジェクトもプロパティに定義できます
var obj3 = {
  a: {
    p: 'hoge',
  }
};

// プロパティには2通りでアクセスできます
console.log(obj1.a);
console.log(obj1['a']); // こちらの書き方では文法上使えないキー名も扱えます

配列

// 宣言
var array = []; // 空配列
array = [1, 2, 'a', 'b', 5]; // 型を混ぜても問題ありません
var array2 = [1,,,2]; // 途中の要素が未定義でも問題ありません

// 参照
console.log(array[3]); // 'b'

// 配列オブジェクトの長さ
console.log(array.length); // 5

// 配列オブジェクトのメソッド
array.push(6);
console.log(array); // [1, 2, 'a', 'b', 5, 6]
array.pop();
console.log(array); // [1, 2, 'a', 'b', 5]
// forEach, mapなどのメソッドもありますが後述します

グローバルオブジェクト

JavaScriptにはグローバルオブジェクトと呼ばれるオブジェクトがあります。グローバルオブジェクトのプロパティはグローバルスコープの変数として扱われます。逆にグローバルスコープの変数はグローバルオブジェクトのプロパティになります。まとめると、グローバルスコープの変数=グローバルオブジェクトのプロパティです。ブラウザにおいてはwindowでグローバルオブジェクトを参照できます。

var a = 1;
console.log(window.a); // 1

window.b = 2;
console.log(b); // 2

関数

JavaScriptにおいて関数はとても重要です。関数は3つの大きな特徴を持っています。
  • オブジェクトである
  • 変数のスコープを作る
  • メソッド・コンストラクタの役割も持つ

関数宣言文

関数はfunction 関数名() {...}で宣言します。関数名に()を付けて実行することができます。戻り値はreturnで返すことが出来ます。
function func(str1, str2) {
  return str1 + str2;
}

console.log(func('string1', 'string2')); // string1string2

関数式

関数式function 関数名() {...}で関数オブジェクトを定義できます。関数宣言文と書式は似ていますが、関数式は式中に書きます。関数式においては関数名を省略でき、このような関数オブジェクトを無名関数といいます。関数式でも、関数宣言文と同様に関数を定義できます。
var func = function() {
  return 'hoge';
};

console.log(func()); // hoge
前の関数宣言文の例とほぼ同義です。関数宣言文は、実質関数オブジェクトを変数に代入していると考えてもらって構いません。

関数の特徴

ここではJavaScriptの関数の特徴を幾つか例を交えて紹介します。

// 関数内で関数を宣言できます
function func1() {
  function innerFunc1() {
    console.log('innerFunc1 was called.');
  }
  var innerFunc2 = function() {
    console.log('innerFunc2 was called.');
  }
  innerFunc1();
  innerFunc2();
}
func1();


// 関数オブジェクトは即時実行できます
(function() {
  console.log('Anonymous function was executed immediately.');
})();

// 引数として関数を受け取ることが可能です
function func2(callback) {
  callback();
}
func2(function() {
  console.log('func2 was called.');
});

// 戻り値として関数を返すことが可能です
function func3() {
  return function() { 
    console.log('Function of func3 was called.');
  };
}
func3()();

// オブジェクトのプロパティにもなります
var hoge = {
  fugaFunc: function() {
    console.log('Object property!');
  }
};
hoge.fugaFunc();

関数スコープ

JavaScriptは関数スコープです。関数内で宣言された変数は、関数外からアクセスできないローカル変数となります。逆に、関数内からは外の変数にアクセスできます。同名の変数が内外にある場合は内側の変数が優先されます。関数スコープにおいては宣言した場所のみが重要で、関数を実行する場所は全く関係ありません。

var a, b, c;
a = 0;

function scopeA() {
  var b = 2;
  function scopeB(d) { // 引数はローカル変数のように振る舞います
    var c = 3;

    // scopeBからは外側のscopeAやグローバルスコープにアクセスできます
    a = 1;
    console.log('a in scopeB: ' + a); // 1
    console.log('b in scopeB: ' + b); // 2
    console.log('c in scopeB: ' + c); // 3
    console.log('d in scopeB: ' + d); // d
  }
  scopeB(4);

  // scopeAからscopeBにはアクセスできません
  console.log('a in scopeA: ' + a); // 1
  console.log('b in scopeA: ' + b); // 2
  console.log('c in scopeA: ' + c); // undefined
}

scopeA();
// グローバルスコープからscopeAやscopeBにはアクセスできません
console.log('a in global: ' + a); // 1
console.log('b in global: ' + b); // undefined
console.log('c in global: ' + c); // undefined

// JavaScriptでは無名関数の即時実行でローカルスコープを作ります
(function() {
  var a = 'hoge';
})();

// ブロックスコープはありません
var a = 1;
if (a == 1) {
  var b = 2;
}
console.log(b);

クロージャーとしての関数オブジェクト

クロージャーとは関数オブジェクトと関数スコープを組み合わせたものです。JavaScriptでは関数がオブジェクトなので、厳密には全ての関数がクロージャーです。ここでは簡単なカウンターを例に、クロージャーの典型的な使い方を紹介します。まずは下のような簡単なカウンターを考えます。

var count = 0; // countをグローバル変数にしなければいけません
function noClosureCounter() {
  count++;
  console.log(count);
}
noClosureCounter(); // 1
noClosureCounter(); // 2
noClosureCounter(); // 3

ご覧の通りグローバル変数を使ってしまっています。クロージャーを使うと、これをローカルに隠蔽す
ることができます。

function generateCounter() {
  var count = 0; // generateCounterの実行毎に初期化される
  return function() {
    count++; // generateCounter内のcountを参照する
    console.log(count);
  }
}

var counter = generateCounter();
counter(); // 1
counter(); // 2
counter(); // 3

var counter2 = generateCounter(); // 関数の実行毎に新しいスコープが作られます
counter2(); // 1

// 関数の特徴で説明したように、以下のような書き方も可能です
generateCounter()(); // 1

クロージャーは変数をローカルに隠蔽するのに有用で、よく用いられます。上の例でさり気なく紹介しましたが、スコープは関数の実行毎に作られます。この性質は重要ですので覚えておいて下さい。

関数型的な書き方


var array = [1,2,3,4,5];
array.forEach(function(i) {
  console.log(i);
});

var doubledArray = array.map(function(i) {
  return i * 2;
});
console.log(doubledArray); // [2, 4, 6, 8, 10]

JavaScriptにおけるクラスベースオブジェクト指向

クラスベース「的な」書き方

JavaScriptはプロトタイプベースなのでクラス、メソッド、アクセス修飾子の構文は存在しません。
しかし、クラスベース的な書き方もサポートされています。JavaScriptにおいては、関数がコンストラクタ・メソッドの役割を、オブジェクトがインスタンスの役割を果たします。
クラスベースでの役割対応するもの
コンストラクタ関数オブジェクト
インスタンスオブジェクト
メンバオブジェクトのプロパティ

function TestClass(message) {
  // コンストラクタではthisで新規オブジェクトを参照します。
  // 新規オブジェクトにプロパティを追加していくことでインスタンスを作ります。

  // メンバー変数とメンバーメソッドの定義。違いは関数オブジェクトかどうかのみです。
  this.var1 = message;
  this.method1 = function() {
    return this.var1;
  };

  // 新規オブジェクトが暗黙的に返されます
}

var testObj = new TestClass('from testObj');
console.log(testObj.method1()); 

コンストラクタによって生成されたオブジェクトを便宜上インスタンスと呼んでいますが、{...}で定義するオブジェクトと同じと考えて良いです。(厳密には違う場合もありますが、プロトタイプの説明が必要なので触れません)

関数の呼び出しコンテキストとthis

これまでに述べたように、関数オブジェクトはコンストラクタ/関数/メソッドの役割を持ちます。これらは、呼び出し方、すなわち呼び出しコンテキストによって決まります。関数オブジェクト自体に違いはありません。
呼び出しコンテキストにより、thisが指す値が変わります。さらに、thisが指す値を指定することもできます。
下の表は4種類のthisをまとめたものです。
コンテキスト構文this
コンストラクタnew func(){}(新規オブジェクト)
関数func()グローバルスコープ or undefined
メソッドobj.func()obj
call, apply, bind呼び出しfunc.call(obj)
func.apply(obj)
func.bind(obj)()
obj

var a = 'global';
var obj = {a: 'object'}; // インスタンス変数
var func = function(arg1) {
  console.log(this.a);
}

// 関数
func(); // 'global'

// メソッド
obj.method1 = func;
obj.method1(); // 'object'

// コンストラクタ
new func(); // undefined({}.aを参照している)

func.call(obj, 'arg'); // 'object'
func.apply(obj, ['arg']); // 'object'

var boundFunc = func.bind(obj);
boundFunc(); // 'object'

thisのハマりポイント

thisでハマりやすいのはコールバック処理を書く時です。簡単な例を紹介します。

function fetch(callback) { // APIからデータを取得する関数を想定
  var response = {a: 'hoge', b: 'fuga'};
  callback(response);
}

function Class1() {
  this.getData = function() {
    fetch(function(response) { // fetch()の中で関数として実行されるため、thisはグローバルオブジェクトになる
      this.response = response;
    });
  }
}

class1 = new Class1();
class1.getData();
console.log(class1.response); // undefined

このようなケースで外のthisの値を参照するために、selfという変数が慣習的に用いられます。

function fetch(callback) { // APIからデータを取得する関数を想定
  var response = {a: 'hoge', b: 'fuga'};
  callback(response);
}

function Class1() {
  this.getData = function() {
    var self = this;
    fetch(function(response) { 
      self.response = response; // 関数スコープの性質でselfにアクセス
    });
  }
}

class1 = new Class1();
class1.getData();
console.log(class1.response); // [object Object] {...}

ES2015

ES2015とは2015年に標準化されたECMAScript2015のことです。
ES2015の新仕様の大半はプロトタイプベースと関数スコープに馴染めない人のためのシンタックスシュガーです。ここではReact等で不可欠なものに絞って紹介します。

クラス

今までのコンストラクタは下のように書いてきました。

function TestClass(message) {
  // メンバー変数とメンバーメソッドの定義。違いは関数オブジェクトかどうかのみです。
  this.var1 = message;
  this.method1 = function() {
    return this.var1;
  };

  // 新規オブジェクトが暗黙的に返されます
}

var testObj = new TestClass('from testObj');
console.log(testObj.method1());

これがクラス構文によりクラスベースらしくなります。

class TestClass {
  // メンバー変数とメンバーメソッドの定義。違いは関数オブジェクトかどうかのみです。
  constructor(message) {
    this.var1 = message;
  }
  method1() {
    return this.var1;
  }
}

var testObj = new TestClass('from testObj');
console.log(testObj.method1());

更にクラス構文では継承も可能です。

class ParentClass {
  constructor(str) {
    this.parentProp = str;
  }
}

class TestClass extends ParentClass {
  constructor(str) {
    super(str);
  } 
}

var test = new TestClass('test');
console.log(test.parentProp); // 'test'


アローファンクション

関数オブジェクトの定義を簡潔に書くことができます。更に、thisの値には定義された場所のthisが設定されます。

var func = function() {

}.bind(this);

// ES2016
var func = () => {};

thisのハマりポイントで紹介した例も、アローファンクションで直感的に書くことができます。

function fetch(callback) { // APIからデータを取得する関数を想定
  var response = {a: 'hoge', b: 'fuga'};
  callback(response);
}

function Class1() {
  this.getData = function() {
    fetch((response) => {
      this.response = response;
    });
  }
}

class1 = new Class1();
class1.getData();
console.log(class1.response); // [object Object] {...}

アローファンクションには、関数内に式しかない場合に更に簡潔に書く記法があります。

// ES2015
odds  = evens.map(v => v + 1)
pairs = evens.map(v => ({ even: v, odd: v + 1 }))
nums  = evens.map((v, i) => v + i)

// ES2015以前
odds  = evens.map(function (v) { return v + 1; });
pairs = evens.map(function (v) { return { even: v, odd: v + 1 }; });
nums  = evens.map(function (v, i) { return v + i; });

ブロックスコープ変数宣言let、定数宣言const


// 定数
const PI = 3.141593;

// ブロックスコープ変数
for (let i = 0; i < a.length; i++) {
  let x = a[i];
}

{
  let a = 'hoge';
}

// ES2015より前の書き方
(function() {
  var a = 'hoge';
})();

参考文献

0 コメント:

コメントを投稿