最近フロントエンドエンジニアになりつつある感じなのですが、ES2015?聞いたことないです状態からこの1ヶ月でJavaScript周りについて色々勉強する必要がありました。
基本的には、やれVueだNodeだCordovaだwebpackだと名前しか知らなかったトレンディーなFWやJSライフサイクルに必死になっいてたのですが、JavaScrptのもっと基本的なところも勉強してるので学んだことを書こうと思います。
変数定義
JavaScriptの変数定義といえばvarでしたが、イマドキは基本的にletとconstを使います。
ちなみに修飾子なしでも変数定義できます。(非推奨。常にグローバル変数になっちゃう)
違いはこんな感じ。
-
letとconstは再宣言できない。 -
constは再代入もできない。 -
letとconstはブロックスコープを持つ。
// 1.再宣言
var hoge = 'hoge'
let huga = 'fuga'
const piyo = 'piyo'
var hoge = 'hogehoge' // できる
let huga = 'fugafuga' // できない
const piyo = 'piyopiyo' // できない
// 変数の巻き上げがなくなる、嬉しい!
// 2.再代入
var hoge = 'hoge'
let huga = 'fuga'
const piyo = 'piyo'
hoge = 'hogehoge' // できる
huga = 'fugafuga' // できる
piyo = 'piyopiyo' // できない
// 再代入しない変数は基本的にconstにしよう
// ただしオブジェクトの中身の変更はできるよ
const obj = { foo: 'foo' }
obj.foo = 'bar' // 中身を変えるだけならできる
console.log(obj.foo) // ->'bar'
// 3.ブロックスコープ
if(true) {
var hoge = 'hoge'
let huga = 'fuga'
const piyo = 'piyo'
}
console.log(hoge) // ->'hoge'
console.log(huga) // ->undefined
console.log(piyo) // ->undefined
// var、危ない
非同期処理
フロントエンドでユーザのアクションに合わせて動くJavaScriptは非同期処理を多用します。
非同期処理の書き方は何度かの変遷を経て便利になりました。
- コールバック関数
-
promiseオブジェクト -
async/await
それぞれ例として、受け取った文字列を3秒後に整形して出力する関数を作ってみます。
// グローバル関数
const shape = function(target) {
return 'Hello, ' + target + '!!'
}
コールバック関数
// 1.callback
const callbackHelloWorld = function(target) {
setTimeout(()=>{
console.log(shape(target))
}, 3000)
}
// 実行
callbackHelloWorld('World')
これだけの例だと短いので普通に読めますが、console.log()の中身がまた関数のコールバックになったり、引数の値によって出力が変わったりと複雑な処理が増えるとネストがどんどん深くなって読みづらくなりそうです。
Promise
// 2.promise
const promiseHelloWorld = function(target) {
return new Promise(function(resolve, reject){
setTimeout(()=>{
resolve(shape(target))
}, 3000)
})
}
// 実行
promiseHelloWorld('World').then((target)=>{
console.log(target) // 成功時の処理
}).catch((e)=>{
console.log(e) // 失敗時の処理
})
Promiseのインスタンスを返却し、非同期処理が終わった後に成功したかどうかで処理を分ける。
非同期処理の前と後で記述を分けられるのでコールバックより可読性が高い。
Promise.all()を使えば並列処理も書ける。
でも毎回return new Promise(function(resolve・・・ってやるの冗長な感じがします。
async/await
// 3.async/await
const asyncHelloWorld = async function(target) {
try {
await setTimeout(()=>{
target = shape(target)
}, 3000)
console.log(target)
} catch(e) {
console.log(e)
}
}
// 実行
asyncHelloWorld('World')
読みやすい・・・はず。
エラー処理も慣れ親しんだtry-catch構文で書けるし、内部的にはPromiseオブジェクトが動いていますが毎回Promiseインスタンスを作らなくてもよくなりました。
Promise.all()での並列処理もPromiseオブジェクトをそのまま使うよりずっと簡単に記述できます。
オブジェクトと型
JavaScriptは全てがオブジェクト、と言われます。(私はそう認識していました。)
実際以下のように、リテラルで定義したプリミティブ型の変数でも以下のように組み込み関数を実行できます。
let foo = 123
console.log(foo.toString()) // ->'123'
これができちゃう理由としては、fooはプリミティブの数値型として存在しながらも、toString()関数を呼ばれた時にJavaScriptが一時的にNumberオブジェクトのインスタンスを作ってくれているからです。(関数が実行された後は削除されプリミティブに戻る)
プリミティブ型もオブジェクト同様に扱うことができるので、明示的にラッパーオブジェクトとして宣言する必要がありません。
JavaScriptのプリミティブ型はnumber、string、booleanの3つで、それぞれのラッパーとしてNumber、String、Booleanオブジェクトが存在し、こいつらのコンストラクタ関数がプリミティブ値を提供してくれます。
JavaScriptのオブジェクトには以下のような特徴があります。
全てのオブジェクトはObject.prototypeから継承される
StringもBooleanもPromiseもFunctionも、全てのオブジェクトは源流をたどるとObject.prototypeを継承しています。
オブジェクトの継承はプロトタイプチェーンによって行われる
オブジェクトはそれぞれprototypeというプロパティを持っています。
全てのオブジェクトは親オブジェクトのprototypeプロパティにアクセス可能で、これによってJavaScriptオブジェクトは継承を実現しています。
プロトタイプチェーンは継承関係を
- 自身のプロパティ
- 自身のオブジェクトの
prototypeプロパティ - 親のプロパティ
- 親のオブジェクトの
prototypeプロパティ
という順に遡ってプロパティが見つかるまで探し、最後はObject.prototypeまでたどり着きます。
例えばNumberオブジェクトのインスタンスは、Object.prototypeのプロパティであるhasOwnProperty()関数を利用できます。
const num = new Number(123)
num.hoge = 'hoge'
console.log(num.hasOwnProperty('hoge')) // ->true
ネイティブオブジェクトを含めた全てのオブジェクトが可変である
全てのオブジェクトの中身は自由に書き換えることができます。
Object.prototypeも、書き換えられちゃいます。
Object.prototype.hoge = 'hoge'
const fuga = {}
console.log(fuga.hoge) // ->'hoge'
this
グローバルオブジェクトかそれ以外か
こいつが結構ややこしくて、
グローバルフィールドで定義された関数内で呼ばれたthisはグローバルオブジェクト(windowなど)を指しますが、定義済みの関数をグローバルオブジェクト以外のオブジェクトのプロパティとして参照した場合、その関数内で呼ばれたthisは参照元のオブジェクトを指します。
???
const hoge = 'hoge'
const obj = { hoge: 'fuga' }
const callHoge = function() { console.log(this.foo) }
obj.callHoge = callHoge // プロパティを追加
callHoge() // ->'hoge' ・・・グローバルのhogeプロパティを指している
obj.callHoge() // ->'fuga' ・・・objのhogeプロパティを指している
でもまあ分かるって感じです。
call()とapply()
call()関数、apply()関数を使うと、thisの参照先を変えられます。
const hogeObj = {
hoge: 'hoge'
}
const fugaFunc = function(fuga) {
this.hoge = fuga
}
// hogeObjとしてfugaFuncを呼び出し
fugaFunc.call(hogeObj, 'fuga')
// thisがhogeObjを指していた!
console.log(hogeObj.hoge) // ->'fuga'
!?!?
知らない関数に無理やりプロパティ書き換えられるhogeObjかわいそう・・・
こんな自由なこともできてしまうので、thisがどこを指しているのかは十分注意が必要そうです。
call()関数の第二引数以降は可変長で、apply()関数は第二引数に配列をとって同様の動きをします。
どういう時に使うと嬉しいのか、今時点ではちょっと想像がつかないですが。
おわり
たまに参考にしてるドキュメントとか記事が古かったりするので、ES2015以降の仕様で変わっている部分とかあるかもしれないですが確認できてないです。
まだまだ変化してる言語なので仕様の変遷も追いかけないと。