JavaScriptを触っていてレキシカル環境やクロージャでつまずいた人も多いはず。
そんなあなた(私)のために借金返済に追われるカイージくんと共に勉強していきましょう。
概要から話していくので、わからないとこがあってもスラーと読み飛ばしていただけると助かります。
登場人物
どこかでみたことがある方々ですね。物語始まってすらないのにすでに借金が385万あってかわいそうに。
後から数人増えます。多分。
関数の使い方
本題に入る前に、数点確認しておきましょう。関数といえば、function
を使った関数宣言、関数式とアロー関数の3種類がすぐ思いつきますね。
const func1 = function () {}; //無名関数での関数式
const func2 = function func2_new() {};//有名関数での関数式
function func3() {} //関数宣言
const func4 = () => {};//アロー関数
利用するときは、
func1();
const func5=func4();//戻り値を呼ぶ(クロージャで説明)
func5();
const func6=func4;//これはただコピーしているだけです。
これらは再利用できる関数であるので、それぞれの中身は基本的には保存できません。(クロージャなどの考えを除いて)
そこで、オブジェクトなどと組み合わせられることが多いです。
const kaiiji = {//カイ-ジくんというオブジェクト
age: "21",
money: -3850000,//すでに借金、、
work: function (hours) {
let money = hours * 900;
console.log(money);
},
};
kaiji.work(3);//->2700円分働いた!
カイージくん、頑張って三時間働き、2700円の賃金を獲得しました。
しかし、これでは働いたお金がmoney
に入ってきません!もしやテーアイグループにむしり取られたのか!
流石に可哀想なので、money
にアクセスして貯金させましょう。
以下のように戻り値を作ってあげることで貯金に足してみましょう。
const kaiji = {
age: "21",
money: -3850000,
work: function (hours) {
let money = hours * 900;
console.log(money);
return money;//Workの戻り値
},
};
const earn1 = kaiji.work(3);//2700円
kaiji.money += earn1;//貯金に回した
console.log(kaiji.money);//->-3847300円
なかなか面倒ですよね。働いたらそのまま貯金に回したいなら、オブジェクトで完結したいですよね。
そんな時は、オブジェクトのプロパティにアクセスして実行してみましょう。
const kaiji = {
age: "21",
money: -3850000,
work: function (hours) {
kaiji.money += 900 * hours;//moneyにアクセス
},
};
kaiji.work(3);//メソッドは実行するだけ。
console.log(kaiji.money);
しかし、メソッドの中でそのままプロパティに触ってしまうと、よくない事が起こる事があります。
トネーガワの地下帝国行き
はじめ、トネーガワは時給100000円の仕事をしていました。
const Tonegawa = {
age: "50",
money: 10000000,//貯金多くね
Work: function (hours) {
Tonegawa.money += 100000 * hours;
},
};
Tonegawa.Work(3);
console.log(Tonegawa.money); //->10300000円。さすが。
しかし、地下落ちしてしまったトネーガワはこの仕事ができなくなり、カイジと同等の仕事しかできなくなりました。
なので、トネーガワのworkをカイジと同じものにしました。
const Tonegawa = {
age: "50",
money: 0,//気づけば0円
work: kaiji.work,//カイジのを持ってきました。
};
Tonegawa.work(3);
console.log(Tonegawa.money); //->0円
すると、トネーガワの貯金は0円のままです!どうしてでしょうか!?
そうです、実は上で作ったkaiji.work
は全てカイージにつながってしまっていたのです!!まんまとカイージの罠にハマってしまいましたね。
thisとは
this
とは、指定したオブジェクトの持つプロパティにアクセスできるようにしたものです。これを使うことでそれぞれ自分のmoney
にアクセスできるようになりそうですね。
const kaiji = {
age: "21",
money: -3850000,
work: function (hours) {
this.money += 900 * hours;
},
};
kaiji.work(3);
console.log(kaiji.money);//->3847300円
const Tonegawa = {
age: "50",
money: 0,
work: kaiji.work,
};
Tonegawa.work(3);
console.log(Tonegawa.money); //->2700円
これで一安心ですね。
しかし、このthis
はどのような仕組みになっているのでしょうか。その仕組みを知る前に、まず、紛らわしくなるなるレキシカル環境から勉強する必要があります。
レキシカル環境
ChatGPT
に聞いたところ、以下の返答を得ました。
レキシカル環境(lexical Environment)は変数や関数のスコープと、そのスコープ内の識別子(変数名、関数名など)との関連付けを管理するメカニズムのことで、関数が定義された時点でのスコープチェーンを保持し、その関数がどこで実行されても正しいスコープチェーンにアクセスできるようにします。
↑何だコレ。呪文ばっかりですね。
スコープはfor文とかの解説によく出てくるやつですね。関数やブロック内で変数や関数の識別子を検索する際に使用されます。
スコープチェーンとは、数や関数の参照が外側のスコープへと連鎖的に遡ることができます。つまり、変数がスコープの中で見つからなければ外に外に探しに行こうとします。
カイージの物語に例えるならば、あるゲーム(内側のスコープ)で行動するとき、彼は自分の戦略や手持ちのカード(現在のスコープ)を使いますが、必要に応じて過去の経験や仲間のアドバイス(外側のスコープ)(イシーダのおっちゃんとか)、さらに、それでも解決しない場合、ゲームの基本ルールや共通の知識(グローバルスコープ)にもアクセスします。
しかし、逆に仲間や過去の経験がカイジの現在の手持ちカードや戦術(現在のスコープ)に直接アクセスすることはできません。
コレをお堅いコードで書くとこんな感じです。
let globalVar = 'I am global!';
function outerFunction() {
let outerVar = 'I am outside!';
function innerFunction() {
let innerVar = 'I am inside!';
console.log(innerVar); // 'I am inside!'が表示される
console.log(outerVar); // 'I am outside!'が表示される
console.log(globalVar); // 'I am global!'が表示される
}
innerFunction();
console.log(innerVar); // エラー: innerVarは定義されていません
}
outerFunction();
console.log(outerVar); // エラー: outerVarは定義されていません
今、Global
,InnerFunc
,OuterFunc
の3つのスコープがありましたよね。それぞれ独立した情報を持っているが、繋がっているように感じたと思われます。
そうです、それぞれがどこに繋がっているかや中の情報を表したものがレキシカル環境なのです!
レキシカル環境には二種類があります。まず、全体の文脈を管理するGlobalObject
、これはプログラムの実行開始時に作成される最上位の環境です。
このレキシカル環境にはglobalVar
変数が保存されています。
次に、関数が呼び出されるたびに作成される関数のレキシカル環境があります。これらの環境は、それぞれのスコープ内での変数や関数の定義を追跡し、後からアクセスできるようにします。
ここではまず、function outerFunction() { ... }
が評価され、outerFunction
関数がグローバル環境に追加されます。この時点で、outerFunction
のコードはまだ実行されていません。
ここでは簡潔に書きましたが、レキシカル環境は、環境レコード(Environment Record)と外部環境への参照(Outer Environment Reference)で構成されています。
外部環境への参照(OuterEnv)は、定義された場所につながります。ここでいうGlobalEnv
は最上位なので、GlobalEnv
にそのまま繋がります。
- 環境レコード (Environment Record):
- そのスコープ内で定義された変数や関数を保持します。
- 外部環境への参照 (Outer Environment Reference):
- そのスコープの外側にあるレキシカル環境への参照を保持します。
コレを用いるとレキシカル関数はこんな感じの概要になります。(イメージです。)
ここでいうと、環境レコードがGlobal
オブジェクトの中身であり、outer
の行き先が外部環境への参照になり、以下のようになります。
GlobalEnv
は最上位なので、GlobalEnv
にそのまま繋がりましたね。
次に、関数のレキシカル環境は、関数が実行されるたびに新たに生成され、関数内で宣言された変数やパラメータのスコープを管理します。続きを見ていきましょう。
outerFunction();
が呼び出された時、outerFunction
のレキシカル環境が作成されます。この環境にはouterVar
変数が含まれます。
また、innerFunction
も定義されます。この時点で、innerFunctionのコードはまだ実行されていません。
innerFunction();
が呼び出されると、innerFunction
のレキシカル環境が作成されます。この環境にはinnerVar
変数が含まれます。
さて、いよいよ実行してみましょう。
・まず、console.log(innerVar);
が実行され、"I am inside!"
が表示されます。
・次に、console.log(outerVar);
が実行され、"I am outside!"
が表示されます。outerVar
はinnerFunction
の外側のレキシカル環境(outerFunctionの
レキシカル環境)に存在するためです。
・最後に、console.log(globalVar);
が実行され、"I am global!"
が表示されます。globalVar
はグローバル環境に存在するためです。
実行コンテキストとスコープ
補足として、後から役に立つ、実行コンテキストとスコープについて解説しておきます。実行コンテキスト(Execution Context)は、JavaScriptのコードが実行される環境を定義する概念です。関数が呼び出されるたびに新しい実行コンテキストが作成されます。
また、JavaScriptのスコープは静的であるので、定義された場所を示します。動的でないことに注意しましょう。
今回で言うと、OuterFunc
とInnerFunc
の実行コンテキストとスコープが作成されますね。
ハンーチョーの策略
カイージ、初めての給料をもらいました。すると、同じ部屋にいたハンーチョーから、怪しい話を持ちかけられました。
さて、どんな内容なのか見ていきましょう。
なんと、手持ちのお金を二倍にしてくれると言うのです。なんて優しいのでしょうか。
その作戦のコードを渡されました。見てみましょう。
var money = 10; // カイジが最初に持ってるお金(万ペリカ)
function gamble() {
console.log(money);
}
function newChallenge() {
var money = 20; // なんと、20にしてくれた!?
gamble(); //
}
newChallenge(); //->どうなる??
結果を見てみましょう。
newChallenge(); //->10
あれ、??何も変わっていません。どうしてでしょうか。レキシカル環境とスコープを見てみましょう。
そうです、先ほどもお伝えしたとおり、JavaScriptのスコープは静的であるので、定義された場所を示すので、Gamble
のmoney
はグローバルオブジェクトを示すのですね。
まぁ、ハンーチョーがそんな優しいわけないですよね
1球4000円のパチンコ台『ヌーマ』
トネーガワ、カイージはお金がなくなり、パチンコに挑むことにしました。
しかもなんと一発4000円。カイージは今109万円持っています。(地下のみんなから集めたお金です)
さて、どんなパチンコか見てみましょう。
function Pachinko(money) {
let Kaiji_money = money;
return function (amount) {
Kaiji_money -= amount * 4000;
console.log(
amount * 4000 + "円使用しました。残りのお金は: " + Kaiji_money + "円"
);
};
}
const UsedBall = Pachinko(1090000); //残金を投入
UsedBall(100); //400000円使用しました。残りのお金は: 690000円
関数の戻り値に関数、、なんだか難しいですね。
一つずつ見ていきましょう。
クロージャ
グローバルオブジェクトには変数等は置かれていません。
ここではまず、function Pachinko() { ... }
が評価され、Pachinko
関数がグローバル環境に追加されます。この時点で、Pachinko
関数のコードはまだ実行されていません。
次に、Pachinko関数に10000000を渡して呼び出し、その戻り値である無名関数をkaijiMoneyに代入していきます。
あれ?なんか変なものがくっついていますね。
そういえばUsedBall
を宣言した時、
return function (amount) {
Kaiji_money -= amount * 4000;
console.log(
amount * 4000 + "円使用しました。残りのお金は: " + Kaiji_money + "円"
);
};
ここの
return function (amount) { ... };
のamount
は定義されていませんでしたよね。なので、Pachinko
関数は、function (amount)
という匿名関数を返し、<undefined>
となります。
そしてようやく、パチンコを打っていきましょう。とりあえず100球打ってみましょう。
宣言されている場所はグローバルオブジェクトなのにどうしてPachinko
にアクセスできるのでしょうか。
コレまでのものを見てきたらわかると思いますが、コレが成り立つのは、レキシカル環境の「外側のレキシカル環境」に関数オブジェクトの「スコープ」を設定するからです。
呼び出しが終わると、以下のようにPachinko
の中身だけ変更されて、annonymousFuncは消えてしまっています。
このように、関数が宣言された時の環境を「閉じ込めて」保持することができます。これにより、関数が外部の変数にアクセスし続けることができるのです。
コレをクロージャと呼びます。
もう一度利用してみましょう。
UsedBall(100);
UsedBall(100);
//[Log] 400000円使用しました。残りのお金は: 690000円
//[Log] 400000円使用しました。残りのお金は: 290000円
ひぃえ〜〜!!どんどん溶けていくぅ〜!!脳汁が〜〜!!
少し長くなってしまったので残りは後半に持っていこうと思います。
この続きとして、コレを利用したカプセル化やクラスやオブジェクトに組み込んだ形について書いていこうと思います。
もしレキシカル環境を理解している方は、それがなんの役に立っているのかを"this"で実感できると思います。
参考資料