前回の記事では、レキシカル環境とクロージャについてカイージとともに学んでいきました。後編となるこちらでは、クロージャを利用したプライベート変数(みたいなやつ)と、アロー関数とfunctionでのthisの挙動について書いていこうと思います。
⇩⇩前回の記事はこちらです⇩⇩
今回も、以下の登場人物が出現します。
登場人物
なんか二人増えてますね。原作ファンブチギレのメンバーたちです。
前回のお話も踏まえながら
1発4000円もするパチンコ"ヌーマ"に挑戦した二人。すると、そこには新たな敵、イチジョーと自分たちを裏切ったイシーダのおっちゃんの娘であるイシーダヒロミがいるではありませんか。
そのパチンコ台の仕組みの一部は以下のようになっていましたね。(少し中身が変わっています。)
function Pachinko(money) {
let Kaiji_money = money;
return function (amount) {
function (amount) {
if (Kaiji_money - amount * 4000 <= 0) {
Kaiji_money = 0;
console.log("お金がつきました。補充してください。");
} else {
Kaiji_money -= amount * 4000;
console.log(
amount * 4000 + "円使用しました。残りのお金は: " + Kaiji_money + "円"
);
}
);
};
}
const UsedBall = Pachinko(1090000); //残金を投入
UsedBall(100); //400000円使用しました。残りのお金は: 690000円
ではこの台でどんどん打っていきましょう
UsedBall(100);//[Log] 400000円使用しました。残りのお金は: 690000円
UsedBall(100);//[Log] 400000円使用しました。残りのお金は: 290000円
UsedBall(100);//[Log] お金がつきました。補充してください。
おや、お金がなくなってしまいました、、カイージくんピンチです!
カイージの逆襲
なんと!(テーアイのブラックカードを使って)1000万円を持ったイシーダヒロミが駆けつけてくれました!
しかし、この関数、お金自体をkaiji_money
に加えることができませんね。そこで、戻り値をオブジェクトにして、その中にaddMoney
という関数とuseMoney
の二つ作ってあげればいいんです!
function Pachinko(money) {
let Kaiji_money = money;
return {
usedBall: function (amount) {
if (Kaiji_money - amount * 4000 <= 0) {
Kaiji_money = 0;
console.log("お金がつきました。補充してください。");
} else {
Kaiji_money -= amount * 4000;
console.log(
amount * 4000 + "円使用しました。残りのお金は: " + Kaiji_money + "円"
);
}
},
addMoney: function (amount) {
Kaiji_money += amount;
console.log(
amount + "円追加されました。現在のお金は: " + Kaiji_money + "円"
);
},
};
}
const pachinkoMachine = Pachinko(1090000); //残金を投入
使う時は、以下のようにメソッドとして使います。
pachinkoMachine.usedBall(100); //[Log] 400000円使用しました。残りのお金は: 690000円
pachinkoMachine.usedBall(100); //[Log] 400000円使用しました。残りのお金は: 290000円
pachinkoMachine.usedBall(100); //[Log] お金がつきました。補充してください。
pachinkoMachine.addMoney(10000000);//10000000円追加されました。現在のお金は: 10000000円
お!10000000円追加され、現在のお金は: 10000000円となっているので、うまくできていますね。
しかし、クロージャは関数に保存すると言っていたのに、どうしてaddMoney
はusedBall
にアクセスできたのでしょうか。
なんじゃこりゃ。と思った方はこちらの前半をご覧ください。
前回でも言ったのですが、レキシカル環境の「外側のレキシカル環境」に関数オブジェクトの「スコープ」を設定するので、クロージャとして保存しているのはPachinko
の方でしたよね。
だから、addMoney
でkaiji_money
がもしなかったらPachinko
でkaiji_money
を探すんですね。
前回のお話から、戻り値がオブジェクトになっただけでした。
なぜかトネーガワも参戦
今、カイージくんが頑張ってパチンコを回していますね。
するとここでトネーガワが急に話しかけてきました。
今回は、PachinkoMachine
を、Pachinko
のクロージャとして設定しましたが、以下のようにPachinkoMachine2
としてクロージャを新しく作って、それぞれ打ってみるとどのようになるのでしょうか。
const pachinkoMachine = Pachinko(1090000); //109万円投入
const pachinkoMachine2 = Pachinko(1000000); //100万円投入
pachinkoMachine.usedBall(100);//400000円使用しました。残りのお金は: 690000円
pachinkoMachine.addMoney(1000000);//1000000円追加されました。現在のお金は: 1690000円
pachinkoMachine2.usedBall(100);//400000円使用しました。残りのお金は: 600000円
pachinkoMachine2.addMoney(1000000);//1000000円追加されました。現在のお金は: 1600000円
お、なぜかそれぞれ保存されています。Pachinko
関数は一つだけなのに、、
なかなかにえげつないですね。もう二度と作りたくはありません。
そうです、これをみたらわかる通り、新しくレキシカル環境を作り、別のクロージャとして保存しているのです!
なので、後からアクセスしたとしてもそれぞれの変数はクロージャごとに保存されているのです!!
(今回はわかりやすいようにPachinko2
としていますが、Pachinko(インスタンスは100万)
のような表記の方が相応しいと思われます。)
また、何を動作をしていない時でもみれるような関数getMoney
も作っておきましょう。後で役に立ちます。
function Pachinko(money) {
let Kaiji_money = money;
return {
usedBall: function (amount) {
if (Kaiji_money - amount * 4000 <= 0) {
Kaiji_money = 0;
console.log("お金がつきました。補充してください。");
} else {
Kaiji_money -= amount * 4000;
console.log(
amount * 4000 + "円使用しました。残りのお金は: " + Kaiji_money + "円"
);
}
},
addMoney: function (amount) {
Kaiji_money += amount;
console.log(
amount + "円追加されました。現在のお金は: " + Kaiji_money + "円"
);
},
getMoney: function () {
console.log("現在のお金は: " + Kaiji_money + "円");
return Kaiji_money;
},
};
}
const pachinkoMachine = Pachinko(1090000); //残金を投入
pachinkoMachine.getMoney();//現在のお金は: 1090000円
うまく動作していますね。
そして、、なんとカイージらはパチンコに勝ち、それぞれ2億円ずつ持ち帰ることにしました。
Eカード:トネーガワとの最終決戦
2億を手にしたカイージ、そんな時、トネーガワからその2億を賭けないかと話を持ちかけられました。もちろんEカードです。
多分原作を読んだことない方々はここまで来てもう飽きてギブアップしてると思いますが、一応Eカードについて解説しておきます。
皇帝側(インペラーサイド)と奴隷側(スレイブサイド)の2つの立場に分かれてプレイします。各プレイヤーは、それぞれ異なる種類のカードを持っています。
カードの選択と配置:各プレイヤーは、自分のデッキから1枚のカードを選び、同時に裏向きで場に出します。
両プレイヤーがカードを裏返して公開し、勝敗を決定します。
その時の勝敗は以下のように決まります。
皇帝 vs 市民:皇帝の勝ち 市民 vs 奴隷:市民の勝ち
奴隷 vs 皇帝:奴隷の勝ち 市民 vs 市民:引き分け
この勝負が決まるまで繰り返し続けます。
やってみないとわからないので、まずは試してみましょう。
Resultを押して、0.5x(倍率)にすると見やすいです。
See the Pen Untitled by shimaf4979 (@shimaf4979) on CodePen.
これのJavaScriptはどうなっているのでしょうか。
見てみましょう。(今回は、手札の保持とどちらが勝利するかのコードの説明のみで、DOM等には触れません。あらかじめご了承ください。)
function createECardGame() {
let player1Deck = { emperor: 1, citizen: 4 };
let player2Deck = { slave: 1, citizen: 4 };
return {
drawCard: function (player, card) {
if (player === 1) {
if (card === "emperor" && player1Deck.emperor > 0) {
player1Deck.emperor--;
return "emperor";
} else if (card === "citizen" && player1Deck.citizen > 0) {
player1Deck.citizen--;
return "citizen";
}
} else if (player === 2) {
if (card === "slave" && player2Deck.slave > 0) {
player2Deck.slave--;
return "slave";
} else if (card === "citizen" && player2Deck.citizen > 0) {
player2Deck.citizen--;
return "citizen";
}
}
return null;
},
playRound: function (player1Card, player2Card) {
if (player1Card === "emperor" && player2Card === "slave") {
return "Player 2 wins";
} else if (player1Card === "slave" && player2Card === "emperor") {
return "Player 1 wins";
} else if (player1Card === "emperor" && player2Card === "citizen") {
return "Player 1 wins";
} else if (player1Card === "citizen" && player2Card === "slave") {
return "Player 2 wins";
} else if (player1Card === player2Card) {
return "Draw";
} else {
return "error";
}
},
getCardCount: function () {
return {
player1: { ...player1Deck },
player2: { ...player2Deck },
};
},
};
}
このゲームの始め方は以下のような感じです。
// Eカードゲームのインスタンスを作成
const eCardGame = createECardGame();
// カードを引く
const player1Card1 = eCardGame.drawCard(1, "citizen");
const player2Card1 = eCardGame.drawCard(2, "citizen");
// ラウンドをプレイ
const result1 = eCardGame.playRound(player1Card1, player2Card1);
console.log(result1); //Draw
// カードの残り枚数を確認
console.log(eCardGame.getCardCount());
//{player1: {emperor: 1, citizen: 3}, player2: {slave: 1, citizen: 3}}
const player1Card2 = eCardGame.drawCard(1, "emperor");
const player2Card2 = eCardGame.drawCard(2, "slave");
const result2 = eCardGame.playRound(player1Card2, player2Card2);
console.log(result2); // 'Player 2 wins'
// カードの残り枚数を確認
console.log(eCardGame.getCardCount());
//{player1: {emperor: 0, citizen: 3}, player2: {slave: 0, citizen: 3}}
図でも書いておきます。
今回は前回と違い、引数が二つあることでわかりずらいように見えますが、やっていることは同じですね。(anonymousではないのは処理中だからということで見逃してください。)
ここで、トネーガワがズルを仕掛けてきました。
なんと卑怯な、これでは負けかねません。
eCardGame.player1Deck.emperor = 0;
すると、以下のような結果になりました。
ypeError: undefined is not an object (evaluating 'eCardGame.player1Deck.emperor = 0')
どうしてでしょうか。
上で、player1Deck
とplayer2Deck
というデータは、createECardGame 関数のスコープ内に閉じ込められています。これらのデッキは関数外から直接アクセスすることはできないのです!
また、getCardCount
、drawCard
、playRound
という内部関数も同様に関数のスコープ内に閉じ込められており、外部から直接呼び出すことはできません。
しかし、内容を知りたい時もありますよね、そういう時のためにgetCardCount
があるのです。
こうして、データとそれに関連する操作を一つの単位(オブジェクト)にまとめ、外部からの不正なアクセスを防ぐことを、カプセル化といい、その時に使われる外部からの参照を禁止する変数をプライベート変数といいます。
クロージャの中にクロージャ(ネストクロージャ)
今回は以上となります。やっと次回、thisの挙動についてかけそうです。
ここまでどうしてレキシカル環境について説明してきたか、その理由がわかると思います。