JavaScript

「JavaScriptパターン」第4章-関数-が面白かったからまとめながら写経した

投稿日:2017年4月21日 更新日:


タイトルにあるようにとても面白かったというのと、JavaScriptパターンの輪読会で第4章「関数」の後半を担当したのでその記録も兼ねて内容まとめながら写経した。

目次

はじめに

JavaScriptでは関数がさまざまな目的で使われるので、JavaScriptプログラマにとって関数の取得は必須です。他の言語なら特別な構文があるような処理でもJavaScriptでは関数で処理されます。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptで最もすばらしいのは、関数の実装方法である。

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

上記にあるようにJavaScriptは関数にいろんな構文が集約されているので関数の理解は必須。これまでJavaScript辛いと思いながらなんとかノリでやってきたけど、この本を読んでJavaScriptたのしい!ってなった。

4.1 背景

JavaScriptの関数には2つの大きな特徴がある

  1. 第一級オブジェクト
  2. スコープを提供する

第一級オブジェクト(ファーストクラスオブジェクト、first-class object)は、あるプログラミング言語において、たとえば生成、代入、演算、(引数・戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる対象のことである。

第一級オブジェクト – Wikipedia

関数とは次のオブジェクトのことを指す

  • プログラムの実行時に動的に作成できる
  • 変数に代入できる、他の変数にコピーされた関数への参照を持てる、拡張できる、いくつかの例外状況を除いて削除できる
  • 他の関数に引数として渡せる、他の関数の戻り値にできる
  • 独自のプロパティとメソッドを持てる

スコープを提供するとは

  • JavaScriptには波括弧を使ったローカルスコープはない(言い換えるとブロックはスコープを作らない)
  • JavaScriptにあるのは関数スコープだけ
  • ifの条件部あるいはforやwhileのループの内部でvarを使って変数を定義しても、その変数はそれらの内部で゙ローカルな変数にはならない(その変数は関数の中でのみローカル)
  • 関数で囲んでなければその変数はグローバル変数になる

4.1.1 用語の整理

名前付き関数式

関数式(無名関数)

関数宣言

4.1.2 宣言と式:名前と巻き上げ

構文上の制約で宣言が使えない時には関数式を使う

これは関数式
引数として関数callMeに渡している

これは名前付き関数式

これも関数式

関数宣言で定義されたものは変数やプロパティに代入することができず、関数呼び出しのときのパラメータとして現れることもない。

4.1.3 関数のnameプロパティ

読み取り専用のnameプロパティが利用できる
※関数宣言パターンではと記載してあるけど今は名前付き関数式でもnameは取得できるみたい

名前付き関数式を使うケースは再帰処理のときなどレアケース

4.1.4 関数の巻き上げ

関数宣言と名前付き関数式の振る舞いは関数の巻き上げがあるかどうかの違いがある

  • hoistMe()の中のfooとbarは先頭に移動しグローバルにあるfooとbarは上書きされる
  • ローカルな関数宣言のfooは仕様のあとで定義されていても巻き上げられて上手く動作する
  • ローカルな関数式のbarは宣言だけが巻き上げられて定義は巻き上げられない(コードの実行がbar()に到達するまでbarはundefinedのまま)

4.2 コールバックパターン

関数はオブジェクトなので他の関数に引数として渡すことができる

  • 引数を渡す時括弧がない点に注意
  • 括弧があると関数は実行されてしまう
  • この関数を実行するタイミングはwiriteCode()に任せる

4.2.1 コールバックの例

「コールバックなし」と「コールバックあり」の動作を確認する

コールバックなし

findNodes()が返したノードの配列をhide()がループする必要があるからこの実装は非効率

コールバックあり

hide()はコードをループする必要がなくなるので実装が簡潔になる

もっと省略して無名コールバックを渡す書き方も可能

4.2.2 コールバックとスコープ

コールバックメソッドが属しているオブジェクトを使う時予期せぬ振る舞いになるので注意が必要。次の例ではmyappというオブジェクトのpaint()メソッドをコールバックとして使う状況を想定する。

この場合、findNodes(myapp.paint)の呼び出しはthis.colorが定義されていない為、期待通りに動作しない。findNodes()はグローバル関数なので、オブジェクトthisはグローバルオブジェクトを参照する。この問題は、コールバック関すと一緒にこのコールバックが属しているオブジェクトを渡せば解決する。

メソッドを文字列で渡すやり方もある

4.2.3 非同期イベントリスナ

  • クライアント側でのプログラミングのほとんどはイベント駆動(load、click、keypress、mouseoverなど)
  • コールバックを使うと非同期に実行可能

4.2.4 タイムアウト

ブラウザのwindowオブジェクトが提供するタイムアウトメソッドは、コールバックパターンの例の1つ

  • 関数thePlotThinkensは変数として渡す(関数へのポインタを渡してるだけ)
  • 関数のポインタのかわりに文字列”thePlotThinkens”を渡すのはeval()と同じアンチパターン

4.3 関数を返す

関数はオブジェクトなので戻り値として使うことができる

クロージャーを使うとプライベートデータを格納することができる

4.4 自己定義関数

関数の定義を動的に行い、変数に代入することができる

関数に初期化による準備作業があり、その準備作業の実行を1回きりにする必要があるときに便利

4.5 即時関数

関数を定義したらすぐにその関数を実行するための構文

即時関数は次の上の書き方と下の書き方があるがJSLintは上の構文を推奨する

  • このパターンは初期化のコードの為にスコープ上のサンドボックスを提供するのに役に立つ
  • コードをすべてのローカルスコープに閉じ込め、グローバル変数がもれないようにすることができる

4.5.1 即時関数のパラメータ

即時関数に引数を渡すことができる

一般に即時関数の引数にはグローバルオブジェクトを渡す

4.5.2 即時関数からの戻り値

即時関数も他の関数と同じように値を返すことができ、その戻り値は変数に代入できる

関数を囲む括弧を省略しても同じ結果が得られる(即時関数と区別しにくい)

これでも同じ結果が得られる

即時関数は以下のようにあらゆる型の値を返すことができる

即時関数はオブジェクトのプロパティを定義するときにも使える

4.5.3 利点と使い方

  • グローバル変数を残さずにやりたい作業を包める

4.6 即時オブジェクト初期化

グローバルスコープを汚染から保護するもうひとつの方法に「即時オブジェクト初期化パターン」がある

  • 1回きりの初期化作業を実行する間グローバル名前空間を保護する

4.7 初期化時分岐

「初期化時分岐」は最適化のパターン

変更前

このコードは非効率で、utils.addListener()やutils.removeListener()を呼ぶたびに同じ検査が何度も繰り返し実行されてしまう。初期化分岐を使えば、ブラウザ機能の検査はスクリプトが読み込まれる際に1回だけですむ。

変更後

4.8 関数プロパティによるメモ化パターン

  • 関数はオブジェクトなのでプロパティを持つことができる
  • 関数はすぐに使えるプロパティとメソッドを持っている

関数の引数を取得するlengthプロパティが自動的に与えられる

メモ化(memoization)とは

  • 関数の結果をキャッシュすること

メモ化の例

  • 関数myFuncはmyFunc.cacheでアクセスできるプロパティcacheを作る
  • cacheプロパティはオブジェクト(ハッシュ)で、この関数に渡されたパラメータparamをキーとしその計算結果を値にする
  • 必要に応じていくらでも複雑なデータ構造にすることができる

上記コードは、関数が取る引数はparamだけであり、プリミティブのデータ型(文字列など)であることが前提で、パラメータを増やして複雑にするときは、それらをシリアライズすることが一般的な解。以下の例は引数オブジェクトをJSONデータにシリアライズしcacheオブジェクトのキーとしてそのデータを使っている。

シリアライズするとオブジェクトの一意性が失われるので注意が必要で、2つの異なるオブジェクトでプロパティがまったく同じ場合キャッシュのエントリも同じになってしまう

4.9 設定オブジェクト

設定オブジェクトはAPIをきれいに提供する方法

多数のパラメータを渡すのは不便なので以下のようにパラメータをひとつにまとめてオブジェクトにする

設定オブジェクトの利点

  • パラメータの順序を覚える必要がない
  • オプションのパラメータを安全に省略できる
  • コードが読みやすく保守が楽になる
  • パラメータの追加や削除が簡単になる

設定オブジェクトの欠点

  • パラメータの名前を覚える必要がある
  • プロパティ名はミニファイされない

このパターンは関数でDOM要素を作る場合や要素のCSSスタイルを設定するときに便利

4.10 カリー化

カリー化を確認する前に「関数の適用」の正確な意味を考える。

4.10.1 関数の適用

  • 純粋な関数プログラミング言語においては、関数は呼び出されるものとしてではなくむしろ適用されるもの
  • JavaScriptにおいても同様で、メソッドFunction.prototype.apply()を使って関数を適用することができる
  • 関数は実際にはオブジェクトでありメソッドを持っている

  • 関数の呼び出しも関数の適用も同じ結果が得られる
  • apply()にはパラメータが2つある
    • 関数の内部でthisに束縛されるオブジェクト
    • 配列で関数の内部で利用可能なargumentsのようなオブジェクト
  • 最初のパラメータがnullの場合、thisはグローバルオブジェクトを指す
    • オブジェクトのメソッドではない関数を呼んだときと同じ挙動
    • 関数がオブジェクトのメソッドであるときは、nullを渡してはいけない(そのオブジェクトが apply() の最初の引数になる)

  • sayHi()の内部でthisはalienを指す(前の例ではthisはグローバルオブジェクトを指す)
  • 関数の呼び出しはシンタックスシュガー※にすぎず、関数の適用と等価である
  • Function.prototypeオブジェクトにはapply()とcall()メソッドがあるが、これはapply()のシンタックスシュガーにすぎないがシンタックスシュガーを使った方が良い場合もある

パラメータをひとつしか取らない関数は、call()を使えば配列を作らなくてすむ

applyとcallの定義

シンタックスシュガー(糖衣構文)とは

糖衣構文(とういこうぶん、syntactic sugar)は、プログラミング言語において、読み書きのしやすさのために導入される書き方であり、複雑でわかりにくい書き方と全く同じ意味になるものを、よりシンプルでわかりやすい書き方で書くことができるもののことである。構文上の書換えとして定義できるものであるとも言える[1]。英: syntactic sugar の直訳に近い構文糖(こうぶんとう)とも言い、糖衣構文あるいは構文糖衣とするのは少々意訳的だがよく言われている。

糖衣構文 – Wikipedia

4.10.2 部分適用

下記は部分適用を説明する為のコードで妥当なコードではない

  • 最初の引数の値を取得したら未知のxを既知の値5で関数全体を置き換える
  • 残りの引数についても同じ置き換えを繰り返す

以下のコードは想像上のpartialApply()メソッド

  • 部分適用によって別の関数が得られる
  • その関数は別の引数で呼ぶことができる
  • 実際にはadd(5)(4)のように書いたのと同じになる
    • add(5) が返す関数 をさらに(4)で呼ぶことできる
    • add(5)(4)を使うかわりにこれと似たadd(5, 4)を使うのは、シンタックスシュガーにすぎないと考えることができる

上記までのコードはデフォルトではこのような振る舞いをしないが、JavaScriptはこれを実現させることができる。この関数に部分適用を理解させて処理させることをカリー化という。

4.10.3 カリー化

  • Haskell Curry という数学者の名前に由来する(Haskell というプログラミング言語もそう)

カリー化の例

  • 最初にadd()が呼ばれたとき、それが返す内部関数を含むクロージャーを作成する
  • このクロージャーは元のxとyの値をそれぞれプライベート変数のoldxとoldyに格納する
  • 最初の oldx は内部関数が実行されるときに使われる
  • 部分適用がなく、xとyの両方が渡されたとき関数は両者の和を返すた

以下のコードではyをローカル変数として再利用する

汎用のカリー化関数例

  • argumentsはJavaScriptの本物の配列ではないのでsliceでargumentsを配列に変換して扱いやすくしている
  • schonfinkelize()が最初に呼ばれたとき、slice()メソッドへのプライベートな参照をsliceに格納し、その引数をstored_argsに格納する
  • このとき引数の最初の要素はカリー化される関数な ので、この最初の要素を配列から取り除く
  • schonfinkelize() は新しい関数を返す
  • 新しい関数は古い部分適用された引数(stored_args)と新しい引数(new_args)をマージし、マージされた結果を元の関数fn(これもクロー ジャー内でのプライベートな利用が可能て)に適用する

次のような便利な使い方も可能

4.10.4 いつカリー化を使うべきか

  • 同じ関数をほとんど同じパラメータで呼び出している箇所があった場合
    • 関数に引数のセットを部分適用することで新しい関数を動的に作成することができる
  • 新しい関数には繰り返されるパタメータが格納されてるので(毎回渡す必要がない)、元の関数が期待する引数の完全なリストを事前に用意しておける

参考


-JavaScript
-

執筆者:

関連記事

ECMAScript 6を触ってみる – モダンになった文法

WEB+DB PRESS Vol.87は、@t_wadaさんの言うとおりES6を速習するのにとても良い書籍だと思う。個人的には「JavaScript: The Good Parts ―「良いパーツ」に …

ECMAScript 6を触ってみる – 標準化されたモジュール管理システム

前回に引き続きJSの速習コース実践ラスト。最後は「WEB+DB PRESS Vol.87 第1特集 第6章-標準化されたモジュール管理システム-」を速習する。 これまでのモジュール管理 JavaScr …

ECMAScript 6を触ってみる – 非同期処理 PromiseとGeneratorによるフロー制御

前回に引き続きJSの速習コースを実践。今回は「WEB+DB PRESS Vol.87 第1特集 第5章-簡潔で柔軟な非同期処理-」を速習する。 PromiseとGeneratorによるフロー制御 これ …

おれ的フロントエンド速習コース

3ヶ月でフロントエンド向上させるにあたって取り組んだことを書く。 フロントエンドって? どこからどこまで?何を指してる?と引っかかる人がいるかもしれないけどそこら辺はなんとなくお察しください。 3ヶ月 …

「slideship Tech Dive vol.1 フロントエンド特集」でLTしてきた

connpassリンク slideship Tech Dive v1.0 #React / ReactVR / #VueJs Webフロントエンド特集 「slideship Tech Dive v1. …