You Don't Know JS 読書メモ
※ このメモでは JavaScript のことを JS と書く。
YDKJS を読み始めた。
勉強しながら、自分の頭の混乱を整理するための完全な個人メモ。
間違っていること多々ありだから参考にしないように。
ただ、公開しながらメモを書くと自分自身がよりまともに整理が出来る気がするのであえて公開する。
このメモは本で書いて理解したことと、俺の過去の知識をつなげつつ、ごちゃ混ぜにして書いているので、すべてが本に書いてあることと思わないように。
すべて自分理解の為なので、あなたの為になると思って参考にすると痛い目に合うかもしれない。
JavaScript がよく分かっていない
俺は vim-mode-plus を 作って fork してリリースしてメンテしているが、JS を仕事で使ったことがない。
vim-mode-plus は CoffeeScript で書かれているが、CoffeeScript も仕事で使ったことはない。
JS とか Web Tech 全般勉強したいな、とりあえず Atom のパッケージ開発を素材に学ぶか。。。単に本読むだけの勉強だと眠くなるし、、、とりあえず CoffeeScript の方がスッキリかけそうだから CoffeeScript でやるか、、 class 構文もあるし、ということで Atom のパッケージ開発をしている間に、vim-mode-plus の開発にのめり込み今に至る。。
結果として CoffeeScript は大量に書いたし、色々使いこなせるようになったが、依然として、JS の理解は怪しいままだ。
これではいかん。ということで、 YDKJS を読み始めた。
これはその読書メモ。
全体の感想
まだ半分位読んだ時点。
個人的には以下のような著者の強い想いを感じた。
- とにかくあらゆる誤解を解いてやる。
- 特に、OO言語でない JSを必死で OO しようとしても無理だからそのミスアプローチを絶対に指摘してやる。
- 曖昧な説明はしないで、明確に明確に、具体的に具体的に説明する。あなたが、理解不能な事象に出くわした時に、それを "Bad Parts" として避けるのではなく、しっかり理解することで逃げずに済むようにする。
this ってなんだ?
-
this
はオブジェクト自身を指すわけではない。 -
this
は関数自身を指すわけではない。
"this
は〇〇だ" 的な言い方は全部誤解。ウソ。
this
が何を指すかは静的には決まらない。実行時にダイナミックに決まる。
どうやって決まる?呼び出し側(call site
)の呼び出し方によって決まる。
呼び出し方は以下の4パターンがあり、それによって this
が何であるか(何を指すか、何がセットされるか) が決まる。
fun
内で、this
は何を指すか?以下の優先度で決まる。
-
new fun()
: コンストラクタ形式呼び出し.this === 自動的に作られるオブジェクト
. -
fun.call(obj)
orfun.apply(obj)
orfun.bind(obj)()
: 明示的なthis
の束縛。this === obj
-
obj.fun()
: メソッド形式呼び出し.this === obj
. -
fun()
: 単なる関数呼び出し.this === globalスコープ
. この"自動グローバルスコープ解決"はfun
を"use strict"
することで禁止にできる。その場合はthis === undefined
.
優先度?
-
bind
(パターン2) された関数をnew
(パターン1) した時のthis
が何になるか?(new で作られたオブジェクトになる。bind
はコンストラクタ形式で呼ばれた時はbindに指定されたオブジェクトをthis
に設定しないように特別に配慮している) -
bind
(パターン2) された関数を 別のオブジェクトのプロパティにセットして、otherObject.fun()
でメソッド形式で呼び出し(パターン3) た時にthis === otherObject
になる?(=ならない)
みたいな話。
コンストラクタって何だ?
JS には他の OO 言語にあるような"コンストラクタ"は存在しない。
コンストラクタ関数とかもない。
ただ、"コンストラクタとして呼び出す"(construction call of function
) という 呼び出し方 があるだけ。
つまり、呼び出し方で決まるということ。
ではどうやれば"コンストラクタとして呼び出す"ことができるのか? new
operator で関数を呼び出せばそれがコンストラクタ形式の関数呼び出し。
静的にコンストラクタ関数とか決まっているわけではなく、new
つけた時にコンストラクタ呼び出しになる。
すべての関数は new
で呼び出せるんだから、コンストラクタ関数になりえる。
ただ、new
つけて呼び出される事を意図してデザインされている関数はある。
そいういう関数を便宜上コンストラクタ関数と呼ぶんだろう。
そういういわゆる”コンストラクタ関数"は"慣習的に"関数名を大文字で始めるらしい。
例えば array ではなく、Array の様に。
new Dog()
呼び出しの時に起こること
- 全く新しいオブジェクトが作られる
newObj = {}
. - 新しく作られたオブジェクトは
[[Prototype]]
リンクされる。(その関数の prototype が[[Prototype]]
にセットされる,newObj.__proto__ = Dog.prototype
) - 新しく作られたオブジェクトが
this
に自動的に bind される。(this
が 1で作られたオブジェクトを指す様になるということthis = newObj
) - 呼び出しの結果としてこの
this
を自動的に return する。ただし、暗黙にreturn this
が関数の最終行に追記されると考えれば良いか。ただし、明示的にreturn otherObject
みたいにして別オブジェクトを返した場合、それが new の結果になるので注意。あくまでも暗黙のreturn this
は明示的なreturn
が無い時に起こる。なので、明確な意図なしに、コンストラクタ呼び出しを意図した関数内でreturn
書くなと言うこと。
function Dog(name) {
// new operator で呼び出された時、this は勝手に作られたオブジェクトにセットされている。
// さらに、new 呼び出しされた 関数の .prototype が、this( = 新しいオブジェクト) の __proto__ プロパティ([[Prototype]]) にセットされている。
console.log(this.__proto__ === Dog.prototype); // true
this.name = name
// new operator で呼び出された時、最後の行に、暗黙の `return this` があるイメージ
}
Dog.prototype.wan = function () {
console.log("WAN! " + this.name);
}
var dog = new Dog("pochi");
dog.__proto__ === Dog.prototype; // true
dog.wan(); // WAN! pochi
2 の [[Prototype]]
って何?
[[Prototype]]
はどのオブジェクトにも勝手に設定されるプロパティで、値としてオブジェクトが設定されている
オブジェクトはプロパティを参照する時、そのオブジェクト自体にリクエストされたプロパティが存在しない場合、この [[Prototype]]
オブジェクトのプロパティから探す。無かった場合、はさらに [[Prototype]]
オブジェクトの [[Prototype]]
オブジェクトを探す。
見つかった時点で探索は終了だが、見つからない場合、最終的に [[Prototype]]
オブジェクトがなくなるまで探索を続ける。
この特殊な [[Prototype]]
は標準ではないが、多くの実装で __proto__
としてアクセス可能で、実質的な標準と考えて良い。
なので、以下の具体例では __proto__
を使う。
-
obj.prop
にアクセス -
obj
自体はprop
プロパティ持っていない -
obj.__proto__
はある? → ある。じゃあobj.__proto__.prop
はある? → ない! -
obj.__proto__.__proto__
はある? → ある。じゃあobj.__proto__.__proto__.prop
はどう? → ない! -
obj.__proto__.__proto__.__proto__
はある? ない。じゃあない。 - 結果
obj.prop
はundefind
になる。
obj = {}
の様にオブジェクト作った場合、obj.__proto__
は Object.prototype
が設定される。
どうやっても、[[Prototype]]
の自動設定から逃れるすべはないのか!?ある! obj = Object.create(null)
とすればよい。obj.__prot__ === undefined
になる。
こういう "素" のオブジェクトを blank slate といったり、dictionary object と言ったする。
OO 言語ではない。
JSは オブジェクトとプロトタイプ(プロトタイプリンク、プロトタイプチェーン、プロトタイプルックアップ)があるだけだ。
inheritance とか言っても、親から子にプロパティや、メソッドが、コピーされるわけではないぜ、単に2つのオブジェクトがリンクされるだけだ。
クラス→インスタンスの関係ではないぜ。プロトタイプオブジェクト←オブジェクトの逆方向のリンクがあるだけだぜ。
色々OOしようと頑張ってもこの事実からは逃れられんから無理してOO概念を当て嵌めようと頑張ることは事実を歪んで理解させる色眼鏡にしかならんぜ。
とにかくOOの項は正直読んでいてしんどい。著者が、誤解を解くことにやっきになっている感じがする。そのバックグラウンドを共有していない俺としてはモチベーションを共有できていないので、
しんどい。
著者は、OO言語ではないJSに OO言語特有のアプローチをもたらそうという数々の試みと失敗の歴史を見てきたからすごくやっきになってるんだと思う。そしてそれは同じ歴史を見てきた読者には助かる説明なのだろう。
「 色々誤解は生じ得るだろうけれども、複雑さをラップしてシンプルに考えるために、親、子、クラス、インスタンスのメタファーやそれを実現するためのコード上の工夫は有用だから、信じ込みすぎないで、割り切って使う分にはええやろ。」というのが今の時点での俺のスタンス。ES6ではクラス構文もあるし。実際 vim-mode-plus でクラス機構無かったら絶対メンテ出来てないだろうし。
"this & Object Prototypes" を読んでの感想。
このパートはすごく読むのがしんどかった。内容が難しい、というよりも著者が前提とするOO言語の定義を共有できていないため、違和感をずっと感じながらモヤモヤした気持ちで読んでいるからしんどかった。
著者は C++ や Java のクラスを前提としていているが、Ruby や Python の方をむしろ知っている俺としては、著者が"Js は OO言語みたいに継承時にコピーが起こるわけではなく、全部ただ Prototype リンクされるだけだ" みたいに言う時、それって Ruby も似たようなもんじゃないの? などど思ってしまう。と言うようなことがずっと続くのがこのパート。
同じように感じる人は結構いると思う。Issueも 2つ見つけた。
Use of "copy" to describe class-based inheritance · Issue #986 · getify/You-Dont-Know-JS
Class inheritance does not imply copies · Issue #786 · getify/You-Dont-Know-JS
特に大きな違和感として、ES6 で導入された class
構文を、この著者は気に入っていないようで、「結局は深い理解の妨げになるからおすすめしない」というスタンスなのが、強く強く感じられて、そこにどうしても賛同できなかった。
別に賛同できなくても学びがあるから読み続けている訳で、それはそれなんでだけど。
Syntax で複雑なことを簡単に表現できるってすごく重要じゃない?シンタックス、俺は見た目すげー重要だという意見なので。
特に、 Behavior Delegation を利用した(OLOO) と OO の対比がツッコミどころ満載な気がした。別に JS で prototype チェーン使って、クラスの継承を実現してもいいじゃん。結局それも prototype delegation じゃないの?とか。著者が強く対比している OO と OLOO ですが、OOの方があまりに Ruby や PythonでのOOを無視した論調になっている(著者はIssueを見る限り自覚している様)のと、それは別にしても 例示して対比している OLOO でやっていることって、そんなに class 構文でやっていることと違う???みたいな疑問が消えなかった。著者は「大いに違う、OLOOの方が絶対良い」という論調なので、読んでいる間中、違和感がずっと続く感じ。
とにかく著者が JS で class や 継承を実現しようとする試み自体が好きじゃないという印象。JS での OO実現を語る時に、 masquarading
とか、uglier
とか負の修飾語が多すぎて読んでいて毒される。
ES6 のクラス構文を説明する項では最後のまとめでこうも書かれている。
Bottom line: if ES6 class makes it harder to robustly leverage [[Prototype]], and hides the most important nature of the JS object mechanism -- the live delegation links between objects -- shouldn't we see class as creating more troubles than it solves, and just relegate it to an anti-pattern?
(意訳) もし ES6 の class が
[[Prototype]]
をどんどん使うことを難しくし、最も重要なJSのオブジェクト機構の仕組み(オブジェクト間のダイナミックな delegation リンク)を隠すだけなら、class は問題の方が多いから、アンチパターンとして使わないようにすべきじゃない?
ホントにそう?別に class 構文使ってJSのオブジェクト機構はそのまま生きてるし、結局難しいことやる時は難しいやり方でやる必要がある(prototypeさわる)のはそれで良いし、むしろ class というシンタックスができたことで、複雑なオブジェクト間の関係をより簡易に表現できるようになってより、使われるんだし、それは prototype を robustly leverage することにつながるだろう。
いやー、このパートは違和感あった。JS 初心者におすすめじゃないわ。このパートは。
この違和感を英語で表現できるだけの英語力が無いから積極的に Issue にもコメント書けんし。
以下の Issueにとても共感した。特に リンクが貼られている Reddit の ECMAScript editor の人のコメント。"JSのクラスが fake だと言うのは初心者にとって全然助けにならない" みたいな。
そのとおりだと思う。
ECMAScript editors: JS classes are not fake · Issue #836 · getify/You-Dont-Know-JS
bterlson_ comments on Using Classes in Javascript (ES6) — Best practice? - JavaScript - reddit
Scope
JS は Function スコープだけがある。ブロックスコープはない。
ブロックスコープ って何? {}
で囲まれた範囲に変数のスコープが限定されること。
var 宣言無しの、代入は、global スコープでの変数代入になるので注意("use strict"
してればこの自動 global スコープは起こらない。)
hoisting について
関数宣言も変数宣言も hoist される.
hosting は"そう考えると理解しやすい" という理解の助けのための方便でしか無い。
ではなぜ hoisting されているように見えるのか?それは JSがコンパイル型言語だから。インタプリタ型言語の様に、上の行から下の行に順番に解釈、実行、次の行というように実行されるわけではないよ。
var a = 1;
という宣言があった時、var a
はコンパイル時に、= 1
はランタイム時に処理される。
-
var a
:a
をセットするスコープを解決する。(コンパイル時) -
= 1
: スコープに問い合わせ、"Hey スコープ!a
に1
をセットしてくれ"。と言う。(実行時)
つまり、変数スコープはランタイム時には解決されているので、以下は全部効果は同じ。
a = 1;
var a;
var a = 1;
var a;
a = 1;
同様に関数宣言も、関数はコンパイル時に解決されているので、関数がスコープからアクセス可能な範囲で定義されている限りにおいては、
関数の"実行"が宣言よりも先にかかれていても、後に書かれていても関係ない。
以下の例では、インタプリタ型言語として JS を捉えると理解できない。そう捉えてしまうと return innerFunction
以降の行は決して実行されないと思ってしまうから。
コンパイル型言語で、関数宣言はコンパイル時に関数宣言は解決されているので、実行時には innerFunction
は available なんだと理解するのがただしい理解。
function outerFunction() {
var count = 0;
return innerFunction;
function innerFunction() {
count++;
return count;
}
}
var counter = outerFunction();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
console.log(counter()); // 4
console.log(counter()); // 5
ただ、こういう細かいことを説明せずとも、hoisting というイメージを使って説明するとスッキリ理解できるので hoisting という説明が流行っている。
let
ただ、ES6 から let
が導入され ブロックスコープが作られるようになった。
for loop での let
の束縛はループのイテレーション毎に、スコープが作られて新しく bind されるので、イテレーション間でスコープがシェアされることはないので心配しなくて良い。ループのブロック内で実行された setTimeout は実行される時点で、ブロックは終了していて、イテレーションをコントロールする一時変数は最終的なブロック終了時の値になっているので、全部同じカウントを表示しちゃう。なので、昔は function スコープを利用して、IIFE で 変数を守っていたけど、letでそういういうことしなくて良くなるよ。
クロージャとは?
レキシカルスコープとは?
変数のスコープが、静的に決まる。ソースコードを実行しないでもスコープが決まる。
JS はレキシカルスコープ。関数のブロック {}
内が一つのスコープ。関数内で作られた関数はその関数の{}
内で別のスコープを作るが、外側のスコープ(外側の {}
の範囲の変数) にアクセス可能。
この外側のスコープへのアクセスは、定義箇所と離れた場所で実行しても失われない。
function outerFunction() {
var count = 0;
function innerFunction() {
count++;
return count;
}
return innerFunction;
}
var counter = outerFunction();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
console.log(counter()); // 4
console.log(counter()); // 5
上記の例で説明する。
-
outerFunction
のスコープは 外側の{}
の範囲。 -
innerFunction
のスコープは 内側の{}
の範囲。 -
outerFunction
を呼び出すと、innerFunction
が返される。それをcounter
にセットしている。 -
counter()
を以降の行で呼び出す度にインクリメントされた数字が返っていることが分かる。 - これの結果から読み取れることを表現すると
-
outerFunction
の実行が終了した後でも、innerFunction
はouterFunction
のスコープへの参照を保持している -
innerFunction
は自分自身が含まれるスコープへずっとアクセスし続けられる。 -
innerFunction
がアクセス可能なスコープはレキシカルに決まり、(ソースコードの静的解析で特定できる)、アクセス可能なスコープは"どこで、いつ実行されようが"変わらない - 関数は、その関数が定義された時点の外側のスコープへのアクセスが実行タイミングにかかわらず可能
- とかいろんな言い方ができる。
-
ダイナミックスコープとは
実行時にスコープを解決する。実行されてみないとスコープは分からん。どういう順序で実行されるかによっても変数が指すものが変わりえる。