2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptを触っていてレキシカル環境やクロージャでつまずいた人も多いはず。
そんなあなた(私)のために借金返済に追われるカイージくんと共に勉強していきましょう。
概要から話していくので、わからないとこがあってもスラーと読み飛ばしていただけると助かります。

登場人物

スクリーンショット 2024-07-08 21.17.05.png
どこかでみたことがある方々ですね。物語始まってすらないのにすでに借金が385万あってかわいそうに。

後から数人増えます。多分。

関数の使い方

本題に入る前に、数点確認しておきましょう。関数といえば、functionを使った関数宣言、関数式とアロー関数の3種類がすぐ思いつきますね。

JavaScript
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に入ってきません!もしやテーアイグループにむしり取られたのか!

スクリーンショット 2024-07-08 21.29.06.png

流石に可哀想なので、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円。さすが。

しかし、地下落ちしてしまったトネーガワはこの仕事ができなくなり、カイジと同等の仕事しかできなくなりました。

スクリーンショット 2024-07-08 22.45.57.png

なので、トネーガワのworkをカイジと同じものにしました。

const Tonegawa = {
  age: "50",
  money: 0,//気づけば0円
  work: kaiji.work,//カイジのを持ってきました。
};
Tonegawa.work(3);
console.log(Tonegawa.money); //->0円

すると、トネーガワの貯金は0円のままです!どうしてでしょうか!?

スクリーンショット 2024-07-08 22.50.06.png

そうです、実は上で作った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文とかの解説によく出てくるやつですね。関数やブロック内で変数や関数の識別子を検索する際に使用されます。

スコープチェーンとは、数や関数の参照が外側のスコープへと連鎖的に遡ることができます。つまり、変数がスコープの中で見つからなければ外に外に探しに行こうとします。

カイージの物語に例えるならば、あるゲーム(内側のスコープ)で行動するとき、彼は自分の戦略や手持ちのカード(現在のスコープ)を使いますが、必要に応じて過去の経験や仲間のアドバイス(外側のスコープ)(イシーダのおっちゃんとか)、さらに、それでも解決しない場合、ゲームの基本ルールや共通の知識(グローバルスコープ)にもアクセスします。
スクリーンショット 2024-07-09 0.48.10.png

しかし、逆に仲間や過去の経験がカイジの現在の手持ちカードや戦術(現在のスコープ)に直接アクセスすることはできません。

スクリーンショット 2024-07-09 0.50.31.png


コレをお堅いコードで書くとこんな感じです。

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、これはプログラムの実行開始時に作成される最上位の環境です。
スクリーンショット 2024-07-09 3.07.47.png
このレキシカル環境にはglobalVar変数が保存されています。

次に、関数が呼び出されるたびに作成される関数のレキシカル環境があります。これらの環境は、それぞれのスコープ内での変数や関数の定義を追跡し、後からアクセスできるようにします。

ここではまず、function outerFunction() { ... } が評価され、outerFunction 関数がグローバル環境に追加されます。この時点で、outerFunctionのコードはまだ実行されていません。

スクリーンショット 2024-07-09 10.52.19.png

ここでは簡潔に書きましたが、レキシカル環境は、環境レコード(Environment Record)と外部環境への参照(Outer Environment Reference)で構成されています。

外部環境への参照(OuterEnv)は、定義された場所につながります。ここでいうGlobalEnvは最上位なので、GlobalEnvにそのまま繋がります。

環境レコード (Environment Record):
そのスコープ内で定義された変数や関数を保持します。
外部環境への参照 (Outer Environment Reference):
そのスコープの外側にあるレキシカル環境への参照を保持します。

コレを用いるとレキシカル関数はこんな感じの概要になります。(イメージです。)
スクリーンショット 2024-07-09 21.16.04.png

ここでいうと、環境レコードがGlobalオブジェクトの中身であり、outerの行き先が外部環境への参照になり、以下のようになります。

GlobalEnvは最上位なので、GlobalEnvにそのまま繋がりましたね。

スクリーンショット 2024-07-09 21.20.58.png


次に、関数のレキシカル環境は、関数が実行されるたびに新たに生成され、関数内で宣言された変数やパラメータのスコープを管理します。続きを見ていきましょう。

outerFunction(); が呼び出された時、outerFunctionのレキシカル環境が作成されます。この環境にはouterVar 変数が含まれます。

また、innerFunctionも定義されます。この時点で、innerFunctionのコードはまだ実行されていません。

スクリーンショット 2024-07-09 21.22.53.png

innerFunction(); が呼び出されると、innerFunctionのレキシカル環境が作成されます。この環境にはinnerVar変数が含まれます。

スクリーンショット 2024-07-09 21.23.21.png

さて、いよいよ実行してみましょう。
・まず、console.log(innerVar); が実行され、"I am inside!" が表示されます。

・次に、console.log(outerVar); が実行され、"I am outside!" が表示されます。outerVarinnerFunctionの外側のレキシカル環境(outerFunctionのレキシカル環境)に存在するためです。

・最後に、console.log(globalVar); が実行され、"I am global!" が表示されます。globalVarはグローバル環境に存在するためです。

実行コンテキストとスコープ

補足として、後から役に立つ、実行コンテキストとスコープについて解説しておきます。実行コンテキスト(Execution Context)は、JavaScriptのコードが実行される環境を定義する概念です。関数が呼び出されるたびに新しい実行コンテキストが作成されます。

また、JavaScriptのスコープは静的であるので、定義された場所を示します。動的でないことに注意しましょう。

今回で言うと、OuterFuncInnerFuncの実行コンテキストとスコープが作成されますね。

スクリーンショット 2024-07-09 22.20.05.png

ハンーチョーの策略

カイージ、初めての給料をもらいました。すると、同じ部屋にいたハンーチョーから、怪しい話を持ちかけられました。

さて、どんな内容なのか見ていきましょう。

スクリーンショット 2024-07-09 22.38.13.png

なんと、手持ちのお金を二倍にしてくれると言うのです。なんて優しいのでしょうか。

その作戦のコードを渡されました。見てみましょう。

var money = 10; // カイジが最初に持ってるお金(万ペリカ)

function gamble() {
  console.log(money);
}

function newChallenge() {
  var money = 20; // なんと、20にしてくれた!?
  gamble(); // 
}

newChallenge(); //->どうなる??

結果を見てみましょう。

結果
newChallenge(); //->10

あれ、??何も変わっていません。どうしてでしょうか。レキシカル環境とスコープを見てみましょう。

スクリーンショット 2024-07-09 22.50.14.png

そうです、先ほどもお伝えしたとおり、JavaScriptのスコープは静的であるので、定義された場所を示すので、Gamblemoneyはグローバルオブジェクトを示すのですね。

まぁ、ハンーチョーがそんな優しいわけないですよね

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関数のコードはまだ実行されていません。

スクリーンショット 2024-07-09 21.29.47.png

次に、Pachinko関数に10000000を渡して呼び出し、その戻り値である無名関数をkaijiMoneyに代入していきます。

予想される形はコレですよね。
スクリーンショット 2024-07-09 23.12.16.png

ではみていきましょう。
スクリーンショット 2024-07-09 23.42.43.png

あれ?なんか変なものがくっついていますね。

そういえばUsedBallを宣言した時、

return function (amount) {
  Kaiji_money -= amount * 4000;
  console.log(
    amount * 4000 + "円使用しました。残りのお金は: " + Kaiji_money + ""
  );
};

ここの

return function (amount) { ... };

amountは定義されていませんでしたよね。なので、Pachinko 関数は、function (amount) という匿名関数を返し、<undefined>となります。

そしてようやく、パチンコを打っていきましょう。とりあえず100球打ってみましょう。

スクリーンショット 2024-07-13 18.44.44.png

宣言されている場所はグローバルオブジェクトなのにどうしてPachinkoにアクセスできるのでしょうか。

コレまでのものを見てきたらわかると思いますが、コレが成り立つのは、レキシカル環境の「外側のレキシカル環境」に関数オブジェクトの「スコープ」を設定するからです。

呼び出しが終わると、以下のようにPachinkoの中身だけ変更されて、annonymousFuncは消えてしまっています。
スクリーンショット 2024-07-09 23.46.22.png

このように、関数が宣言された時の環境を「閉じ込めて」保持することができます。これにより、関数が外部の変数にアクセスし続けることができるのです。

コレをクロージャと呼びます。

もう一度利用してみましょう。

UsedBall(100); 
UsedBall(100); 
//[Log] 400000円使用しました。残りのお金は: 690000円 
//[Log] 400000円使用しました。残りのお金は: 290000円 

ひぃえ〜〜!!どんどん溶けていくぅ〜!!脳汁が〜〜!!

少し長くなってしまったので残りは後半に持っていこうと思います。
この続きとして、コレを利用したカプセル化やクラスやオブジェクトに組み込んだ形について書いていこうと思います。

もしレキシカル環境を理解している方は、それがなんの役に立っているのかを"this"で実感できると思います。

参考資料

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?