Secrets of the JavaScript Ninja 2nd 読書メモ 第5章 達人のための関数:クロージャとスコープ

Secrets of the Javascript Ninja

  • クロージャによる開発の単純化
  • 実行コンテキストでJavaScriptプログラムの実行を追跡する
  • レキシカル環境で変数スコープを追跡する
  • 変数の型を理解する
  • クロージャの動作を調べる

クロージャを理解する

JavaScriptを書く上で、クロージャは欠かせないものである。クロージャを使うと、クロージャを定義した時点において、同一スコープに属するクロージャの外部の変数を操作することができる。

最も基本的なクロージャは以下のようになる。下記サンプルで、outerFunction関数は、outerValueにアクセスできる。

var outerValue = 'ninja';
function outerFunction() {
  console.log(outerValue);
}
outerFunction();

クロージャの働きがわかりやすい例は以下のようになる。

var outerValue = 'samurai';
var later;

function outerFunction() {
  var innerValue = 'ninja';

  function innerFunction() {
    console.log(outerValue);
    console.log(innerValue);
  }

  later = innerFunction;
}

outerFunction();
later();

outerFunctionの呼び出しによって値がセットされたinnerValueは、呼び出しの終了によってメモリ上から消えてしまい、最後のlater()によるinnerFunctionの呼び出しの時点ではundefinedになっているようにも思える。
しかし、実際にlater()を実行すると、innerValueには’ninja’が入っている。

実は、innerFunctionを定義するとき、関数宣言を定義するだけでなく、関数定義の時点のスコープ内にある全ての変数を包含するクロージャが作成されているのだ。ここで重要なのは、クロージャ経由で外部の変数にアクセスする場合、その変数はクロージャが破棄されるまでメモリ上に残り続けるという点である。

クロージャを動かす

プライベート変数(のようなもの)

JavaScriptにはプライベート変数はないが、クロージャを使うことで似たようなものを作り出すことができる。

function Ninja() {
  var feints = 0;
  this.getFeints() = function() {
    return feints;
  };
  this.feint = function() {
    feints++;
  }
}

var ninja1 = new Ninja();
ninja1.feint();
console.log(ninja1.feints); // undefined
console.log(ninja1.getFeints()); // 1

var ninja2 = new Ninja();
console.log(ninja2.getFeints()); // 0

クロージャをコールバックとして使う

setInterval(function() {
  // doSomething
}, 100);

実行コンテキストによるコード実行の追跡

JavaScriptにおいて、実行の基本的な単位は関数である。JavaScriptのコードは、関数が関数を呼び、そして呼び出し元に戻っていくような流れで実行される。このとき、JavaScriptエンジンは関数呼び出しのスタックを記憶している。

コードがJavaScriptエンジンによって実行されるとき、それぞれの文は一定の実行コンテキストによって実行される。実行コンテキストには、グローバルコンテキストと関数実行コンテキストの2種類がある。

  • グローバルコンテキストは1つだけ存在する
  • 関数実行コンテキストは関数呼び出しのたびに作成される

関数呼び出しのたびに実行コンテキストが作成されるが、その関数の処理が終わると元の場所に復帰する必要がある。そのため、関数内でさらに関数が呼ばれるような場合には、関数の実行コンテキストがスタックとしてメモリ上に保持される。

function skulk(ninja) {
  report(ninja + " skulking");
}
function report(message) {
  console.log(message);
}
skulk("Kuma"); // グローバルコンテキストからの関数呼び出し

// 1. グローバルコンテキストが作成される
// 2. skulk関数の実行コンテキストがスタックに積まれる
// 3. report関数の実行コンテキストがスタックに積まれる
// 4. report関数の実行コンテキストが破棄され、skulk関数に復帰する
// 5. skulk関数の実行コンテキストが破棄され、グローバルコンテキストに復帰する

関数の実行スタック(Call Stack)は、Chrome DevTools等を活用することで簡単に確認できる。

レキシカル環境(lexical environments)で識別子を追いかける

レキシカル環境(lexical environments)とは、JavaScriptエンジンの内部の構成要素で、識別子と変数のマッピングを追いかけるのに使われる。

var ninja = "Hattori";
console.log(ninja);

上記コードで、レキシカル環境は、console.log文の際にninja変数について尋ねられる。レキシカル環境は、一般にはスコープと呼ばれる。

通常、レキシカル環境はJavaScriptコードの特定の構造と結びついている。すなわち、関数、コードのブロック、try-catch文のcatch部分である。これらの構造は独自の識別子マッピングを持つことができる。

コードのネスト

レキシカル環境は、多くの場合、コードのネストに基づいている。

const ninja = 'Muneyoshi';

// skulk関数はグローバル
function skulk() {
  const action = 'skulking';

  // report関数はskulk関数の内側
  function report() {
    const reportNum = 3;

    // forループはreport関数の内側
    for (let i = 0; i < reportNum; i++) {
      console.log(ninja + ' ' + action + ' ' + i);
    }
  }
}

skulk関数を呼び出すたびに、新しいレキシカル環境が作成される。

重要なのは、内側のコードは外側でアクセスされた変数にアクセス可能なことである。

コードのネストとレキシカル環境

ローカル変数、関数宣言、関数パラメーターの追跡に加えて、それぞれのレキシカル環境は外部のレキシカル環境も追跡しなければならない。JavaScriptでは、関数をファーストクラスオブジェクトにすることで、外部の環境の追跡を可能にしている。

関数が作成されると、関数が作成されたレキシカル環境への参照が、 [[Environment]] という内部プロパティとして保持される。二重の角カッコは、JavaScriptエンジンが内部で使用するプロパティの印であり、プログラムから直接触ることはできない。

関数が実行されるたびに、新しい関数の実行コンテキストが作成され、スタックに積まれる。加えて、新しく関連するレキシカル環境が作成される。新しく作成されるレキシカル環境の外部環境のために、JavaScriptエンジンは関数の内部の [[Environment]] プロパティから環境を取り出す。これによって、新しく実行される関数のための環境が用意される。

JavaScript変数の種類を理解する

JavaScriptでは、3種類のキーワードを使って変数を定義できる。var, let, constである。これらは (1) 変更可能性(mutability) (2) レキシカル環境との関係 という2点で異なっている。

変数の変更可能性(mutability)

変数宣言のキーワードを変更可能性で分類するなら、constは変更不可、varletは変更可、となる。constで宣言された変数は変更不可(immutable)となり、値の再代入ができない。一方、varまたはletによって宣言された変数は何度でも値を再代入することができる。

const変数

const 変数には、宣言と同時に初期値を設定する必要がある。const 変数は以下のような目的で使用される。

  • 再代入すべきでない変数を明示する
  • 固定された値を定義する

const 変数には再代入ができないため、意図しない変更から保護されている。また、JavaScriptエンジンによる最適化も可能になる。再代入の不要な変数(多くは不要なはず)に対しては、constを使って宣言することが推奨される。

なお、constで宣言した変数は再代入ができないだけで、オブジェクトのプロパティを変更したり、配列の要素を追加・削除したりすることは可能である。オブジェクトや配列の変更を禁止したい場合はObject.freezeメソッドを使用する。

// strictモードでないと、freezeしたオブジェクトへの代入はエラーにはならず、無視される
'use strict';

const job = 'samurai';

try {
  job = 'ninja';
  console.log(job);    
} catch (e) {
  console.log('error');
}
// => error

const ninja = {};

try {
  ninja.weapon = 'katana';
  console.log(ninja.weapon);
} catch (e) {
  console.log('error');
}
// => katana

Object.freeze(ninja);

try {
  ninja.weapon = 'kunai';
  console.log(ninja.weapon);    
} catch (e) {
  console.log('error');
}
// => error

const weapons = [];
weapons.push('kunai');

// 配列のfreezeにもObject.freezeを使う
Object.freeze(weapons);

try {
  weapons.push('shuriken');
  console.log(weapons);    
} catch (e) {
  console.log('error');
}
// => error

変数定義キーワードとレキシカル環境

レキシカル環境との関係(スコープ)の観点で分類するなら、varlet, constの間で線が引かれる。

varキーワードの使用

varキーワードを使用するとき、変数は最寄りの関数かグローバルなレキシカル環境に保存される(ブロックのレキシカル環境は無視されることに注意!)。

for (var i = 0; i < 3; i++) {
  console.log(i);
}
console.log(i); // iにはforループの外でもアクセスできる

let, constを使ってブロックスコープ変数を定義する

let, const を使って定義した変数は最寄りのレキシカル環境に保存される。もし最寄りがブロックのレキシカル環境であれば、そこに保存される。

for (let i = 0; i < 3; i++) {
  const message = 'messge:' + i;
  console.log(message);
}
console.log(typeof i, typeof message);
// => undefined, undefined

再代入可能な変数には let を使うことで、よりスコープの狭い変数を定義することができる。後方互換性以外の理由でvarを使う理由はない。

レキシカル環境に識別子を登録する

識別子の登録過程

JavaScriptコードの実行は2段階に分けて行われる。

最初のフェーズは新しいレキシカル環境が作成されるたびに実行される。このフェーズでは、コードは実行されず、JavaScriptエンジンは変数と関数の宣言を探して現在のレキシカル環境に登録する。2番目のフェーズでは、コードが実行される。このフェーズは以下のように詳細化できる。

  1. 関数環境を作成する場合、arguments識別子を作成する
  2. 関数またはグローバル環境を作成する場合、コードを走査して関数宣言を探し、識別子を登録する
  3. 変数宣言のための走査が行われ、varで宣言された変数は最寄りの関数の、letまたはconstで宣言された変数は最寄りのブロックのレキシカル環境に登録される

関数宣言より前に関数を実行する

  • 関数宣言を使用して定義された関数には宣言より前でもアクセスできる
  • 関数式やアロー関数はできない

関数の上書き

関数宣言を使用すると、その関数を参照する変数はコードの実行よりも先に定義される。

console.log(typeof fun); // function
var fun = 3;
console.log(typeof fun); // number
function fun(){}
console.log(typeof fun); // number

まとめ

  • クロージャによって、その関数が定義されたスコープ内にある全ての変数にアクセス可能になる
  • クロージャは疑似プライベート変数やコールバック関数に利用できる
  • JavaScriptエンジンは実行スタックによって関数の実行を追跡する
  • JaaScriptエンジンはレキシカル環境(スコープ)によって識別子を追跡する
  • JavaScriptでは、グローバル・関数・ブロックのいずれかのスコープの変数が定義できる
  • 変数の定義には var, let, const キーワードが使用できる
  • クロージャはJavaScriptのスコープのルールの副作用である

『ポートとソケットがわかればインターネットがわかる』を読んだ

ポートとソケットがわかればインターネットがわかる――TCP/IP・ネットワーク技術を学びたいあなたのために (Software Design plus)

Geekなページ のあきみち氏による、TCP/IPの入門書。情報処理技術者試験対策本のような、無味乾燥な知識をただ覚えるための本ではなく、ネットワークの初歩的な知識と、それを実際に何かに使うことの橋渡しをしようとする意図が感じられる。

具体的には、この手の入門書には珍しく、本書にはソケットを利用したプログラムのサンプルが載っている(※)。その他、ネットワークコマンドの使用例も記載されていて、手を動かしながら学ぶことができる。

挿絵も的確で、本文だけではわかりづらいところでも理解の助けになっている。

ネットワークを初めて学ぶ人だけでなく、自分のように、ネットワークに苦手意識のあるソフトウェアエンジニアが読んでも得るものは多いと思う。

※ただし、C言語で書かれているので、厳しいという人もいそう。Working With TCP SocketsというRubyを使ってソケットプログラミングを行う入門書もあるけど、こっちはこっちで英語なのがつらいという人が多いかも。

Secrets of the JavaScript Ninja 2nd 読書メモ 第4章 関数呼び出しを理解する

Secrets of the Javascript Ninja

暗黙的な関数パラメータの使用

関数には、暗黙的に渡されるパラメータが2つある。argumentsthisである。

argumentsパラメータ

argumentsパラメータは関数への引数の集合である。argumentsを使えば明示的に定義されていないパラメータも受け取ることができる。これを利用すると、関数オーバーロードを実現できる。

ES2015のrestパラメータを使用すれば、argumentsが必要になる場面は減少するが、古いコードに触るときのためにもargumentsの挙動は理解しておいたほうがよい。
argumentsにはlengthプロパティがあり、引数の数を調べることができる。また、argumentsからは配列アクセスの形式で値を取り出せる。

function whatever() {
  for (let i = 0, len = arguments.length; i < len; i += 1) {
    console.log(arguments[i]);
  }
}

whatever();
whatever(1);
whatever(1, 2);

argumentsは配列のようだが、配列ではない。そのため、sort等の配列操作用メソッドを使うことはできない。配列のように扱いたい場合は、ES2015のArray.fromメソッド等を使用して配列に変換する必要がある。

function sum() {
  return Array.from(arguments).reduce((p, c) => p + c, 0);
}

console.log(sum()); // 0
console.log(sum(1)); // 1
console.log(sum(2, 3)); // 5

// なお、restパラメータは配列なので変換不要
function sum (...args) {
  return args.reduce((p, c) => p + c, 0);
}

thisパラメータ

thisは関数呼び出しのコンテキストを保持するパラメータである。JavaやC#のようなオブジェクト指向言語とは異なり、JavaScriptのthisの値はどのように呼び出されたかによって異なる。JavaScriptのオブジェクト指向の理解にあたっては、thisは最も重要な部分のひとつである。

関数呼び出し

関数呼び出しにはいくつかのパターンがある。

  • 通常の関数: skulk()
  • メソッド: ninja.skulk()
  • コンストラクタ: new Ninja()
  • applyまたはcallによる呼び出し: skulk.call(ninja) または skulk.apply(ninja)

関数としての呼び出し

関数としての呼び出しでは、thisの指す値はstrictモードか否かによって異なる。

function ninja() {
  return this; // globalオブジェクト(ブラウザならwindow)
}

function samurai() {
  "use strict";
  return this; // undefined
}

メソッドとしての呼び出し

関数がオブジェクトのプロパティとして割り当てられている場合、この関数をオブジェクトのメソッドと呼ぶ。この場合、thisパラメータが指すのは、メソッド呼び出しの対象となったオブジェクトである。

var ninja = {};
ninja.getThis = function(){ return this; };
console.log(ninja.getThis() === ninja); // true

コンストラクタとしての呼び出し

コンストラクタとは、オブジェクトを組み立てる関数である。コンストラクタを使うことで、同様の機能を持つオブジェクトを簡単に作ることができる。

function Ninja() {
  this.skulk = function() {
    return this;
  }
}

var ninja1 = new Ninja();
var ninja2 = new Ninja();

console.log(ninja1.skulk() === ninja1);
console.log(ninja2.skulk() === ninja2);

関数がnew演算子とともに呼び出されると(コンストラクタとしての呼び出し)、空のオブジェクトがthisに割り当てられる。このオブジェクトはnew演算子の結果として返る。

applyとcallによる呼び出し

イベントハンドラに紐付けた関数において、thisはイベントが発生したオブジェクトを指す。このように、関数のコンテキストを呼び出し側から操作する方法がapplycallである。

関数のapplyメソッドを使って呼び出しを行う場合、第1引数に関数コンテキストとして用いるオブジェクト、第2引数に呼び出し時の引数の配列を渡す。callメソッドも同様だが、引数は配列ではなくパラメータリストに直接渡す。

function juggle() {
    let result = 0;
    for (let n = 0; n < arguments.length; n += 1) {
        result += arguments[n];
    }
    this.result = result;
}

const ninja1 = {};
const ninja2 = {};

juggle.apply(ninja1, [1, 2, 3, 4]);
juggle.call(ninja2, 5, 6, 7, 8);

console.log(ninja1.result); // 10
console.log(ninja2.result); // 26

関数コンテキストの問題を修正する

関数コンテキストを操作する方法には、call/applyの他にアロー関数式とbindメソッドがある。

アロー関数式による関数コンテキストの回避

アロー関数式は、みずからのthisをもたない。代わりに、その関数が宣言された時点のthisパラメータの値を保持している。
イベントハンドラとして関数を登録しても、thisの値は変わらない。

bindメソッドの使用

関数のbindメソッドを使用すると、その関数が実行されるコンテキストを呼び出し方法に関わらず変更できる。bindメソッドは、別の関数コンテキストを持った別の関数を返す関数である。

Secrets of the JavaScript Ninja 2nd 読書メモ 第3章 一級市民としての関数:定義と引数

Secrets of the Javascript Ninja

平凡なJavaScriptプログラマーとJavaScriptニンジャを分かつものは、関数型言語としてのJavaScriptを理解しているか否かである。

最も重要な点は、JavaScriptにおいて関数は一級オブジェクト(あるいは、一級市民)であるということである。関数は、その他のJavaScriptと同じように扱うことができる。変数に入れたり、リテラルで定義したり、関数の引数にしたりできる。

機能面の違い

関数がファーストクラスオブジェクトである

関数をファーストクラスオブジェクトとして扱うことは、関数型プログラミングのはじめの一歩でもある

メモ:本書の次は JavaScript関数型プログラミング を読もうと思います

コールバック関数

コールバック関数とは、関数を利用できるよう引数などの形で渡すこと。イベントハンドラなど、様々な場面で使用するテクニック。

関数をオブジェクトとして扱う

関数を通常のオブジェクトと同様に扱うことで、様々なことを実現できる。

関数を保持する

関数をコレクションの中に保持して、あとでまとめて呼び出すことができる。
単純な実装としては配列の中に保管することになるが、以下のように、関数にプロパティを設定することで重複登録を奉仕できる。
(ES2015のSetを使うとより良い実装ができる)

const store = {
  nextId: 1,
  cache: {},
  add: (fn) => {
    if (!fn.id) {
      fn.id = this.nextId++;
      this.cache[fn.id] = fn;
      return true;
    }
  }
};

function ninja() {}

assert(store.add(ninja), 'Function was safely added');
assert(!store.add(ninja), 'But it was only added once');

function assert(value, text) {
  if (text) console.log(text);
  return !!value;
}

自己メモ化関数

メモ化とは、前回呼び出し時に計算した値を記憶しておくことである。

function isPrime(value) {
  if (!isPrime.answers) {
    isPrime.answers = {};
  }
  if (isPrime.answers[value] !== undefined) {
    return isPrime.answers[value];
  }
  let prime = value !== 1; // 1 is not a prime

  for (let i = 2; i < value; i++) {
    if (value % i === 0) {
      prime = false;
      break;
    }
  }

  return isPrime.answers[value] = prime;
}

console.log(isPrime(5));
console.log(isPrime.answers[5]);

関数定義

JavaScriptにおける関数定義には以下の4つの方法がある。

// 関数宣言と関数式
function myFunc() { return 1; }

// アロー関数(ES2015)
myArg => myArg*2

// 関数コンストラクタ(ほとんど使わない)
new Function('a', 'b', 'return a + b')

// ジェネレーター関数(ES2015)
function* myGen() { yield 1; }

関数宣言と関数式

最も一般的な関数定義の方法は、関数宣言と関数式である。この2つはよく似ているが、区別すべきである。

関数宣言

関数宣言では、関数名が必須。

function myFunctionName(myFirstArg, mySecondArg) {
  myStatement1;
  myStatement2;
}

関数式

JavaScriptの関数は、他のオブジェクトと同様、リテラルで定義できる。
文の中に関数のリテラルが含まれる場合、関数式と呼ばれる。
関数式を使うと、通常の変数と同様、定義した場所以降で関数が利用可能になる。
これに対して、関数宣言は、スコープの冒頭で定義したものとみなされる(巻き上げ)。

// 関数式(基本形)
var myFunc = function() {};

// 関数式は引数にも書ける
myFunc(function() {
  return function() {};
});

// 名前付き関数式
var f = function namedFunction() {};
f(); // OK
namedFunction(); // 未定義関数の呼び出しエラー
console.log(f.name); // 'namedFunction'

即時関数

関数を定義してすぐに呼び出すテクニックを即時呼び出し関数式(Immediately Invoked Function Expression, IIFE)、または即時関数(Immediate Function)という。
即時関数では、通常関数名を書くところに関数式を書いて、それを括弧で囲む。

// 通常の関数呼び出し
myFunctionName (3);

// 即時関数
(function(){}) (3);

// 括弧で囲まない場合、
// 名前のない関数宣言とみなされて構文エラーになる
function(){} (3);

// この書き方でもOK(全体を括弧で囲むことで、関数宣言ではなく関数式とみなされるようにする)
(function(){} (3));

// こういう書き方もある
+function(){}();
-function(){}();
!function(){}();
~function(){}();

アロー関数式

アロー関数式はES2015で追加された機能である。
JavaScriptでは関数式を多用するので、短く書く方法が導入された。
アロー関数式という呼び名は、 => 演算子(アロー演算子)に由来している。

const values = [0, 3, 2, 5, 7, 4, 8, 1];

// 通常の関数式
values.sort(function(value1, value2) {
  return value1 - value2;
});

// アロー関数式(functionキーワードを省略)
values.sort((value1, value2) => {
  return value1 - value2;
});

// 関数本体が1つの式だけの場合は、そのまま式を書ける(式の値はreturnされる)
values.sort((value1, value2) => value1 - value2);

// 引数が1つだけの場合は引数の括弧も省略できる
const greet = name => `Greetings ${name}`;

// ただし、引数が1つもない場合には括弧は省略できない
const helloWorld = () => 'Hello, world!';

引数と関数のパラメータ

  • パラメータ(parameter、仮引数)とは、関数定義の一部として定義する変数である
  • 引数(argument、実引数)とは、関数呼び出し時に渡す値である

パラメータとは異なる数の引数を渡した場合、JavaScriptではエラーは発生しない。過剰なパラメータには割り当てられず、値が渡されなかったパラメータの値はundefinedになる。

Rest parameters(余り物パラメータ)

パラメータの定義で、n番目のパラメータの名前の前に ... をつけると、このパラメータはRest parameters(余り物パラメータ)となる(※ES2015で導入された機能)。
このパラメータは、n番目以降の全ての引数を格納する配列になる。

function multiMax(first, ...remainingNumbers) {
  const sorted = remainingNumbers.sort((a, b) => b - a);
  return first * sorted[0];
}

console.log(multiMax(3, 1, 2, 3)); // 9

デフォルトパラメータ

// ES5までのデフォルトパラメータ
function performAction(ninja, action) {
  action = typeof action === 'undefined' ? 'skulking' : action;
  return ninja + ' ' + action;
}

// ES2015のデフォルトパラメータ
function performAction(ninja, action = 'skulking') {
  return ninja + ' ' + action;
}

// デフォルトパラメータには任意の式を書ける(可読性が下がるので避けたほうがよい)
function performAction(ninja, action = 'skulking', message = ninja + ' ' + action) {
  return message;
}

Secrets of the JavaScript Ninja 2nd 読書メモ 第2章 実行時にページを組み立てる

Secrets of the Javascript Ninja

クライアントサイドWebアプリケーションのライフサイクル

  1. ユーザがブラウザのアドレスバーにURLを打ち込む
  2. ブラウザがリクエストを生成してサーバに送信する
  3. サーバがクライアントにレスポンスを返す
  4. ブラウザがHTML/CSS/JavaScriptを処理して最終的なページを組み立てる
  5. ブラウザがイベントキューを監視してイベントに応答できるようにする
  6. ユーザがページの要素を操作する
  7. ユーザがWebアプリケーションを閉じる

ページの組み立てのフェーズ

  1. HTMLを構文解析して、DOMを組み立てる
  2. JavaScriptコードを実行する

※JavaScriptエンジンは、script要素を見つけると、そこでDOMの組み立てを一時停止してJavaScriptを実行する。そのため、script要素がHTMLの途中にあり、読み込みや実行に時間がかかると、ブラウザの描画が停止してパフォーマンスが悪化して感じられる。script要素はbody要素の末尾に置くべきである。

イベントの取り扱い

クライアントサイドのWebアプリケーションはGUIアプリケーションなので、ユーザーの様々な操作(イベント)に反応する必要がある。

イベントハンドリングの概要

ブラウザの実行環境はシングルスレッドの実行モデルである。イベントは単一のキューに入れられ、上から順に1つずつ処理される。

イベントハンドラを登録する

イベントハンドラの登録には以下のいずれかの方法が使用できる。

// 特殊なプロパティに関数を代入する
window.onload = function(){};

// addEventListener メソッドの使用
document.body.onclick = function(){};

プロパティへの代入は、一度に一つのイベントハンドラしか登録できない。複数登録したい場合はaddEventListenerを使う必要がある。