JavaScript: The Good Partsを読んで中級者へなりたい! (勉強内容まとめ)
最近Webアプリケーションのフロントエンドの知識が足りていない!と思いJavaScriptを意識して触るようにしてきました。
基本的な文法や簡単なAPIは押さえたり、Angularでアプリ作ってみたりして、徐々に身に付いてきたかなーと思っています。ですが、未だに初心者の域を出ないかな・・・と。
そんな訳で優秀で信頼できる後輩に「何か良い本とか知らない?」と聞いてみたところGood Partsを教えてもらいました。
[tmkm-amazon]4873113911[/tmkm-amazon]
古い本なのでそこだけ微妙らしいですが、良い本みたいです。
この本、Good Partsと言うよりはBad Partsの方が合っている気がしています。なぜかというと、基本方針が「JavaScriptは良い機能もあるけど、悪い機能が多いので、悪い部分は使わないようにしよう!」というものだからです。
何はともあれ、良い本ならば学べる事も多いだろう!という考えで読み進めてきました。難しい部分もありますが、薄い本なのでそれほど辛くはないです。今日はこの本から学んだ事をまとめてみようと思います。
ブロックコメント/* */は注意
正規表現リテラルで*/が出てくるので、正規表現部分をブロックコメントで囲むようなケースは注意が必要です。
コメントは行コメント//で統一するとか、ブロックコメントは関数の説明部分だけに使用するとか、何かしらの基準を自分の中に持っておくと良いかもしれないです。
予約語
caseだとかelseだとかは他の言語と同様だし、まず使わないので大丈夫かと思います。default delete inあたりは気をつけた方が良いかなという印象です。
数値
JavaScriptでは数値型は1つ。1と1.0は同じ値。結構驚きでした。
ブロックと変数定義場所
ブロックは一連の命令文のことで、中括弧で囲まれている。ただしJavaScriptのブロックは、ほかの一般的な言語とは異なり、新しいスコープを生成しない。したがって、変数はブロックの中ではなく、関数の先頭で定義すべきだ。
JavaScript: The Good Parts p.12
これはどうなんでしょうか。Code Complete(別の本)やリーダブルコード(別の本)では変数は利用する位置に近い位置で定義する的な事が書かれていましたし、Javaプログラマの自分としてはその通りにすべきだと思います。著者もこれは分かっててp.42で他の言語ではそうだけど、JavaScriptは違うから関数の先頭で定義するんだ!と言っています。9章のスタイルで、その理由として、JavaScriptだと変数を利用した後に定義することもできる、これは書き間違いのように見える。と言っています。
利用した後に定義というのは以下のようなことかな、と思っています。
value = '9980'; console.log(value); // 色々な処理 var value = '980'; //再定義 console.log(value);
確かにvalueを使った後でvar valueが登場すると違和感を感じますし、書き間違えと思うかもしれません。
※1行目のvalueはグローバルに暗黙的に定義されます。7行目のvar valueもグローバルスコープに書かれているので、グローバルに定義されるのですが。
しかし関数の頭でvar count = personCount();とかやってその200行下のforブロックでcountを利用する、みたいなシーンを考えると微妙な気がします。個人的にはCode Completeに従いたいです。
falseと見なす値
null undefined 空文字列 数値0 NaNもfalseとみなされるようです。つまり以下のコードはValidです。
if(null){ console.log('null...'); }else{ console.log('not null!!!'); }
Javaの場合はif(null)とか書くとnullをbooleanに変換できないよ、というコンパイルエラーになります。強い静的型付き言語の場合はコンパイラによるチェックが嬉しいわけで、実際自分も嬉しいです。この本の筆者曰く強い静的型付き言語のコンパイラによるエラー指摘は本当に心配しなければならないエラーではないそうです。なるほど、確かにそう考えると動的型付きも良いのかもしれません。実際if(obj)とか書けるのは良いかもしれません。
ちなみに私は過去にif(val === undefined || var === null …みたいなコードを書いたことがあります・・・。
&&でTypeError例外を回避
いわゆるNullPointerException的な奴です。例えばmyObjというオブジェクトがあるけど、それはnameプロパティを持っていない状況を考えます。このときmyObj.nameは定義されていないのでundefinedですが、myObj.name.firstNameはTypeErrorになります。
以下のコードのように&&を使うと回避できます。firstNameが定義されていなければundefinedに、定義されていればその内容(以下だとtsukaby.com)になります。
var myObj = {}; console.log(myObj.name); //undefined console.log(myObj.name.firstName); //TypeError console.log(myObj.name && myObj.name.firstName); //undefined myObj = {name:{firstName:'tsukaby.com'}}; console.log(myObj.name && myObj.name.firstName); //tsukaby.com
関心はしましたが、使う場面はいまいち想像できませんでした。
参照渡し
オブジェクトは参照渡しです。コピーされることはないようです。
var myObj = {name:'tsukaby'}; var tmpObj = myObj; console.log(tmpObj.name); //tsukaby myObj.name = '.com'; console.log(tmpObj.name); //.com tmpObjも同じオブジェクトを参照しているため
個人的にはこれは値渡し(アドレスという値渡し)だと思っています。Javaもそうで、参照渡しという人は多いけど、Javaも値渡しだと思っています。参照渡しはC#のrefなどを指すのだと考えています。
JavaScriptはJavaなどと同様のようです。以下のコードはchange関数で値を変更する事はできません。
function change(target){ target = {name: 'change'}; } var myObj = {name:'tsukaby'}; console.log(myObj.name); //tsukaby change(myObj); console.log(myObj); //tsukaby
target.name = ‘change’なら値は変わるのですが。これはJavaやCなんかも同じですね。こういうタイプは個人的には値渡し(アドレス渡し)という認識です。
オブジェクトは結局ポインタ(アドレス)、関数に渡すときは値渡しみたいに覚える事にしました。
プロトタイプ
すべてのオブジェクトは、プロトタイプオブジェクトとリンクしていて、そこからプロパティを継承している。
プロトタイプは正直未だに良く分かっていませんが、これがJavaで言うところのclassつまり、ひな形機能なのかなと考えています。
プロパティの列挙
あるオブジェクトのプロパティを順になめていきたい場合、for in文ではなく、プロパティを列挙した配列+forを使うと良いようです。
プロパティの削除
deleteでプロパティを削除すると、プロトタイプチェーン上の同じ名前のプロパティが見えるようになります。メソッドをオーバーライドしたけど、やっぱ止めて、スーパークラスのメソッドを使う的な感じ・・?
利用シーンはまだ想像できないです。
グローバル領域はなるべく使わない
これはどの言語でも同じですね。もしJavaScriptでグローバル領域を使う場合はvar MYAPP = {}として、このMYAPPに各プロパティを追加してグローバルに使うように、とのことです。
確かにJavaScriptは名前空間ないし、下手な変数名付けて他のライブラリと競合したらまずいです。下手にたくさん作らない方が良いのはその通りだと思います。
上記のケースだとMYAPPがいわゆる名前空間になっているから大丈夫という訳ですね。
呼び出しのパターンで変わるthis
人のコードを読んでいるとvar that = this;を見かけます。初めは意味が分かりませんでした。thatってなんだ!?と。thisは場合によってglobalを指すことがあるので、それを回避する為の方法ですね。
例えば以下のコードですが、myObj.doSomething()はメソッド呼び出しパターンですが、doSomething2()は関数呼び出しパターンです。そのため、thisの扱いが違います。
doSomethingの中でthisはmyObjです。そのため、thatにmyObjを退避しておくことになるため、doSomething2内の1行目では’myObj local value’がプリントされます。ですが、2行目では関数呼び出しパターンのためthisはglobalを指しており、’global value’がプリントされます。
著者がこのthisは設計ミスと言っているように、訳が分かりませんね。こういうものだと覚えておくことにしました。
var myObj = {}; myObj.value = 'myObj local value'; myObj.doSomething = function(){ var that = this; var doSomething2 = function(){ console.log(that.value); //myObj local value console.log(this.value); //global value } doSomething2(); } //Global var value = 'global value'; myObj.doSomething();
可変長引数
関数内でargumentsという変数を見ると、引数が取れます。そのためfunction()のように仮引数が0個でも、関数内でargumentsを使いつつ、関数呼び出し側で()内に実引数を与える、というようなことが可能。ようするに可変長引数ですね。
関数の暗黙のreturn(undefined)
関数は常に戻り値を返すもので、特にreturnで指定していない場合はundefinedが返るようです。newした場合はundefinedでなくthisが返るようです。
必ずreturnするんだ!?と思いましたが、戻るのはundefinedだし、別にどうでも良い情報な気がしてきました。
throwするオブジェクト
慣例的にthrow {name:’hoge’, message:’hoge’};として、nameとmessageを持たせるようです。
クロージャ
関数内に関数を書けたり、それによって変数を隠蔽できるのは少し驚きです。
カスケード
setterメソッドなどは普通は戻り値なしですが、thisを返すようにすればチェーンできます。このチェーンのことをカスケードと言うそうです。JavaではBuilderパターンでよく知られてますので、これはすんなり頭に入りました。JavaScriptはJava以上にチェーンになってることが多いですね。
オブジェクト指定子
func(a, b, c, d, e)のように引数が多くなる場合は、以下のようにした方が安全だし可読性も上がります。順番を気にしなくて良いのは大きいですね。
func({ age: a, name: b, birthday: c, gender: d, address: e });
配列の要素の削除
変数はdeleteで定義を削除してundefinedにすることができますが、配列の各要素に使う場合は注意が必要です。というよりは使用禁止ですね。
var arr = ['zero', 'one', 'two', 'three', 'four', 'five']; console.log(arr); //["zero", "one", "two", "three", "four", "five"] console.log(arr.length); //6 delete arr[1]; console.log(arr); //["zero", undefined, "two", "three", "four", "five"] console.log(arr.length); //6
deleteによって’one’の部分がundefinedになっていますが、 要素が減る事は無くlengthは6のままです。
何とかしたいときはarray.spliceを使えばOKです。
var arr = ['zero', 'one', 'two', 'three', 'four', 'five']; console.log(arr); //["zero", "one", "two", "three", "four", "five"] console.log(arr.length); //6 arr.splice(1, 1); console.log(arr); //["zero", "two", "three", "four", "five"] console.log(arr.length); //5
spliceによってindex1の場所から1個の要素を取り除いています。ちゃんと’one’が消えてlengthが5になりました。
spliceは遅いので要注意
上記のspliceは便利ですが、詰め替え処理のせいで遅いです。例えば以下のコードを実行してみます。
var arr = []; var arr2 = []; for(var i=0; i<100000; i++){ arr.push(i); arr2.push(i); } // 先頭から1つずつ要素を取り除く console.time('timer'); for(var i=0; i<100; i++){ arr.splice(0, 1); } console.timeEnd('timer'); //私がやったときは大体6msでした // 配列の後ろの方に対して1つずつ要素を取り除く console.time('timer2'); for(var i=0; i<100; i++){ arr2.splice(90000, 1); } console.timeEnd('timer2'); //私がやったときは大体1msでした
要素数10万の2つの配列に対してspliceを100回ずつ実行し、実行時間を計測します。何回かやりましたが、splice(0, 1)の方は6msほどで、splice(90000, 1)の方は1msほどでした。
前者の方が削除した要素の後の部分が大きいので詰め替えが大量に発生してその分時間がかかります。
どうやって回避するかですが、例えば連結リスト(Linked List)とかでしょうか。要素を削除してもリンク(ポインタ)を少し繋ぎ変えるだけなので詰め替え処理は発生せず早いと思います。少し探しましたが、連結リストは見つけられませんでした。もしかして標準では用意されていないのでしょうか。
array.sortはstringとして処理される
var arr = [3, 297, 45, 99, 5, 1000, 27]; console.log(arr); //[3, 297, 45, 99, 5, 1000, 27] arr.sort(); console.log(arr); //[1000, 27, 297, 3, 45, 5, 99]
sortは配列の中身の型までは意識してくれないらしく、文字列として処理するようです。なので、上記のようなことに。
著者は比較関数を自分で用意すれば対応できると言っていて、実際以下は正しく動きます。
var arr = [3, 297, 45, 99, 5, 1000, 27]; console.log(arr); //[3, 297, 45, 99, 5, 1000, 27] arr.sort(function(a, b){ return a - b; }); console.log(arr); //[3, 5, 27, 45, 99, 297, 1000]
数値のコンパレータを自分で用意しなくてはならないということに納得が行きませんが・・・。とりあえず数値のソートは普通にはできないと覚えておく事にしました。
セミコロン挿入機能の注意(return文)
return { hoge: 'hoge' };
とかなっていると、returnの後に自動で;が挿入されてundefinedが戻り値になってしまうそうな。K&Rスタイルに従って{は行末に置いてreturn {とするのが良さそうです。
浮動小数点数の加算
JavaScriptでは0.1+0.2=0.30000000000000004です。これはどの言語でも大抵そうで、注意しなければならない問題です。他の言語だとDecimalみたいなクラスを使って誤差を防ぎますが、JavaScriptではスケーリングして対応するようです。
var num = 0.1*10 + 0.2*10; console.log(num/10);
10倍してやった後に10で割ってやると。結局他の言語のDecimalとかと同じ仕組みですね。
function hoge(){}とvar hoge = function(){}は同じ
ずっと疑問に思っていたので、この本読んでよかったです。
varの方だけを利用する方が良いかもしれません。これからはそうしようと思います。理由は本を参照ください。
付録
各章のまとめが載っているので、さらっと復習したいときには便利かと思います。
JSLintの解説も載っていて、良いコーディングに役立つと思います。ですが、最近だとJSHintの方が話題になっていますし、自分もJSHintを使っているので、その点古い本だなーと思いました。
意味が分からない点もありますし、納得できない点もありましたが、かなり勉強になりました。
JavaScript初めたばかりの人には良い本かと思います。みなさんもいかがですか?