Posted at

今度こそ理解できる! JavaScriptの参照が完全に分かる記事


仕事でJavaScriptを書くワイ

const arr1 = [1, 2, 3];

// (中略)

const arr2 = arr1;
arr2.push(4);

// (中略)

console.log(arr1); // [1, 2, 3, 4]

ワイ「何や、配列を書き換えたら別の変数に入れた配列も一緒に変化したで! JavaScriptのバグやな!」

上司「バグってんのはお前の頭やろ! もう一回勉強し直さんかい!」

※この記事は全編やめ太郎さんリスペクトでお送りする2次創作的な記事です。


参照について調べるワイ

ワイ「なんや調べたら参照がどうのとか言ってる記事が出てきよるで……。よく分からんから周りの人に聞いてみよか」

同僚「参照ってのはオブジェクトのことやで(大嘘)」

上司「JavaScriptの関数引数は値渡しと参照渡しがあるんや(大嘘)」

ハスケル子「参照っていうのは書き換え可能なレコードのことですよ」1

ワイ「言ってることがバラバラで何の参考にもならんわ!」

ワイ「仕方ないから根気よくネット検索で調べるとするで」


一番頼りになるものは

(3時間後)

ワイ「いくら調べてもクソみたいな解説サイトしか出てこないで……」

ワイ「結局参照ってのは何なんや」

ハリー先輩「お困りのようだな」

ワイ「ハリー先輩! ワイが再就職しても付いてきてくれたハリー先輩じゃないでっか!」

ハリー先輩「こういうときに役立つのが仕様書や」

ハリー先輩「仕様書JavaScriptとは何かを定義する文書で、JavaScriptに関する最も正確な情報源なんや」

ワイ「仕様書がJavaScriptっていう言語を決めとるんやから、間違った情報が載っとったらおかしいっちゅうわけでんな」

ハリー先輩「ちなみに仕様書はJavaScriptのことをECMAScriptって呼んどるけど、実質同じだから気にせんでもええで」

ワイ「ハリー先輩、『JavaScript 仕様書』で検索しても仕様書なんて出てきまへんで」

ハリー先輩「仕様書は英語で書かれとるんやから英語で検索するんや。『JavaScript specification』やな」

ハリー先輩「今の最新版はこれや。仕様書は年に1回発行されるんやけど、これは2018年6月に発行されたES2018やな」


仕様書で参照を探す

ワイ「いや、仕様書談義で忘れるところやったわ」

ワイ「ワイは参照が分からなくて困ってたんでっせ」

ハリー先輩「せやったな、じゃあ仕様書から参照について書いてあるところを探すで。参照はreferenceや」

ハリー先輩「仕様書は左上に検索欄があって検索しやすいようになっとるから活用しいや」

ハリー先輩「あったで」

6.2.4 The Reference Specification Type


A Reference is a resolved name or property binding. A Reference consists of three components, the base value component, the referenced name component, and the Boolean-valued strict reference flag. The base value component is either undefined, an Object, a Boolean, a String, a Symbol, a Number, or an Environment Record. A base value component of undefined indicates that the Reference could not be resolved to a binding. The referenced name component is a String or Symbol value.

A Super Reference is a Reference that is used to represent a name binding that was expressed using the super keyword. A Super Reference has an additional thisValue component, and its base value component will never be an Environment Record.


ワイ「なるほど、完全に理解したで」

ハリー先輩「は?」

ワイ「いや、よその作者さんのキャラクターやからアホキャラにするのは申し訳ないんや」

ハリー先輩「(どこ向いて喋っとるんやこいつ……)」

ハリー先輩「さよか、じゃあ仕事に戻るわ」

ワイ「(仕様書を完全に理解できるのはこの記事の独自設定なんでご理解のほどよろしく頼むで)」

ワイ「なになに、要するに参照変数名とかプロパティ名を表す概念なんやな」

ワイ「代入とかの時にどこを書き換えればええか指し示すのが参照なんや」

ワイ「まずは変数の場合を考えてみるで」

let x = 3, y = 10;

x = y / 2;

console.log(x); // 5

ワイ「これは変数xyを定義してxy / 2を代入するプログラムやな」

ワイ「y / 2y10が入っとるから10 / 2になってそれを計算するから5や」

ワイ「こういう風に変数の中身を持ってきたり式を計算したりすることを評価 (evaluation) と呼ぶで」

ハスケル子「じゃあx = y / 2を評価するとx3y10だから3 = 10 / 2ですね」

ワイ「んなわけあるかい! x = y / 2代入式やからこれは変数xy / 2を代入するっちゅう意味や」

ハスケル子「わかりました、代入演算子=は右だけ評価して左は評価しないから最終的にx = 5になるんですね」

上司「ちゃうで

ワイ「うおっ!(ワイの出番を奪いよって! おとなしく茶でも飲んどけや、タボが!)」

上司「こういうプログラムを考えてみるんや」

const obj = { name: "foo" };

function f() {
return obj;
}

f().name = "bar";

console.log(obj.name); // bar

ワイ「今回の代入式はf().name = "bar"でんな」

上司「f()の返り値はobjやから、これはobj.name"bar"を代入してることになるで」

上司「だから最後のobj.name"bar"や」

ワイ「f()という関数呼び出しを実行するのも評価やねん」

ワイ「つまり=の左もちゃんと評価されとるっちゅうわけやな」

ハスケル子「今度こそ分かりました。=の左のxとかf().name評価した結果が参照なんですね」

ワイ「せや」

ワイ「代入演算子=っちゅうのは、左辺を評価して得た参照右辺を評価して得た値代入する働きをするんや」

上司「x = y / 2はまず左辺のxを評価してxへの参照が得られるで」

ワイ「右辺を評価すると5やから、xへの参照5が代入されることによって変数xの中身が5になるんや」

ハスケル子「f().name = "bar"の場合はf().name評価されてobj.nameへの参照になるんですね」

ワイ「その通り」

ワイ「xみたいに単体の変数はそのままxへの参照に評価されて、obj.nameとかf().nameみたいなプロパティアクセスの式はプロパティへの参照に評価されるで」


変数は常に参照に評価される

ハスケル子「でも変数の評価都合良すぎじゃないですか?」

ワイ「どういうことや?」

ハスケル子「x = y / 2xxへの参照に評価されるんですよね」

ワイ「せやな」

ハスケル子「でもyは中身が10なので10に評価されてますよね」

ハスケル子「xyも変数なのに何で片方は参照で片方は変数の中身に評価されるんですか?」

ワイ「ええ質問やな」

ワイ「実は変数は常にその変数への参照に評価されるんや」

ワイ「仕様書のここに書いてあるで」

12.1.6 Runtime Semantics: Evaluation


IdentifierReference : Identifier

1. Return ? ResolveBinding(StringValue of Identifier).


ワイ「Identifierxとかの変数のことなんやけど」

ワイ「その評価結果は ResolveBinding(StringValue of Identifier) や」

ワイ「ResolveBindingは8.3.2 ResolveBinding (name, [env])に書いてあるで」

ワイ「細かいことは省略するけど、最後にこう書いてあるからまあ参照を返すんやろなあってことが分かるで」


4​. Return ? GetIdentifierReference(env, name, strict).


ハスケル子「つまりy / 2yyへの参照に評価されるんですね」

ワイ「せや」

ワイ「でも次に/の計算をするときは参照やなくて値が必要や」

ワイ「そういうときは参照から値を取り出して値に変換されるで」

ハスケル子「仕様書のここですね」

ワイ「(こいつ……もう仕様書を……)」

6.2.4.8 GetValue (V)



  1. ReturnIfAbrupt(V).

  2. If Type(V) is not Reference, return V.

  3. Let base be GetBase(V).

(後略)


ハスケル子「詳細は省略しますけど、3以降がVが参照のときの処理ですね」

ハスケル子「変数への参照のときは6で、プロパティへの参照のときは5です」

ワイ「せやな(震え声)」

ワイ「まとめとして仕様書の代入式の評価の部分を見てみるで」

12.15.4 Runtime Semantics: Evaluation

ハスケル子「1から6までありますけど、x = y / 2とかf().name = "bar"の処理は1で行われていますね。1の処理はこうなっています」


a. Let lref be the result of evaluating LeftHandSideExpression.

b. ReturnIfAbrupt(lref).

c. Let rref be the result of evaluating AssignmentExpression.

d. Let rval be ? GetValue(rref).

e. (略)

f. Perform ? PutValue(lref, rval).

g. Return rval.


ワイ「まずaで=の左を評価しとるで。bはエラー処理やからまあ気にせんでええわ」

ワイ「そしてcで=の右を評価しとるんや」

ハスケル子「ポイントはdですね。さっき出てきたGetValueを使っています」

ワイ「せや。=の右は評価した後にGetValueによって値に変換されとるで」

ワイ「逆に=の左は値に変換せずに参照のまま使っとるんや」

ハスケル子「実際の代入処理はfで行なわれてるんですね」

ワイ「こういう風に、普通の計算だとほとんどGetValueで参照に変換されてまうけど」

ワイ「代入式とかでは参照のまま使うようになっとるんや」

ワイ「これが全部仕様書を見れば一目瞭然やな! 仕様書さまさまやで!」

ハスケル子「ちなみにPutValueの定義を見れば分かりますけど、=の左を評価しても参照にならなかった場合はfでエラーになるんですね」

ワイ「あとeは省略したけど筆者の別記事で解説しとるから見てってや!」

ワイ「ワイは出てこないけどな!」

3歳娘「C++とか分かる人は、参照の関係は左辺値右辺値をおもいうかべると分かりやすいよ。」

ワイ「うおっ!(C++に詳しそうな既存キャラが居ないからって娘を使うなや!)」

ワイ「(あと筆者もCとかC++には詳しくないけど3歳娘に免じて許してくれや!)」


参照の他の使い道を知る

(数日後)

ハスケル子「やめ太郎さん」

ワイ「(ワイはやめ太郎なんか? それとも今は筆者が違うからうひょなんか? 混乱してきたで)」

ワイ「おう」

ハスケル子「何ですかその人格が2つ同居してそうな顔は」

ハスケル子「参照が役に立つのは代入のときだけなんですか?」

ハリー先輩「そんなことはないで」

ワイ「ハリー先輩、仕事はええんでっか」

ハリー先輩「delete演算子の解説をしたろ思ってな」

ワイ「(delete演算子か……。聞いたことはあるけど使ったこと無いで)」

ハリー先輩「delete演算子を使うコードなんて9割クソコードやからな」

ハリー先輩「闇の深い案件に関わらん限りはなかなかお目にかからないで」

ワイ「(こいつワイの心を……!)」

const obj = { name: "foo" };

console.log(obj.name);

// obj.nameを削除
delete obj.name;

console.log(obj.name); // undefined

ハリー先輩「deleteはオブジェクトのプロパティを削除する演算子や」

ハリー先輩「この例ではobj.nameを削除したからobj.nameが無くなっとるで」

ワイ「delete obj.nameではまずobj.name評価されてobj.nameへの参照になるんやな」

ハリー先輩「そうや」

ハリー先輩「delete参照を見て削除するプロパティを決める」

ハリー先輩「参照どのオブジェクトどのプロパティかって情報を持っとるのがポイントやな」

ワイ「obj.nameへの参照ってことが分かればdelete演算子くんがobjというオブジェクトをいじってnameというプロパティを消せばええって分かるちゅうことや」

ハスケル子「obj.nameに評価して"foo"になってしまうとどのプロパティを削除すればいいか分からない」

ハスケル子「だから参照が必要なんですね」


ワイ「あともう1つ参照の大事な使い道があるで」

ワイ「メソッド呼び出しのときにthisの値を決めるのも参照の役目や」

const cat = {

name: "ねこ",
mew() {
console.log(`${this.name}「にゃーん」`);
}
};

cat.mew(); // ねこ「にゃーん」

ワイ「このコードの動作を説明してみい」

ハスケル子「cat.mew()でメソッドを呼び出しているので、mewの中ではthiscatになるんですね」

ハスケル子「だからthis.name"ねこ"です」

ワイ「せや」

ワイ「cat.mew()という関数呼び出しを評価するときはまずcat.mew評価されるで」

ハスケル子「呼び出そうとしている関数がプロパティへの参照評価されたらメソッド呼び出しになるんですね」

ワイ「繰り返しやけど、プロパティへの参照どのオブジェクトどのプロパティへの参照かって情報を持っとるで」

ワイ「どのオブジェクトの部分がメソッド内のthisに反映されるんや」

3歳娘「参照は変数に入れられるの?」

ワイ「ええ質問やな! さすが我が娘や!」

ハスケル子「(何で会社に居るんですかこの子)」

ワイ「変数とかに入れられるのはや、参照は入れられないで」

const cat = {

name: "ねこ",
mew() {
console.log(`${this.name}「にゃーん」`);
},
};

const mew = cat.mew;
mew(); // エラーが発生(strictモードの場合)

ワイ「cat.mew参照に評価されるけど」

ワイ「変数に入れるときはGetValueでに変換されるんや」

ハスケル子「ということは変数mewの中身はただの関数で自分がどのオブジェクトのプロパティなのかは知らないんですね」

ハスケル子「だからmewを呼び出してもthiscatにならないんですね」

ワイ「せや」

ハリー先輩「参照が役に立つのは大体これくらいやな」

ハスケル子「代入、delete、そしてメソッド呼び出しですね」

ワイ「それ以外の場合は大体参照に変換されて使われるで」

ハスケル子「(この人仕様書を全文検索するのが面倒くさいから大体でごまかしてますね)」


スーパーな参照

(1週間後)

ハスケル子「仕様書のReferenceの説明に気になるところがあるんですけど」

ハスケル子「ここに書いてあるスーパーリファレンスってなんですか?」


A Super Reference is a Reference that is used to represent a name binding that was expressed using the super keyword. A Super Reference has an additional thisValue component, and its base value component will never be an Environment Record.


ワイ「それはスーパーな参照や」

ワイ「スーパーな参照を使うと処理速度が10倍になるで」

ハリー先輩「(息を吐くようにを……)」

ハリー先輩「スーパーリファレンスはsuper.fooのようにsuperのプロパティに対する参照や」

ワイ「superってのはクラスの継承をする時に使えるやつでんな」

ワイ「例を作ったから見てくれや」

class Person {

greet() {
return "こんにちは";
}
}
class Wai extends Person {
greet() {
return super.greet() + "やで";
}
}

const yametaro = new Wai();
yametaro.greet() // "こんにちはやで"

ハリー先輩「Personクラスとそれを継承したWaiクラスを定義しとるな」

ハスケル子「Waigreetメソッドの中でsuper.greetが出てきましたね」

ワイ「せや、super継承元のクラスのメソッドを呼び出したいときに使えるで」

ワイ「WaigreetメソッドはPersongreetメッセージの結果に"やで"を追加するようになっとるんや」

ハリー先輩「まあsuperPerson.prototypeを指し示しとるみたいなもんやな」

ハスケル子「(prototypeって何だろう)」

ワイ「(クラスを使えばprototypeは触らんでもええようになってるから気にせんでもええで)」

ハリー先輩「superの参照は普通の参照とはちょっと違う動作をするで、deleteできなかったりとか」

ワイ「一番大事なのはsuperの参照をメソッド呼び出しに使用した時でんな」

ハリー先輩「せや、呼び出されたメソッド内でのthisは今のthisと同じになるで」

ハスケル子「thissuperになるわけではないのが普通のプロパティへの参照との違いですね」


typeof

ハスケル子「これは余談なんですけど」

ワイ「いきなりワイの席に来て何言ってんねん」

ハスケル子「typeof演算子もオペランドを評価して参照として扱うんです」

ワイ「オペランドってのは演算子に渡される引数みたいなやつのことやで」

const x = 123;

console.log(typeof x); // "number"

ハスケル子「こういう場合はまずオペランドのx評価して参照を得て」

ハスケル子「参照に変換したら123なので結果が"number"になります」

ワイ「なんや参照として扱っとらんやんけ!」

ハスケル子「では次の例を見てください」

console.log(typeof y); // "undefined"

console.log(y); // エラーが発生

ハスケル子「この変数y宣言されていない変数なのでyを取得しようとするとエラーになります」

ハスケル子「でも実はtypeofで調べるだけならエラーにならないんです」

ワイ「これがyを取得しとらん証拠っちゅうわけやな」

ハスケル子「yみたいな存在しない変数は評価すると未解決の参照になるんです」

ハリー先輩「仕様書でいうとここやで」

8.1.2.1 GetIdentifierReference


The abstract operation GetIdentifierReference is called with a Lexical Environment lex, a String name, and a Boolean flag strict. The value of lex may be null. When called, the following steps are performed:



  1. If lex is the value null, then

    a. Return a value of type Reference whose base value component is undefined, whose referenced name component is name, and whose strict reference flag is strict.



1(後略)


ワイ「base value componentがundefinedな参照が未解決の参照ってことやな」

ハスケル子「未解決の参照はを得ようとしたらエラーになるんですけど」

ハスケル子「typeof演算子はオペランドを評価した結果が未解決の参照のときは"undefined"を返すのでエラーにならないんです」

12.5.5.1 Runtime Semantics: Evaluation



  1. Let val be the result of evaluating UnaryExpression.


  2. If Type(val) is Reference, then

    a. If IsUnresolvableReference(val) is true, return "undefined".



  3. Set val to ? GetValue(val).


  4. Return a String according to Table 35.




まとめ

ワイ「なんか仕様書ばっかり読んで疲れたやで」

ハリー先輩「でもJavaScriptの参照の何たるかは分かったやろ?」

ワイ「変数とかプロパティアクセス評価した結果が参照になって、代入式のとか評価に使われてるんやな」

ハスケル子「でも」

ハスケル子「参照って仕様書にしか出てこないですよね? 実務の役に立たないですよね?

ワイ「せやで

社長「記事冒頭の配列の話は何だったんだね?」

const arr1 = [1, 2, 3];

// (中略)

const arr2 = arr1;
arr2.push(4);

// (中略)

console.log(arr1); // [1, 2, 3, 4]

ワイ「あれは釣り餌や

社長「お前明日から来んでええわ」

〜完〜





  1. ハスケル子「書き換え不可能な変数を原則とする関数型言語では、書き換え可能な変数を参照と呼ぶんです。参照があると関数型言語の魅力が台無しですよ」 ワイ「JavaScriptと関係ないやないかい!」