計算量(O記法)について [30seconds of interviews]

O記法は、コンピュータサイエンスにおいてアルゴリズムの複雑さを表すために使用される。 優秀なアルゴリズムは高速に結果を返し、かつ複雑度は低いと言える。

アルゴリズムは常に同じ結果にはならず、与えられるデータによって異なる結果になる場合がある。同じ要素を扱った場合でも、あるケースでは高速に実行され、他のケースでは低速になるということが起きる。

以下にJavaScriptでの例を示す。例中の基本時間は1msとする。

O(1)

arr[arr.length - 1]

定数時間。1000 elements = 1ms.

arrayの要素数にかかわらず、理論的には同じ毎回時間実行することになる。(データ量に依存せず毎回1msの処理時間)

O(N)

arr.filter(fn)

線形時間。1000 elements = 1000ms.

実行時間はarrayの要素数に比例して線形的に増加する。 これは、関数が結果を返すまでに全てのarrayの要素をループするからである。

O([1, N])

arr.some(fn)

1000 elements = 1ms <= x <= 1000ms.

実行時間は与えられるデータによって異なる。ここでの最良の場合はO(1)であり、最悪の場合はO(N)である。

O(logN)

arr.sort(fn)

対数時間。1000 elements = 3ms.

ブラウザは通常、logNであるsort()メソッドのクイックソートアルゴリズムを実装している。これは大規模な要素に非常に効率的。

O(N2)

for (let i = 0; i < arr.length; i++) {
  for (let j = 0; j < arr.length; j++) {
    // ...
  }
}

二乗時間。1000 elements = 1000000ms.

実行時間は要素の数に応じてその二乗で増えていく。ネストされた二重ループなどはこれにあたる。

O(N!)

const permutations = arr => {
  if (arr.length <= 2) return arr.length === 2 ? [arr, [arr[1], arr[0]]] : arr
  return arr.reduce(
    (acc, item, i) =>
      acc.concat(
        permutations([...arr.slice(0, i), ...arr.slice(i + 1)]).map(val => [
          item,
          ...val
        ])
      ),
    []
  )
}

階乗時間。1000 elements = Infinity (practically) ms.

Nの階乗で実行時間は増加するため、1つの要素を追加するだけでも実行時間の増加に大きく影響する。

tips

  • ネストされたループの実行時間は指数関数的に増加するので、そのようなコードを書く際には計算量に注意。

30secondsofinterviews.org

仮想DOMはなぜライブラリ/FWに使われるのか [30seconds of interviews]

仮想DOMとは

仮想DOM(virtual DOM)は、DOMをJavaScriptオブジェクトの形式で表現したもの。

これらのオブジェクトには、ノード名/属性/子ノードなど、実際に表示されるDOMノードを記述するプロパティがある。

<div class="counter">
  <h1>0</h1>
  <button>-</button>
  <button>+</button>
</div>

上記マークアップの仮想DOMは、次のようになる。

{
  nodeName: "div",
  attributes: { class: "counter" },
  children: [
    {
      nodeName: "h1",
      attributes: {},
      children: [0]
    },
    {
      nodeName: "button",
      attributes: {},
      children: ["-"]
    },
    {
      nodeName: "button",
      attributes: {},
      children: ["+"]
    }
  ]
}

仮想DOMを使う理由

ライブラリやフレームワークは仮想DOMをパフォーマンス向上の手段として使っている。

アプリケーションの状態が変わる時、更新を反映するためにDOMの再構築を行う必要があるが、これにはコストがかかる。

仮想DOMを使うことで、状態が変更されたときに古い仮想DOMと新しい仮想DOMの差分を比較し、その差分だけを実際のDOMに反映させることができる。 そのため、仮想DOMを使うことで一般的にはDOM変更の際のコストを小さくし、その結果表示速度を向上させることができる。

30secondsofinterviews.org

JavaScript 0.1 + 0.2 === 0.3 はなぜfalseと評価されるのか [30seconds of interviews]

0.1 + 0.2 === 0.3 // false

一見trueになるように見えるが結果はfalseになる。

これはJavaScriptIEEE 754*1を採用し、64ビット浮動小数点数を使用するためである。 要するに、コンピュータは2進数ベースで動いているため10進数の少数を計算する場合、正しく2進数に変換できず誤差を生じてしまう。

0.1 + 0.2 // 0.30000000000000004

この問題の解決方法として、2つの値の差が定義した誤差マージン epsilon*2 より小さいかどうかを判定する関数を使う。

const approxEqual = (n1, n2, epsilon = 0.0001) => Math.abs(n1 - n2) < epsilon
approxEqual(0.1 + 0.2, 0.3) // true

30secondsofinterviews.org

*1:https://ja.wikipedia.org/wiki/IEEE_754

*2:非常に小さな数の意

JavaScriptの純粋関数とは何か?[30seconds of interviews]

純粋関数とは何か?

純粋関数(pure function)とは、これら2つの条件を満たす関数を指す。

  • 同じ入力が与えられると、その関数は同じ出力を返す。
  • 関数は、その関数のスコープにおいて副作用を起こさない。

純粋関数は、上記の2つの条件を満たす限り、関数内のローカルデータを変更することができる。

Pure

const a = (x, y) => x + y
const b = (arr, value) => arr.concat(value)
const c = arr => [...arr].sort((a, b) => a - b)

Impure

const a = (x, y) => x + y + Math.random()
const b = (arr, value) => (arr.push(value), arr)
  • tips
    • 純粋関数は、その信頼性のため推測が用意にできる。
    • 全ての関数は、明示的に副作用を起こさない限り純粋関数であるべきである。
    • もし関数が値を返さないのであれば、すなわち副作用が起こることを示している。

30secondsofinterviews.org

JavaScriptでパイプ関数を作成する [30seconds of interviews]

以下の挙動をする pipe関数を作成する

const square = v => v * v
const double = v => v * 2
const addOne = v => v + 1
const res = pipe(square, double, addOne)
res(3) // 19; addOne(double(square(3)))
  • 与えられた全ての関数をスプレッド演算子...を使って収集する。
  • pipe処理の初期値としてxを受け取るようにする。
  • Array.prototype.reduce()を使って一連の関数を実行する。そのとき、reduceで逐次得た結果を次の関数の引数として渡すようにする。
const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x)

上記をわかりやすく冗長に書くと以下のようになる。

const pipe = (...fns) => {
  return (x) => {
    return fns.reduce((v, fn) => fn(v), x)
  }
}

30secondsofinterviews.org

Javascriptオブジェクトをクローンする [30seconds of interviews]

How do you clone an object in JavaScript?

スプレッド構文...を使うと、オブジェクト自身の列挙可能なプロパティを新しいオブジェクトにコピーできる。オブジェクトの浅いクローン(shallow clone)が作成される。

const obj = { a: 1, b: 2 }
const shallowClone = { ...obj }

上記のテクニックでは、prototypeは無視される。さらにネストされたオブジェクトはクローンされずそれらの参照がコピーされるので、ネストされたオブジェクトは依然としてオリジナルと同じオブジェクトを参照する。ディープクローンする場合は、オブジェクト内にネストされる可能性のある任意のタイプのオブジェクト(Date, RegExp, Function, Setなど)を効果的にクローンするために上記の方法よりはるかに複雑になる。

色々なコピー方法:

30secondsofinterviews.org

CSS BEMとは何か? [30seconds of interviews]

What is CSS BEM?

BEMとは、スコープ問題を解決するために名前空間を定義することでよりCSSをメンテナンスしやすくするCSSクラスの命名規則

BEMはblock, element, modifier の略。blockはプロジェクトを通して再利用可能なスタンドアロンコンポーネントであり、サブコンポーネント(elements)の名前空間として機能する。modifiersは blockまたはelementが特定の状態にあるとき、または異なる構造やスタイルのときにフラグとして利用される。

/* block component */
.block {
}

/* element */
.block__element {
}

/* modifier */
.block__element--modifier {
}

マークアップの例

<nav class="navbar">
  <a href="/" class="navbar__link navbar__link--active"></a>
  <a href="/" class="navbar__link"></a>
  <a href="/" class="navbar__link"></a>
</nav>

上のケースの場合、navbar はblock, navbar__linkはelement(navberコンポーネントの外では意味をなさない)、navbar__link--activeはmodifier(navbar__link 要尾とは異なる状態を示す)。

modifiersは冗長なので、多くの場合、modifierとしてis- *フラグを使用される。

<a href="/" class="navbar__link is-active"></a>

これらは、必ずelementに連鎖し、決して単独で使用しない。

.navbar__link.is-active {
}

30secondsofinterviews.org