最近フロントエンドエンジニアになりつつある感じなのですが、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以降の仕様で変わっている部分とかあるかもしれないですが確認できてないです。
まだまだ変化してる言語なので仕様の変遷も追いかけないと。