こんばんは。@Esperna です。
今回はJavascriptのおける変数の巻上げ(hoisting)について書きます。
背景
私はファームウェアのエンジニアですが、Webのことをもっと知りたいので、OAuth徹底入門という本を読んでいます。
この本の5章ではシンプルなOAuthの認可サーバーの構築を行うのですが、
その中でクライアントの認証部分をBasic認証のAuthorizationヘッダーを使って認証している下記コードがありました。これを最初見たときは変数clientIdのスコープがどうなっているのかが私には分かりませんでした。
app.post("/token", function (req, res) {
var auth = req.headers["authorization"];
if (auth) {
var clientCredentials = decodeClientCredentials(auth);
var clientId = clientCredentials.id;
var clientSecret = clientCredentials.secret;
}
if (req.body.client_id) {
if (clientId) {
res.status(401).json({ error: "invalid_client" });
return;
}
var clientId = req.body.client_id;
var clientSecret = req.body.client_secret;
}
var client = getClient(clientId);
//以下略...
私はC++やC言語に触れている期間が長かったので、
最初、変数clientIdのスコープをC++やC言語での変数のスコープと同様に考えてしまいました。
つまり、if(auth)節内で定義されたclientId はif(auth)内でしか有効でないように見えてしまい、authが値を持っている場合でも、一番最後の行のgetClientの引数はundefinedになってしまう?と誤解し、混乱してしまったのです。
Javascriptにおける変数の巻き上げ
これを理解するにはJavascriptにおける、varによる変数の宣言と代入、そして変数の巻き上げを理解する必要があります。
Javascript Primerには以下の記述があります。
varによる変数宣言は、宣言部分が暗黙的に最も近い関数またはグローバル変数の先頭に巻き上げられ、代入部分はそのままの位置に残るという特支種な動作をします。
こちらを踏まえると、先ほどのコードは実際の実行時には、
次のように解釈されて実行されているものと考えられます。
app.post("/token", function (req, res) {
var clientId//宣言部分が暗黙的に最も近い関数またはグローバル変数の先頭に巻き上げられる
var auth = req.headers["authorization"];
if (auth) {
var clientCredentials = decodeClientCredentials(auth);
clientId = clientCredentials.id;//変数への代入はそのままの位置に残る
var clientSecret = clientCredentials.secret;
}
if (req.body.client_id) {
if (clientId) {
res.status(401).json({ error: "invalid_client" });
return;
}
clientId = req.body.client_id;//変数への代入はそのままの位置に残る
var clientSecret = req.body.client_secret;
}
var client = getClient(clientId);
この変数の宣言が最も近い関数またはグローバルスコープの先頭に移動しているように見える動作のことを変数の巻き上げ(Hoisting)と呼びます。
このHoistingの動作は非常に分かりにくいので、letで以下のように書いても良いのかなと思っています。
app.post("/token", function (req, res) {
let clientCredentials = null;
let clientId = null;
let clientSecret = null;
let auth = req.headers["authorization"];
if (auth) {
clientCredentials = decodeClientCredentials(auth);
clientId = clientCredentials.id;//変数への代入はそのままの位置に残る
clientSecret = clientCredentials.secret;
}
if (req.body.client_id) {
if (clientId) {
res.status(401).json({ error: "invalid_client" });
return;
}
clientId = req.body.client_id;
clientSecret = req.body.client_secret;
}
let client = getClient(clientId);
ちなみにletでは変数を宣言する前にその変数を参照すると、 ReferenceErrorの例外が発生します。
なので、明示的に初期化します。
結論
letを使うと、初期化し忘れに気づきやすくなりますし、同じ変数を誤って二度宣言することも防げますし、varを使わずに済むなら極力letを使うのが安全だなと思いました。varが言語仕様から無くならないのは後方互換性のためだそうです。