JavaScript に向き合う
趣味や学問でプログラミングをする人にとって、汎用言語である Python をマスターしておけば大体事足りる。ただ JavaScript は近年めきめきとプログラミング言語としての頭角を現してきており、腰を据えて学ぶのも悪くない。
Pythonista にとって二番目にマスターすべき言語は JavaScript であると私は確信している。理由は以下だが、それを詳説することは本稿の目的ではない。
- Webブラウザという最強のUI上で動作するほぼ唯一の処理系
- キラーアプリ: D3.js, React.js(などSPA実現方式)
- 非同期プログラミングの先輩 (Promise, await)
- 高密度で処理を記述可能
本記事は Python で大体何でも作れるプログラマ向けに、JavaScript で本格的に処理をかけるようにするための手引きとなることを目的とする。(というか私自身に向けた資料でもある。)気づいたときに随時更新する。
Pythonは 3.5+, JavaScript は最新(主に ES6以上)を利用。 node.js のインタプリタ機能を適宜利用する。プログラミングに関する基本知識とJavaScriptの基本、Pythonには習熟していることを前提とする。
JavaScript マスターへの道
Python をマスターした諸兄において、プログラミング言語の取得方法を説教するのは釈迦に説法であろう。ざっとこんな順序で知識を整備していくのが近道ではないかと思うがどうか?
- 対話シェルで言語仕様を手に馴染ませる(基本制御, 文字列操作, 配列操作)
- ライブラリを手に馴染ませる
- 構造化する(関数、クラス、モジュール)
- 味のある制御をする(関数型プログラミング、非同期プログラミングなど)
なお、注意するべきことがある。JavaScript の歴史は長く、大幅な言語仕様の変更を経ながらプログラミング言語としての完成度を上げてきた。これが意味することは、過去のTipsの多くがバッドノウハウになってしまっているという点だ。Web記事や書籍が参考になるどころか悪影響になりうる。その意味で、以下のQiitaエントリは上記の懸念を払拭するのに役立つので必ずチェックするべき。少なくとも本投稿よりは有用である。
あとMDNの以下のページは若干のボリュームがあるが、気づきが多いだろう
現代的な JavaScript の書き方は Airbnb のコードガイドも有用
- JavaScript Style Guide by airbnb
それでは、以下本編
Step#01: 手に馴染ませる
まず「反復操作」から始めよう。反復操作は、CやJavaと同等の do .. while
文, for
文があるが、(Python でもそうであるように)そんなに使わない。配列に紐づいた反復処理に対しては forEach
を使うのが一般的。また、map
, reduce
, filter
も存在するので Python において反復操作をするのと同じ感覚で操作できる。なお、JavaScript にも内包表記があるが非推奨 。最近の潮流としては、配列以外のイテラブルでも同じ感じで使えるような for .. of
文も推奨されている。for .. in
文は配列には使わない。for..in
とfor..of
の違いはこのMDNへのリンクの最後のほうにある
> const arr=[1,2,3]
undefined
> arr.forEach(d=>{console.log(d)})
1
2
3
undefined
> arr.forEach((d,i,me)=>{console.log(`#${i}: ${d}`)})
#0: 1
#1: 2
#2: 3
undefined
> arr.map(d=>2*d)
[ 2, 4, 6 ]
> arr.filter(d=>d%2==0)
[ 2 ]
> arr.reduce((a,b)=>a+b, 0)
6
> for (let d of arr){console.log(d)}
1
2
3
undefined
上記は配列に関する反復処理を見た。一方辞書型に関する反復処理はどうするか?推奨されるのはMap
型をつかって上記と同様にfor .. of
文を使うことだ。しかし、Map
型は比較的新しいデータ構造であるし、キャストや初期化がめんどくさいのでオブジェクトをそのまま使うことが多い。ここでは触れないが、Set
型もあるので重複が気になる場合は採用する。
> const dic = {one:1, two:2}
undefined
> for (const [key,value] of Object.entries(dic)) {console.log(`${key}:${value}`)}
one:1
two:2
undefined
> const mdic = new Map([ ['one',1],['two',2] ])
undefined
> for (const [key,value] of mdic) { console.log(`${key}:${value}`) }
one:1
two:2
undefined
続いて「文字列関連」を見てみよう。まずは、数値⇒文字列。Pythonでは str.format()
があり柔軟なフォーマッティングができるが、JavaScriptではtoFixed()
以外は外部モジュールを頼るくらいか。 d3-format を使おう。日付⇔文字列の相互変換は Date
型およびそれに付随するメソッドも役不足感は否めない。d3-time-format を使うと、strftime
に似たフォーマッティングもできるし、文字列をDate
型にするというパーサーを簡単に作ることができる。
> Math.PI.toFixed(3)
'3.142'
> const d3 = require('d3')
undefined
> d3.format('.3f')(Math.PI)
'3.142'
> d3.format('.1%')(0.1234)
'12.3%'
> d3.format('05')(123)
'00123'
> d3.format('10')(123)
' 123'
> d3.format(',.1f')(1234.5678)
'1,234.6'
> d3.timeFormat("%Y-%m-%d")(new Date)
'2018-02-10'
> d3.timeParse("%Y-%m-%d")("2019-01-01")
2018-12-31T15:00:00.000Z
すでに何気なく使ってきたバックティックによる文字列リテラルだが、これはPythonでいう '''
と同様に、改行を\nで表現せずに含めることが出来たり、${i*2}
等のように変数を埋め込める便利な表現なので使わない理由がない。
> const txt = `hello
... world`
undefined
> console.log(txt)
hello
world
undefined
> const h='hello' , w='world'
undefined
> console.log(`${h} ${w}`)
hello world
undefined
Step#02: 地獄に落ちる
JavaScriptはイベントループ上で動作するため、同期関数が少ない。関数を二回連続呼ぶような f1();f2();
というコードにおいて、f1
が非同期関数な場合、f1
の終了を待たずに f2
が開始される。それは困るよって場合は、最近では await が導入されて await f1(); f2();
と書けるようになった。
とはいえ、多くのライブラリはまだ await(Promise) 対応をしていない。じゃあどうしているかっていうと、f1
の引数にコールバックを渡すようなAPI設計になっているわけだ。つまり、f1(f2);
のように f1
の処理が完了し準備ができた場合に、 f1
がf2
を呼ぶためにf2
を引数として渡すという設計だ。より正確には、f2
がf1
の結果を使いたいこともあるのでこんな感じのAPIになっていることが多い f1(d=>{f2(d));
。f2
を直接渡すのではなく、f1
で得られたデータをf2
に渡せるように新しく関数を作るわけ。
以上をまとめると、
one = await f1(arg)
two = await f2(one)
three = await f3(two)
await f4(three);
といった感じの処理は、
f1(arg, (one)=>{
f2(one, (two)=>{
f3(two, (three)=>{
f4(three)
}
}
})
というように書かなければいけない。これがコールバック地獄だ。
今後 JavaScriptのライブラリを選定する際には、API設計がコールバック型か、Promise型かを確認しよう。もちろん、Promise型のAPIを持つライブラリを選択するのが望ましい。なお、Nodeのutil
モジュールには promisify
というユーティリティ関数があり、コールバック型APIをPromise型APIに変換できるので、これを使ってみるのもいいかもしれない(ただし万能ではない)。
ただ、JavaScriptには await/Promise 導入前の遺産であるコールバック地獄があるのは事実だが、様々なライブラリがノンブロッキング関数で提供されているのはJavaScriptの大きな魅力の一つとなっている。プログラミング初心者には手に負えないかもしれないけれど。
Step#03: 長所を愛でる
JavaScript ならではのテクニックをみて回ろう。
- クロージャ
クロージャは Python と比べるとものすごく簡単にかける。関数がオブジェクトだから、かな。
function createCounter (){
let _value = 0
return {
value: ()=>_value ,
incr: ()=>_value++ ,
decr: ()=>_value--
}
}
counter = createCounter()
console.log(counter.value()) //0
counter.incr()
console.log(counter.value()) //1
counter.decr()
counter.decr()
console.log(counter.value()) //-1
console.log(counter._value) //undefined, unaccessible
- ジェネレータ
JavaScript におけるジェネレータは、Pythonのそれと同様、大まかに言って二つの用途がある。イテレータと(Pythonでいう)コルーチン。イテレータの方は難しくなく、要素数が無限にある配列だとか、配列の要素を遅延評価するときに使うと威力を発揮する↓
function * gen(){
yield 1
yield 2
yield 3
}
// for-of statement
for (d of gen()) {console.log(d)}
// generator to iterator to array
console.log([...gen()]) //[1,2,3]
// advance iterator manually
const it = gen()
console.log(it.next())�// 1
console.log(it.next())�// 2
console.log(it.next())�// 3
console.log(it.next())�// undefined
コルーチンの方もPython版とそんなに変わらない。Python の場合、コーラーは next(iterable)
でコルーチンから値を受け取り、iterable.send(value)
でコルーチンに値を渡すことを覚えているだろうか? JavaScript の場合は どちらの場合にもnext
を使う。
function* translator(initmsg){
const answer = {one:1, two:2, three:3}
let question
do{
question = yield answer[question]
} while (question != 'quit')
}
t = translator()
t.next()
console.log(t.next('one').value) //1
console.log(t.next('two').value) //2
console.log(t.next('three').value) //3
console.log(t.next('quit').done) //true
ジェネレータのコルーチンとしての性質を利用すると、非同期関数の取り扱いがものすごくうまくいく。これはPython で yield from
を使った非同期処理がうまくいくのと同じ理屈。ちょっと難しいので、 JavaScript の記事へのリンクを貼ってお茶を濁しておく (ざっくり, しっかり)。 ジェネレータを使った非同期処理は、 co
redux-saga
など人気のあるライブラリで使われているので我慢して理解しよう。
- イテレータプロトコル
Python で拡張for文が使えるためには __next__()
を実装するとかStopIteration を返すとか、そういう規約に従ったもの = Iterable が必要だった。JavaScriptにおいても for of
文で反復操作を行うための規約、すなわちイテレータプロトコルというものがあり、それに則ったものは for of
文で扱える。具体的には、 String
, Array
, Map
, Set
, ジェネレータオブジェクトが該当する。
iterables =[
'hello',
[1,2,3],
new Map([['one',1],['two',2]]),
new Set([['one',1],['two',2]]),
(function* g(){yield 1; yield 2})()
]
iterables.forEach(it=>{
for (d of it) { console.log(d) }
})
イテレータプロトコルを持つオブジェクトは、for of
文だけでなく、スプレッド...
, yield*
(yieldの移譲), destructural assignment にも対応する
ite = [1,2,3]
//spread syntax
newobj = [...ite]
//yield*
function* gen(){
yield* ite
}
//destructual assignment
const [a,b,c] = ite
Step#04: 職人技を盗む
珍しい書き方があっても動じないようにしよう。
- Object 関連
オブジェクトのメンバに変数がポツンと入っているような {h}
のような表現。これは変数名がキー、値は変数の値が入る。{...ite}
. のような表現(スプレッドシンタクス)は、iteの各要素が展開される。オブジェクトのコピーなどの用途に使う。ディープコピーではないのに注意。Object.assign
もディープコピーじゃない。ディープコピーはライブラリlodashとかを使う。キー名を変数で与えたいときは{[key]:value}
のような表記をする。
const one =1
const dic ={one, two:2} //{one:1,two:2}
const dic2 = dic����������//{one:1,two:2}
const newdic = {...dic} //{one:1,two:2}
console.log(dic===dic2) //true
console.log(dic===newdic) //false
console.log({...dic, three:3}) //{one:1,two:2,three:3console.log({...dic, one:'(-1)*(-1)'}) //{one:(-1)*(-
// spread is not deep copy
let obj = {odd:[1,3,5],even:[2,4,6]}
let obj2 = {...obj}
obj.odd.pop()
console.log(obj2.odd) // Wow! [1,3]
// Objects.assign cannot help
obj = {odd:[1,3,5],even:[2,4,6]}
obj2 = Object.assign({},{...obj})
obj.odd.pop()
console.log(obj2.odd) // [1,3]
const _=require('lodash')
obj = {odd:[1,3,5],even:[2,4,6]}
obj2 = _.cloneDeep(obj)
obj.odd.pop()
console.log(obj2.odd) // [1,3,5]
const key = 'hello'
const value = 'world'
console.log({[key]:value}) //{ hello: 'world' }
- 関数
!function ....{} ()
は即時関数の実行を表す。可変長引数は function (arg1,arg2,...args)
となる。時々 arguments
という謎ワードが入ってくることがある、、、、とかをもう少し詳しくかく
Step#05: 有名ライブラリを知る
Pythonと違い、JavaScriptは標準ライブラリが相当貧弱なので外部ライブラリを知ることはとても大事。
- ユニットテスト
導入必須。 mocha
+ chai
か、 jest
あたりが有名。Promiseもコールバックもモックもできる。
- Lint、フォーマッタ
es-lint
一択。eslint-config-airbnb
がよく使われる。フォーマッタは prettier
。
- HTTPクライアント
サーバサイド(Node)、ブラウザともにHTTPリクエストを送信する標準関数は微妙に使いずらい。Fetch API の話とaxiosの紹介と、よい比較記事 の紹介
- ユーティリティ系
lodash
, immutable
あたりはモダンJSでカバーされつつあるが、それでもまだ有用。
Step#06: 実行環境を気にする
Nnvm, npm, babel, browsify, webpack, あたり?あとモジュールの話。