32. You Don’t Know JS: Types & Grammar Chapter 1: Types
【所要時間】
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を持つか持たないかで、記述方法が大きく違いそう。