Javascript に再入門した

Javascript 再入門

ふと、MDNのチュートリアル記事眺めてたら知らないことばかりだったのでまとめてみようかと思いました。 最初の方は Javascript に関係ないことが続きます。

以下からは 自分が気になったMDN の文を意訳しながら記述しています。

Web を始めよう

大文字小文字、スペースの扱い

MDN のチュートリアル記事では、ファイル名とフォルダー名はすべてスペースを入れずロワーケースで書かれている。
理由は2つ:

  1. 多くのコンピュータ、特にウェブサーバーは文字の小文字・大文字に敏感だから。MyImage.jpgmyimage.jpg は別のものと解釈されるので 文字の大きさを統一した方がしょうもない問題を回避できる。
  2. スペースの扱い方はブラウザやウェブサーバー、プログラミング言語で違うから。例えばファイル名にスペースを使ったら、あるシステムは 一つのファイルを二つと解釈するかもしれないし、あるサーバーはそのスペースを %20URI に使われるスペースとして使われる文字)と置き換えるかもしれない。これは、ファイルとのリンクができなくなる問題を引き起こしかねない。アンダースコア "_" よりもダッシュ "-" を使ってロワーケースで書いた方が良い。

Google Search エンジンは "-" を言葉の区切りとして扱うがアンダースコアはそう扱わない。それらの理由でファイル名にはロワーケースにスペースなしが望ましい。

Javascript

Javascript はページで何してるの?

参照:What is JavaScript doing on your page?/MDN

ブラウザでウェブページをロードしたとき何が起きているのか? そのストーリーを簡単に解説すると、ウェブページをブラウザでロードした場合、実行環境(ブラウザのタブ)内で(HTML, CSS, Javascript) が実行される。

MDN ではこれを原材料(コード)を受け取り製品(ウェブページ)を生産する工場としてたとえながら説明している。 f:id:bitsukun75:20190601133227p:plain

by What is JavaScript doing on your page?

Javascript は、HTML と CSS とが集められウェブページ上で一体となった後、ブラウザのJavascript エンジンによって実行される。これにより、ページの構造とスタイルはJavascriptが実行される前にすでに準備が完了されている状態となる。

このプロセスは非常に良いものだ。なぜなら、Javascript の非常に一般的な使い方は、Document Object Model API (DOM) を介してHTMLとCSS を修正しユーザーインターフェイスを更新することだから。もし、Javascirpt が、HTMLとCSSが準備完了になる前に、ロードされ実行されようとされたらエラーが起こるだろう。

Browser security

参照: Browser security/MDN

ブラウザのタブは実行するコード用につきそれぞれ専用の区分けされたバケットを持つ。これを専門用語で "execution environments" という。これにより、ほとんどの場合それぞれのタブ上のコードは完全に分かれて実行される。これは、安全面で良い基準です。もしこれがなかったらハッカーが他のウェブサービスからユーザの情報を盗むためのコードを書き始めるでだろう。

Script loading strategies

参照: Script loading strategies/MDN

実行時にロードするスクリプトを得ることに関わる懸念点がいくつかある。典型的な問題の例は、ページ上のエレメントを操作するJavascript(正確には、Document Object Model)を使おうとしている場合に、その対象の HTML がロードされる前に そのJavascript がロード、パースされ実行できない状況になること、であろう。

上記のような問題には解決策があるが、テキスト内の Javascript と 外部 Javasciprt で違う。

まず、テキスト内のJavasciprt では、以下のようにする。

document.addEventListener("DOMContentLoaded", function() {
  ...
});

これは、ブラウザのHTML の ボディが完全にロードされパースされることを意味する "DOMContentLoaded" イベントのリスナーであるイベントリスナーである。Javascript はイベントが発火されるまでこのブロック内で実行されないので、エラーが回避できる。

ちなみにテキスト内の Javasciprt とは HTML の script エレメントに直接書かれたコードのことである。

次に、外部Javascriptでは以下のように defer アトリビュートを追加する。これはブラウザー<script> タグに到達してもHTMLのコンテントをダウンロードし続けるように教えるものである。

<script src="script.js" defer></script>

これにより、HTMLと Javascirpt が同時にロードされる。

旧来の解決方法では、<body> タグと対応する </body> タグのすぐ前に <script> タグが置かれていた。<body> タグ内の HTML エレメントが完全にロード、パースされてから<script> エレメントをロードしようとしたものだ。この解決方法の問題点は スクリプトのロード、パースが、HTML DOM が完全にロードされるまでブロックされることだ。これは、膨大な Javascirpt が必要な巨大なウェブページにおいて、主要なパフォーマンス問題を引き起こし、ウェブサイトの速度を低下させてしまう。

async and defer

ブロックされたスクリプトの問題を緩和させることのできる二つの方法がある。
deferasync だ。

非同期スクリプトはページの描画を妨害することなくスクリプトをダウンロードしスクリプトがダウンロードし終わったら即座にそのスクリプトを実行する。スクリプトが特定の順序で実行されるの保証はなく、残りのページが表示されることを止めないことだけしか保証されない。async を使う一番いいタイミングはページのスクリプトがお互いに独立していて、ページ上の他のスクリプトに依存していない時だろう。

例えば、次のようなスクリプトエレメントがあるとしよう。

<script async src="js/vendor/jquery.js"></script>

<script async src="js/script2.js"></script>

<script async src="js/script3.js"></script>

このスクリプトがロードされる順番に依存することはできない。jquery,jsscript2.jsscript3.js の前か後にロードされるかもしれない。この状態で、jquery に依存したスクリプト内の関数はエラーを発生させる。なぜなら、jquery はそのスクリプトが実行される時定義されていないからだ。

defer はページ上に出現した順番でスクリプトを実行し、スクリプトとコンテントがダウンロードされたら即座にそれらを実行する。

<script defer src="js/vendor/jquery.js"></script>

<script defer src="js/script2.js"></script>

<script defer src="js/script3.js"></script>

deferを持つ全てのスクリプトはページに表示される順にロードされるので、上記の例では jquery.jsscript2.jsscript3.js の前にロードされることと、script2.jsscript3.js の前にロードされることがが保証される。

まとめ

  • パースを待つ必要がなく、依存がなく独立して実行できる場合に、async を使う。
  • パースを待つ必要があり、他のスクリプトに依存している場合は、deferを使ってロードして<script>エレメントをブラウザに実行してほしい順に並べる。

var と let の違い

参照: The difference between var and letEdit/mdn

var hosting

参照:var hosting var と let の違いを調べる前に変数宣言のメカニズムについて言及しておく。

変数宣言は他のコードが実行されるよりも前に行われるため、変数をコードのどこかに宣言することはそれを一番上に宣言するのと同じである。これは次のことを意味する:変数は宣言する前に現れて使用されるのである。この挙動を hosting という。変数の宣言が関数やグローバルコードの一番上に移動させらるからである。

bla = 2;
var bla;

// ...is implicitly understood as:

var bla;
bla = 2;

このような理由で、変数は常にスコープ内の一番上(グローバルコードや関数の一番上)に宣言して、どの変数が関数のスコープ内(ローカル)かということとスコープチェインで解決されるかをはっきりさせておくことが推奨されている。

ここで、hosting は変数宣言には影響はあるが、値の初期化には影響しないことを指摘しておく。値は割り当て指示に到達した地点で実際に行われる。

function do_something() {
  console.log(bar); // undefined
  var bar = 111;
  console.log(bar); // 111
}

// ...is implicitly understood as:

function do_something() {
  var bar;
  console.log(bar); // undefined
  bar = 111;
  console.log(bar); // 111
}

参照:Temporal dead zoneSection

letvar 同様にhosting されるが、変数が評価されるまで初期化されない。初期化される前に変数にアクセスするとReferenceError を起こす。これを Temporal Dead Zone という(以下ではTDZ)という。

function do_something() {
  console.log(bar); // undefined
  console.log(foo); // ReferenceError
  var bar = 1;
  let foo = 2;
}
スコープルール

参照: Scoping rules

let で宣言された変数は宣言されたブロックの中にスコープを持ち、そのスコープが包含する全ての子ブロックにも同じスコープに属する。次の例での、letvar の大きな違いは var のスコープはそれを閉じた関数内のすべてにわたっているといことである。

function varTest() {
  var x = 1;
  if (true) {
    var x = 2;  // same variable!
    console.log("First value in let",x);  // 2
  }
  console.log("Second value in let",x);  // 2
}

function letTest() {
  let x = 1;
  if (true) {
    let x = 2;  // different variable
    console.log("First value in let:",x);  // 2
  }
  console.log("Seconde value in let:" ,x);  // 1
}

console.log(varTest())
console.log(letTest())
The temporal dead zone and typeof

参照: The temporal dead zone and typeof

単に宣言されていない変数と undefined を値としてもつ変数と違って、TDZの変数の型をチェックするために typeof演算子を使うと、ReferenceError が起こる。

// prints out 'undefined'
console.log(typeof undeclaredVariable);
// results in a 'ReferenceError'
console.log(typeof i);
let i = 10;
Redeclarations

参照:Redeclarations

同じ関数やブロック内のスコープ内で同じ変数を再び宣言すると、SyntaxError が起こる。

if (x) {
  let foo;
  let foo; // SyntaxError thrown.
}

switch 文でも起こる。

switch(x) {
  case 0:
    let foo;
    break;
    
  case 1:
    let foo; // SyntaxError for redeclaration.
    break;
}

しかし、上記は case クロージャーのなかにブロックをネストさせて新しくスコープされたレキシカル環境を作れば回避できる。

let x = 1;

switch(x) {
  case 0: {
    let foo;
    break;
  }  
  case 1: {
    let foo;
    break;
  }
}