JavaScript
es6
es2015

Destructuring AssignmentでReferenceError: can't access lexical declaration before initialization

More than 3 years have passed since last update.

とあるサイトのスクレイピング用コードをWebコンソールで書いていた際、分割代入(Destructuring Assignment)を使うようコードを変更すると、件名のエラーが発生するという事象に遭遇しました。サクッと原因がわからず1時間ほど悩んだので、記事を書いておきます。


処理内容

書いていたコードは以下のようなoriginalからexpectedを得るための変換処理です。

var original = [['key1', 'value1'], ['key2', 'value2-1'], ['', 'value2-2'], ['', 'value2-3'], ['key3', 'value3']]

var expected = [['key1', 'value1'], ['key2', ['value2-1', 'value2-2', 'value2-3']], ['key3', 'value3']]


分割代入導入前

分割代入を使わず、意図した結果を得ることができていたコードです。

original.reduce(toExpected, [])

// -> [["key1", "value1"], ["key2", ["value2-1", "value2-2", "value2-3"]], ["key3", "value3"]]

function toExpected(result, row) {
var key = row[0]
var value = row[1]

if (!key.length) {
var last = result.pop()
var lastKey = last[0]
var lastValue = last[1]
key = lastKey
value = appendValue(lastValue, value)
}

result.push([key, value])
return result
}

keyvalueで代入文がそれぞれ2つずつあって冗長ですよね。意図もわかりにくいです。ifブロックの中をextractすることも考えたのですが、FirefoxだけでなくChromeもバージョン49から分割代入がサポートされたので、まずは分割代入を使って読みやすくすることにしました。


分割代入導入後 その1

分割代入を使うように変更したコードです。

original.reduce(toExpected, [])

// -> (Firefox) ReferenceError: can't access lexical declaration `lastKey' before initialization
// -> (Chrome) ReferenceError: lastKey is not defined(…)

function toExpected(result, row) {
var [key, value] = [row[0], row[1]]

if (!key.length) {
let [lastKey, lastValue] = result.pop()
[key, value] = [lastKey, appendValue(lastValue, value)]// <-- ここの右辺評価次にエラーが発生
}

result.push([key, value])
return result
}

先ほどのコードと比べるとかなり読みやすくなりましたよね? でもReferenceError: can't access lexical declaration `lastKey' before initializationというエラーが発生するようになってしましました。lastKeyはちゃんと宣言してるし初期化もしてるしどういうことなんでしょう? ブロック内のvarをついでにletに変更したのがいけなかったのかな? などと考え、とりあえずletvarに戻してみました。


分割代入使用後 その2

先ほどのコードのletvarに変更したコードです。

original.reduce(toExpected, [])

// -> [["key1", "value1"], ["", "value2-3"], ["key3", "value3"]]

function toExpected(result, row) {
var [key, value] = [row[0], row[1]]

if (!key.length) {
var [lastKey, lastValue] = result.pop()// <-- ここを変更
[key, value] = [lastKey, appendValue(lastValue, value)]
}

result.push([key, value])
return result
}

今度はエラーは発生しなくなりましたが、結果が期待しているものではなくなってしまいました。最悪ですね。letを使おうがvarを使おうが、エラー発生行の分割代入をやめると問題が解消するので、分割代入に何か制限でもあるのかな?なんて考えながらググってみましたが、それらしき情報は何一つ見つからず、悩みました。


原因

何がいけなかったんでしょうか?

OKとNGのケースでの違いは、分割代入のみのように見えます。

//OK

let [lastKey, lastValue] = result.pop()
key = lastKey
value = appendValue(lastValue, value)

//NG
let [lastKey, lastValue] = result.pop()
[key, value] = [lastKey, appendValue(lastValue, value)]

上の2つのコードを見て、原因わかりますか?

私は一晩寝て、翌朝起きた時に自分のやっちゃったことに気が付きました。

原因は、行頭が[で、かつ、直前行のセミコロンが省略されているためです。分割代入は直接は関係ありません。すみません。(よくわからない方は、『Effective JavaScript』のItem 6: Learn the Limits of Semicolon Insertionをどうぞ)

個人用のコードは基本的にセミコロン省略したい派なのですが、(を行頭に書くために;(とすることはあっても、[を行頭に書くために;[としたことは今までになく、すぐに気が付きませんでした。「だからいつもセミコロン書けって言ってるだろ」と罵られそうな案件ですが、ESLintを通せばno-unexpected-multilineで警告されるので宗旨替えの予定はありません。

最終的に下記のように修正して解決しました。めでたしめでたし。(えっ、読みにくい?)

//OK

let [lastKey, lastValue] = result.pop()
;[key, value] = [lastKey, appendValue(lastValue, value)]


まとめ

分割代入を使いはじめると、行頭に[を書くことも増えてくると思いますので、セミコロン省略派の方々は普段から注意しつつ、ESLint的なセーフガードも使って、わかりにくいエラーで悩まないようにしましょう。


参考資料