32. You Don’t Know JS: Types & Grammar Chapter 1: Types

Tatsuya Asami
13 min readAug 1, 2018

--

【所要時間】

2時間48分(2018年7月31日)

【概要】

型について

【要約・学んだこと】

JSのようなdynamic languageはタイプを持たない。

typed languageが好みなら、”type”という単語の使い方に反対するだろう。これらの言語では、typeはJSよりもはるかにたくさんの意味を持つ。

JSはtypeを持つと主張するべきではなく、tagやsubtypeと呼ばれるべきだという人もいる。

ここではおおよその定義として使っていく。タイプは固有で、特定のvalueの動作を一意に識別する特性を持つbuili-inセットであり、他のvalueや、engineとdeveloperどちらとも区別をする。

言い換えると、engineとdeveloperがvalue”42”と43を扱うとき、stringsとnumberとしてそれぞれ扱う。これらは異なるタイプを持つ。

これは完全な定義とは言えないが、JSを表現するのに十分だ。

A Type By Any Other Name…

それぞれのタイプと固有の動作を正しく理解することは、適切に、正確にvalueを異なるタイプに変換する方法を理解するために必要。ほぼ全てのJSプログラムは、いくつかのshapeやformでvalueを強制的に扱う必要がある。

coercionはexplicit(明確)な方法だけではなく、不注意でおかしな方法で発生してしまうことがある。

このcoercionによる混乱は、おそらくJS ディベロッパーにとって最もストレスとなる。そして言語上の欠陥とみなされるほど危険だと非難されてきた。

JSのタイプを完全に理解することで、なぜcoercionの評判が悪いのか説明するのが目的だ。まずはvalueとtypeについてもっと理解しよう。

Built-in Types

JSは7つのbuilt-in typeを定義している。

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol -- added in ES6!

objectを除きprimitiveと呼ばれる。

typeof operatorはvalueがどのtypeを与えられているのかを調べるが、正確に1対1でマッチするわけではない。

null はtypeof operatorと合わせるとバグを引き起こす。

typeof null === "object"; // true

このバグは20年存在し、直る見込みもない。なぜなら多くのwebコンテンツがバグの動作に依存しており、修正することでより多くのバグを作るからだ。

とすると、typeof が返す7番目のstring valueは何か。

typeof function a(){ /* .. */ } === "function"; // true

functionはJSのbuilt-inタイプでトップレベルに位置すると考えるのが簡単で、typeof operatorの動作を与えられている。しかし、もし仕様を読むと、実際にはobjectのsubtypeであることがわかる。具体的には、functionは”callable object”として参照され、内部の[[call]] propertyを持つobjectである、呼び出し可能なobjectと呼ばれる。

functionが実際にobjectであるという事実は有用。最も大事なのは、それらはプロパティを持つことができる。

function a(b,c) {
}
console.log(a.length);
VM436:4 2

このfunction objectのlength propertyは、宣言されている公式parametersの数が設定されている。

bとcというformal named parametersがfunctionに宣言されているので、 “length of function” は2となる。

typeof [1,2,3] === "object"; // true

arrayについては、ただのobjectだ。それらのobjectをsubtypeと考えるのが最も適切で、この場合は数値的にインデックス付けされるという追加の特徴と、自動的に更新された.lengthのプロパティを維持する。

Values as Types

JSではvariableはtypeをもたない。valueがtypeを持つ。 variableはいかなるvalueをいつでも持つことができる。

JSはtype enforcementを持たない。engineはvariableが最初のtypeのvalueと同じtypeを常に保持していると主張しない。variableは、1つのassignment statementではstringを持ち、次ではnumberを持つといったようになる。

value 42はnumberという固有のtypeを持ち、そのtypeは変わらない。”42”のようなstring typeは、coercionと呼ばれるプロセスを通じてnumber value 42から作ることができる。

variableに対してtypeofを使うと、variableのtypeは何かを尋ねない。なぜならJS variableはtypeを持たないからだ。その代わり、 variableのvalueのタイプは何かを尋ねる。

var a = 42;
typeof a; // "number"

a = true;
typeof a; // "boolean"

undefined vs "undeclared"

variableが現在valueを持たない、実際には undefined valueを持つ。そのようなvariableにtypeof を呼び出すと、undefinedを返す。

var a;

typeof a; // "undefined"

var b = 42;
var c;

// later
b = c;

typeof b; // "undefined"
typeof c; // "undefined"

多くのディベロッパーはundefinedとundeclaredを同じように考えようとするが、JSでは全く異なる。

undefined variableはアクセスできるscopeで宣言されたものだが、その時点でvalueがないことだ。
一方undeclared variableはアクセスできるscopeで公式に宣言されていないということだ。

var a;

a; // undefined
b; // ReferenceError: b is not defined

b is not defined と b is undefinedは違う意味だ。さらに

var a;

typeof a; // "undefined"

typeof b; // "undefined"

typeof operator はundeclared variableであってもundefinedを返す。b がundeclared variableでも、typeof b を実行するときにエラーを投げない。これはtypeofの動作の特別なセーフティガードだ。

undeclaredと返してくれた方が混乱はしない。

typeof Undeclared

いうまでもなくこのセーフティガードはブラウザ上のJSを扱うときは有効な特徴で、複数のscript fileがshared global namespaceにvariableをロードできる。

Note:
多くのディベロッパーがglobal namespaceにvariableは置かないべきで、全てmodule, private/separate namespaceに含むべきだと信じている。これは素晴らしい理論だが、実際にはほぼ不可能だ。ES6はmoduleのfirst-class supportを追加し、はるかに実用的にするだろう。

例えばDEBUGと呼ばれるglobal variableによってコントロールされるプログラムがデバッグモードをもつとする。consoleにメッセージをログインすると行ったデバッグタスクが実行される前にvariableが宣言されたかどうかチェックしたい。
top-level global var DEBUG = true 宣言が、 “debug.js” fileにのみ含まれる。そして、開発やテスト時のみブラウザでロードし、プロダクションではしない。

しかし、他のアプリケーションコードで、global DEBUG variableのチェックの仕方を気にする必要があるので、ReerenceErrorを投げない。typeofのセーフティガードはこのような場合に助かる。

// oops, this would throw an error!
if (DEBUG) {
console.log( "Debugging is starting" );
}

// this is a safe existence check
if (typeof DEBUG !== "undefined") {
console.log( "Debugging is starting" );
}

このような種類のチェックは、user-defined variable(DEBUGのような)を扱っていなかったとしても、役に立つ。もしbuil-in APIの特徴チェックしているなら、エラーを投げずにチェックするのを発見するのに役立つ。

if (typeof atob === "undefined") {
atob = function() { /*..*/ };
}

もしまだ特徴にpolyfillと定義していないなら、それを定義し、atob 宣言をするのにvar を使うのを避けたいだろう。もしif statement内に var atobを宣言するなら、たとえif conditionがパス(なぜならglobal atobはすでに存在している。)しなかったとしてもこの宣言はscopeのトップにhoistedされる。
あるブラウザや、特別なglobal built-in variable(しばしばhost objectと呼ぶ)のtypeで、このduplicate declarationはエラーを投げるだろう。varを除くことがこのhoisted 宣言を防ぐ。

safety guardの特徴なしでglobal variableをチェックする他の方法は、全てのglobal variableがglobal objectのpropertyもまた観察することだ。ブラウザでは基本的にwindow objectだ。そのため上記のチェックは安全に終わる。

if (window.DEBUG) {
// ..
}

if (!window.atob) {
// ..
}

undeclared variableの参照とは異なり、存在しないobject property(global window objectでさえ)へのアクセスを試みるとReferenceErrorは投げられない。

一方で、window referenceでglobal variableをマニュアルで参照するのを避けるディベロッパーもいる。特にコードが複数のJS environment(ただのブラウザではなく、例えばserer-side node.js)で動作する必要があり、global objectがいつもwindowと呼ばれるわけではない。

技術的にはもしglobal variableを使っていないなら、このtypeofのセーフティガードは有用で、これらの環境はあまり望ましくない。そしてあるディベロッパーはこの設計を望まずして見つけるだろう。他の人に彼らのプログラムやmoduleにコピペしてもらいたいutility functionを想像して、プログラムがあるvariableを定義したかどうかをチェックしたい。

function doSomethingCool() {
var helper =
(typeof FeatureXYZ !== "undefined") ?
FeatureXYZ :
function() { /*.. default feature ..*/ };

var val = helper();
// ..
}

doSomethingCool() はFeatureXYZというvariableをテストし、もし見つかればそれを使うが、もしなければ自身の変数を使う。もし誰かがそれらのmoduleやプログラムでこのutilityを含むなら、それらがFeatureXYZを定義したかどうか安全にチェックする。

// an IIFE (see "Immediately Invoked Function Expressions"
// discussion in the *Scope & Closures* title of this series)
(function(){
function FeatureXYZ() { /*.. my XYZ feature ..*/ }

// include `doSomethingCool(..)`
function doSomethingCool() {
var helper =
(typeof FeatureXYZ !== "undefined") ?
FeatureXYZ :
function() { /*.. default feature ..*/ };

var val = helper();
// ..
}

doSomethingCool();
})();

ここではFeatureXYZがglobal variableでは全くないが、安全のためにtypeofのセーフティガードをまだ使っている。重要なのは、ここではチェックのために使えるobjectがない(windowを持つglobal variableと同様)ので、typeofは非常に役に立つ。

他のディベロッパーはdependency injectionと呼ばれる設計パターンを好む。doSomethingCool() の代わりにFeatureXYZがその外部、主編で定義されてるかどうかを暗黙的に調べる場合、依存関係を明示的に渡す必要がある。

function doSomethingCool(FeatureXYZ) {
var helper = FeatureXYZ ||
function() { /*.. default feature ..*/ };

var val = helper();
// ..
}

これらの機能をデザインするにはたくさんの選択肢がある。どれも正解でも間違えでもない。それぞれのアプローチには様々なトレードオフがある。しかし、全体として、宣言されていないタイプの安全ガードがより多くの選択肢を与えるのはいいことだ。

Review

JSは7つのbuilt-in typeを持つ。null, undefined, boolean, number, string, object, symbol それらはtypeof operatorで識別することができる。

variableはtypeを持たないが、valueは持つ。これらのtypeはvalueの本質的な動作を定義する。

多くのディベロッパはundefinedとundeclaredはおおよそ同じことだと想定するが、JSではかなり異なる。Undefinedは宣言されたvariableを持つことができるvalueだ。 Undeclaredはvariableはまだ宣言されていないことを意味する。

JSは残念ながらこれら2つの用語を融合し、エラーメッセージだけでなく、どちらの場合でもundefinedのtypeof の value を返す。

しかし、宣言されていないvariableに対して使用された時のtypeofのセーフティガードは特定の場合で役に立つ。

【わからなかったこと】

【感想】

JS以外の言語ではvariableがtypeを持つものもあるということだが、variableがtypeを持つか持たないかで、記述方法が大きく違いそう。

--

--

Tatsuya Asami
Tatsuya Asami

Written by Tatsuya Asami

Front end engineer. React, TypeScript, Three.js

Responses (1)