kidoOooOoooOOom

ゲーム開発やってます

Effective JavaSceipt 読書メモ 項目12~17

第2章 変数のスコープ

項目12 変数の巻き上げ(ホイスティング)を理解する

JSではブロックスコープをサポートしない。変数定義は、それを最も近く囲んでいるステートメントやブロックではなく、それを含む関数をスコープとする。
JSの変数宣言の振る舞いは、宣言と代入の2つの部分で構成されていると考えれば理解しやすい。宣言の部分を暗黙のうちに、それを囲む関数の冒頭に「巻き上げる」が、代入の部分は、その場所に残す。
下記のようなコードを書いた場合、

function f() {
  // ...
  {
    var x = ...
  }
}

巻き上げにより、振る舞いとしては下記のコードのようになる。

function f() {
  var x;
  // ...
  {
    x = ...
  }
}

この紛らわしさを防ぐために、var宣言を関数の先頭で宣言することによる手作業巻き上げを行うのも有り。自分はその方が分かり易くて良いと思う。
JSで、例外的にブロックをスコープとするとのはexceptionである。try...catchで補足される例外は、そのcatchブロックだけをスコープとする変数になる。

function test() {
  var x = "aaa";
  try {
    // ...
  } catch (x) {
    x = "error"; // ここのx は 先頭で宣言されたxとは別スコープ
  }
}

項目13 ローカルスコープを作るには即時関数式(IIFE)を使おう

次に示すコードは、プログラマーの意図に反してundefinedになるであろう。

function hogehoge(a) {
  var result =[];
  for (var i = 0, n = a.length; i < n; i++) {
    result[i] = function() { return a[i]; };
  }
  return result;
}
var foo = hogehoge([10,20,30]);
var f = foo[0];
console.log(f()); // undefined

変数iがクロージャによって最後の数値しか参照しないため、iがa.lengthを超えた値になった時のものを参照してしまっている。
これを解決する方法としては、即時関数式(immediately invoked function expression: IIFE) を用いる方法がある。

function hogehoge(a) {
  var result =[];
  for (var i = 0, n = a.length; i < n; i++) {
    (function(j) {
      result[i] = function() { return a[j]; };
    })(i);
  }
  return result;
}
var foo = hogehoge([10,20,30]);
var f = foo[0];
console.log(f()); // 10

これにより、変数jが即時関数式内でforループ毎の変数iを保持するため、期待した結果になる。

項目14 名前付き関数式のスコープは可搬性がないので注意しよう

この項目は、正しく実装されたES5環境を前提にするかしないかで注意すべき観点が異なる。最近はES5環境前提になってきているので、リーダーブルコード優先する方針でメモる。

名前付き関数式は下記のように書ける。

var x = function double(x) { return x * 2; }

一方、名前無しの関数式は下記のように書ける。

var x = function(x) { return x * 2; }

この2つの関数式の違いとしては、デバッグ環境で現れる。
Errorオブジェクトのスタックトレースにこの関数の名前が現れてくるため、情報が欲しい場合はできるだけ名前をつけた方がデバッグもしやすい。また、ソースコードでも名前が付いていた方が理解を助けると思うため、ES5環境前提においては名前付き関数を使った方がよさそうである。

項目15 ブロックローカルな関数宣言のスコープも可搬性がないので注意しよう

ES5になるまで、下記のようなブロックローカルな関数宣言を書いた場合の挙動について正式な見解が無かった。

function test(x) {
  if (x) {
    function f() { return "local" };
  }
  return null;
}

ES5からは、上記なようなコードをstrictモードでエラーとして報告するようになった。
今のところ、ネストした関数宣言を書きたい場合はvar宣言と関数式を使って行うことならば可能である。

function test2(x) {
  if (x) {
    var f = function f() { return "local" };
  }
  return f;
}

console.log(test2(true)()); // local

項目16 eval でローカル変数を作らない

evalは非常に強力で柔軟性の高いツールであるがゆえ、その利用には注意しなければならない。
evalで最も単純な間違いのひとつが、スコープとの干渉である。evalで変数を作ることにより、呼び出し側のスコープが汚染されてしまうケースが起こりうる。

// use strict;
var evaltest = "i am evil";
function evalTest(x) {
  if (x) {
    eval("var evaltest = 'i am justice';");
  }
  return evaltest;
}
console.log(evalTest(true)); // i am justice 
console.log(evalTest(false)); // i am evil

このコードでは、evaltest変数がevalによって動的に汚染されてしまう。
ただし、ES5のstrictモードを付くと、evalはネストしたスコープ内で実行されるため変数が汚染されない。

use strict;

var evaltest = "i am evil";
function evalTest(x) {
  if (x) {
    eval("var evaltest = 'i am justice';");
  }
  return evaltest;
}
console.log(evalTest(true)); // i am evil
console.log(evalTest(false)); // i am evil

項目17 直接eval より、間接evalが好ましい

直接evalと間接evalの2通りの使い方が用意されている。
直接evalは、evalを呼び出した場所の完全なスコープをアクセスすることができる。この威力があまりにも強大であるため、直接evalを含むコードはパフォーマンス上に悪影響がある。
これに対し、間接evalは下記のような書き方で記述する方法である。

(0, eval)(src);

間接evalの場合、引数はグローバルスコープとして評価される。これにより、コンパイラの最適化が行いやすくなるためパフォーマンス上の影響が直接evalに比べて小さい。また、スコープも限定されるため安全性も高まる。
というわけで可能な限り直接evalではなく間接evalを使うべきである。

mongodb設定周りメモ

パラメータの設定方法について。
mongod( or mongos)起動時に、コマンドライン引数か config ファイルで設定する。両方に同じパラメータの設定があった場合、configファイルが優先される。

$ mongod --plogappend true --verbose
  • configファイルの場合

まず、/etc/mongod.cnf ファイルを用意しておき、

$ mongod --config /etc/mongod.cnf

で読み込み。


今回メモったパラメータは下記の通り。

パラメータ名 説明 デフォルト値
dbpath DBファイルを格納するディレクトリのパス。 /data/db
port 待ち受けポート番号 27017
logpath ログ出力先 標準出力
logappend ログに追記するかどうか flase
directoryperdb データベースごとにデータファイルを作るかどうか false
pidfilepath pidファイルのパス。指定しないとpidファイルが作られない none
rest restインターフェースを有効にするかどうか。http://localhost:28017/{db_name}/{collection_name}/ で GETするとJSONが返ってくる false
journal trueだとジャーナルを確実に永続化し、一貫性を保つ。一貫性を保証しなくてもよい場合は、nojournal=trueとしてジャーナルを無効にする。パフォーマンスとのトレードオフで選択する。 (64bit)true, (32bit)false
configdb mongos用パラメータ。使用するconfig dbのホスト名を書く

Effective JavaSceipt 読書メモ 項目8~11

第2章 変数のスコープ

項目8 グローバルオブジェクトを使うのは、最小限にとどめる

JSではグローバル名前空間に変数を作る事はあまりにも簡単。変数定義時に何も宣言する必要がない。
経験を積んだプログラマほど、グローバル空間の汚染はバグを産み、コードを追いにくくすることは分かっている。
できるだけ変数はそれを必要とするコードだけがアクセスできるローカル変数にすべきである。

項目9 ローカル変数は、かならず宣言しよう

グローバル変数よりも厄介なものは、偶発的なグローバル変数である。
var をつけずに宣言してしまった変数は、容赦なくグローバル変数になってしまう。
これを避けるためにも、変数をチェックできる lintツールの導入は必須。

項目10 withは避けよう

(withは使ったことないけれど)悪質な機能とのこと。
withはとにかく使うなという結論である。

項目11 クロージャと仲良くしよう

クロージャを理解するには、3つの基本的な事実を学ぶだけでよい。

第1の事実:JSでは現在の関数の外側で定義された変数を参照できる。

function makeSandwich() {
  var hoge = "hoge";
  function make(filling) {
    return hoge + " and " + filling; // 関数の外側で定義された hoge 変数を参照できる
  }
  return make("foo");
}
makeSandwich();

第2の事実: 関数は、その外側の関数がリターンした後になっても、まだ外側の関数内で定義された変数を参照できる。

function makeSandwich() {
  var hoge = "hoge";
  function make(filling) {
    return hoge + " and " + filling; // 関数の外側で定義された hoge 変数を参照できる
  }
  return make;
}
var f = makeSandwich();
f('test'); // hoge and test
f('banana'); // hoge and banana

このカラクリとしては、JSの関数値は呼び出されたときに必要となる情報を含むが、それは実行すべきコードだけではない。その関数を囲むスコープの中で定義されている変数を参照する可能性があれば、それらの変数も内部的に保存する。このように、自分を囲むスコープにある変数を覚えている関数を、クロージャと呼ぶ。

第3の事実: クロージャは外側の変数を更新できる。クロージャは、外側の変数へのリファレンスを保存するのであって、値をコピーするわけではない。したがって、変数の更新は、それをアクセスするすべてのクロージャに反映される

function box() {
  var val = undefined;
  return {
    set: function(newVal) { val = newVal; },
    get: function() { return val; },
    type:  function() { return typeof val; }
  }
}

var b = box();
console.log(b.type()); // undefined
b.set(98.6);
console.log(b.get()); // 98.6
console.log(b.type()); // number


Effective JavaSceipt 読書メモ 項目4~7

第1章 JavaScriptに慣れ親しむ

項目4 オブジェクトラッパーよりもプリミティブが好ましい

例えばstringのプリミティブ型の比較では同値として比較できるが、

console.log("hello" === "hello"); // true

Stringオブジェクトにすると、別のオブジェクトとして扱われるため比較演算子が期待通りの動きにならない。

var s1 = new String("hello");
var s2 = new String("hello");
console.log(s1 === s2); // false
console.log(s1 == s2); // false

プリミティブ値に対してStringオブジェクトのプロパティやメソッドを呼び出すと、JSが「暗黙の強制」を行い、期待通りの結果が得られる。

console.log("hello".toUpperCase()); // HELLO

項目5 型が異なるときに==を使わない

下記のような式の結果は、暗黙の強制により同値として扱われてしまう

console.log("1.0e0" == { valueOf: function() { return true; } }); // true

== 演算子は、引数の型が異なるとき、データ表現の違いをなんとかごまかして包み隠そうとする。
=== は厳密な同値演算子であり、比較に暗黙の強制が関わらないないことをハッキリ示すことができる。
よって、暗黙の強制があまりにも明示的な時以外では、===を使うべきである。

項目6 セミコロン挿入の限度を学ぼう

結論としてはセミコロン自動挿入を期待してはいけないということ(JShintでも警告でる)。
まず、セミコロン挿入の第1ルールは下記の通り。

セミコロンが挿入されるのは、"}"トークンの前か、1個以上の改行の後か、プログラム入力の末尾だけである
よって次の例は違法となる。

function area(r) { r = +r return Math.PI * r * r } // error

セミコロン挿入の第2ルールは下記の通り。

セミコロンが挿入されるのは、次の入力トークンを字句解析できないときだけである

これは処理の順番によって挿入されるケースとされないケースが分かりにくいため注意が必要。
例えば下記の2つのケースでは、処理は同じ3つの文だが、セミコロン挿入によって異なる挙動になる。

a = b // セミコロンが推論される
var x // セミコロンが推論される
(f()) // セミコロンが推論される
var x // セミコロンが推論される
a = b // セミコロンが推論されない!!
(f()) // セミコロンが推論される

3つ目のルールは、

セミコロンが、forループ頭部の区切りとして、あるいは空文として、挿入されることは決して無い

つまり、下記はエラーになる

for ( var i = 0, total = 1 // parse error
        i < n
        i+) {
      }

項目7 文字列は16ビットの符号単位を並べたシーケンスとして考えよう

JavaScript文字列の要素は16ビットの符号単位である 

ということが重要。
例えば、UTF-16においては、𝄞 (ト音記号)は 0xd834 と 0xddle の1対の符号単位で表現される。
これに対し、Javascriptでlengthを調査すると、

console.log("𝄞".length); // 2 

2を表示してしまう。

このことを意識することは非常に労力がいるため、符号位置を意識する文字列操作を書くには、できるだけサードパーティーのライブラリを使ったほうが良い。

Node.jsでエクセルparseして出力など

本来はやりたくないことだが、大量のエクセルファイルで用意されてしまったデータから必要なものだけ抜き出して整形して出力する必要性があったので対応した。
使用したのは xlsx モジュール

例えば、下記のようなテンプレートのエクセルファイルがあるとする。
f:id:gidooom:20150213162534p:plain

このエクセルファイルを開いて、指定の要素を取り出すコードは下記の通り。

var fs = require("fs");
var XLSX = require('xlsx');

var PROP_MAP = {
  'C2': 'master_id',
  'C4': 'name',
  'C5': 'description',
  'C6': 'age',
  'C7': 'height',
  'C8': 'weight',
};

var workbook = XLSX.readFile('hogehoge.xlsx');

workbook.SheetNames.forEach(function(y) {
  var worksheet = workbook.Sheets[y];
  for (var z in worksheet) {
    if(z[0] === '!') continue;
    console.log(z + "=" + JSON.stringify(worksheet[z].v));
  }
});

出力結果は下記の通り。

B2="master_id"
C2=1000
B4="name"
C4="hogehoge man"
B5="description"
C5="saying hogehoge"
B6="age"
C6=35
B7="height"
C7="180cm"
B8="weight"
C8="75kg"

後は取得できたデータをごにょごにょ整形して出力してあげればおk。

Node.jsでBasic認証

basic-auth-connect モジュールを使うと簡単にできる。
そのまま user名とpasswordを渡すと、認証エラーの場合は 401を返してくれる。

var basicAuth = require('basic-auth-connect');
app.use(basicAuth(username, password));

ちょっと特殊な処理をかましたい場合は、callback関数渡しでもok.

connect()
.use(basicAuth(function(user, pass){
  return 'tj' == user && 'wahoo' == pass;
}))

デフォルトのエラーハンドリング処理を変えたい場合は、エラーハンドリング用の関数を渡すことも可能。

connect()
.use(basicAuth(function(user, pass, fn){
  User.authenticate({ user: user, pass: pass }, fn);
}))

serverspec v2で NICのリンク速度テスト

serverspecでNICのリンク速度設定のテストを行う場合は下記のように書く。

describe interface('eth0') do
  its(:speed) { should eq 1000 }
end

Ubuntu 12.04 のサーバで実行してみると、ethtool が無いよと怒られたので、ansible の taskに追加。

- name: Install ethtool
  apt: pkg=ethtool state=installed
  tags: packages

次は gawk が無いよと怒られたので、更に task追加

- name: Install gawk
  apt: pkg=gawk state=installed
  tags: packages

これで実行できるようになった。