31. You Don’t Know JS: Scope & Closures Chapter 3: Function vs. Block Scope

Tatsuya Asami
29 min readJul 31, 2018

--

【所要時間】

6時間37分(2018年7月30,31日)

【概要】

関数とブロックスコープについて

【要約・学んだこと】

Scope From Functions

JSはfunction-based scopeを持つ。宣言したそれぞれのfunctionはbubbleを作るが、他の構造はscope bubbleを作らない。

ここでfunction scopeとそれの意義を調べて見る。

function foo(a) {
var b = 2;

// some code

function bar() {
// ...
}

// more code

var c = 3;
}

foo(..)のscope bubbleは a, b, c, bar というidentifierを含む。それはscopeのどこに宣言が現れても関係なく、variableかfunctionはscope bubbleに含まれている。

bar(..)は自身のscopeを持つ。global scopeは1つだけidentifierを持ち、fooに付属する。

なぜなら、a, b, c, barは全てfoo(..)のscope bubbleに属し、foo(..)の外側にはアクセスできない。そのため、下記のコードはidentifierがglobal scopeを利用できないので、ReferenceErrortとなる。

bar(); // fails

console.log( a, b, c ); // all 3 fail

しかし、a, b, c, foo, bar すべてのidentifierはfoo(..)の中にアクセスすることが可能で、bar(..)の中も利用できる。(shadow identifierがbarの中で宣言されていないことを想定している)

function scope は全てのvariableがfunctionに属しているという考えを勧め、function全体を利用、再利用することができる。(実際にnestしたscopeでさえ利用できる。)この設計はかなり使いやすく、JS variableの動的性質を全て利用し、必要に応じてさまざまなタイプのvalueを取ることができる。

言い換えると、予防策に気をつけなければ、scope全体に存在するvariableは、予期せぬ落とし穴を導きかねない。

Hiding In Plain Scope

functionを宣言し、その中にコードを加えるというのがfunctionについての伝統的な考え方だ。しかし、逆に考えると、強く有益だ。コードの任意のセクションをとり、その周りのfunction宣言を包み、コードを”hide”する。

実行結果は疑問文のコードの周りにscope bubbleをつくる。それはそのコードのどの宣言(variableかfunction)が事前にscopeを閉じるというよりは、新たなwrapping function scopeを結ぶという意味だ。

なぜhiding variableとfunctionsが有益なテクニックなのか。

scope-based hidingの動機付けには様々な理由がある。それらはソフトウェアデザインの原則である”Principle of Least Privilege(最低特権の原則)”から発生する傾向にあり、また、”Least Authority(最低権限)” か “Least Exposure(最低露出)”とも時々呼ばれる。
この原則はソフトウェアデザイン内で述べる。例えばmodule/objectのAPIなど。何が最低限必要かだけを後悔し、他の全てをhideする。

この原則はvariableとfunctionが含むscopeはどれかの選択まで及ぶ。もし全てのvariableとfunctionがglobal scope内にあるなら、それらはもちろんどのnested scopeにもアクセスできる。しかし、これはコードの適切なしようがそれらのvariableやfunctionへのアクセスを妨げるため、非公開にするべき多くのvariableかfunctionを公開しており、”least”の原則に違反している。

function doSomething(a) {
b = a + doSomethingElse( a * 2 );

console.log( b * 3 );
}

function doSomethingElse(a) {
return a - 1;
}

var b;

doSomething( 2 ); // 15

この場合、b variableと doSomethingElse(..) functionは doSomething(..)がどう働くかの詳細のprivateのようなものである。enclosing scopeに、b と doSomethingElse(..)へのaccessを与えることは、不要なだけではなく、予想していなかった使われ方をされる可能性があり、危険な可能性がある。
これはdoSomething(..)の想定していた予防措置に違反するかもしれない。

適切なデザインはdoSomething(..)scopeの中のプライベート詳細をhideする。

function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}

var b;

b = a + doSomethingElse( a * 2 );

console.log( b * 3 );
}

doSomething( 2 ); // 15

b と doSomethingElse(..)がdoSomething(..)のみにコントロールされる代わりに、外側にアクセスできない。このfunctionalityとend-resultは影響されないが、デザインは詳細プライベートを隠し、よりよいソフトウェアだと通常考えられる。

Collision Avoidance

scope内のhiding variableとfunctionの他の利点は、同じ名前だが、異なる使い方をしようとしている2つのidentifierの意図しない衝突を避けることだ。衝突の結果、予期せずvalueを上書きすることがよくある。

function foo() {
function bar(a) {
i = 3; // changing the `i` in the enclosing scope's for-loop
console.log( a + i );
}

for (var i=0; i<10; i++) {
bar( i * 2 ); // oops, infinite loop ahead!
}
}

foo();

bar(..)内にある i = 3 は、 foo(..) で宣言された i を予期せぬ上書きをする。この場合、 i が固定された3というvalueになり、永遠に <10 となるため、無限ループする結果となる。

bar(..)内のassignmentは、どういうidentifierの名前が選ばれようとも、local variableだと宣言する必要がある。var i =3; は問題を修正する。(そしてiを宣言することが、shadowed variableを作る。)
さらに、var J = 3;といった、完全に違う名前のidentifierを選ぶという手段もある。しかし、ソフトウェアデザインは同じidentifier nameを自然と呼ぶ。そのため、scopeを宣言内部で使うことが、この場合の最適オプションだ。

Global “Namespaces”

variableで特に強い事例は、global scopeで起こる。プログラムにロードされた複数のライブラリが、もし適切にfunctionやvariableの内部にhideしていないなら、互いに簡単に衝突する。

このようなライブラリは通常global scope内で、独自の名前をもつ1つのvariable宣言(大抵はobject宣言)を作る。

このobjectはそのライブラリのために、namespaceとして使われ、そこでは全ての機能の公開が、top-level lexically scopeのidentifierそのものというよりは、そのobject(namespace)のpropertyとして作られる。

var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
// ...
},
doAnotherThing: function() {
// ...
}
};

Module Management

衝突を避ける他の方法に、より新しいmodule アプローチがある。これは様々なdependency(依存)マネージャーを使用する。これらのツールを使用して、ライブラリがglobal scopeにidentifierを追加することはないが、代わりに、dependencyマネージャーのさまざまなメカニズムを使用して、特定のscopeにidentifierを明示的にインポートする必要がある。

これらのツールはlexical scopeルールから除外されたmagic機能を持っていないことに気をつけるべき。それらはここで説明するルールをしようするだけで、identifierが共有scopeに入れられないように強制することができる。そして、プライベートをもつ非衝突型のscopeを保持する代わりに、偶然scopeが衝突することを防ぐ。

この場合、防御的にコードを作成し、dependencyマネージャーが実際にそれらを使うことなく同じ結果を達成することができる。

詳しくは Chapter 5のmodule pattern参照。

Functions As Scopes

コードのsnippetをとることができ、その周りのfunctionを包むことができる。そして囲まれたvalueまたはfunction宣言を、そのfunctionの内部スコープの外部スコープから効果的にhideすることがわかった。

var a = 2;

function foo() { // <-- insert this

var a = 3;
console.log( a ); // 3

} // <-- and this
foo(); // <-- and this

console.log( a ); // 2

このテクニックは機能するが、理想的とは言えない。いくつか問題がある。第一に、named-function foo()を宣言しなくてはならない。それはidentifier named foo自体は、囲まれたscope(ここではglobal)を汚すという意味だ。囲まれたコードが実際に実行されるために、name(foo())によって、functionをはっきりと呼ぶ必要もある。

もしfunctionが名前を必要としなければ、そしてもしfunctionが自動的に実行されることができたら、それがより理想的だ。

var a = 2;

(function foo(){ // <-- insert this

var a = 3;
console.log( a ); // 3

})(); // <-- and this

console.log( a ); // 2

ここで何が起こるかというと

ただのfunction…ではなく、囲まれたfunction statement、 (function… で始まる。些細な違いにおもえるが、これは大きな違いだ。スタンダード宣言としてfunctionを扱う代わりに、function-expressionとしてfunctionが扱われる。

Note:
declaration(宣言)とexpression(式)の最も簡単な違いは、statementのfunctionの単語の位置(ラインではなく明確なstatement)だ。もしfunctionがstatementの最初にあるなら、function declarationだ。そうでなければfunction expressionだ。

function declarationとfunction expressionで見ることができる重要な違いは、名前がidentifierとして結ばれる場所だ。

2つのsnippetを比較すると、最初のsnippetは name foo が囲われたscopeに結ばれ、直接 それをfoo()に呼ぶ。2つ目のsnippetでは、 name foo は囲われたscopeには結ばれていないが、 自身のfunctionの中に結ばれている。

言い換えると、expressionとしての(function foo(){..}) は identifier fooがscopeの外側ではなく、.. が示すscope内のみで検出されることを意味する。name foo をそれ自身の内側にhideすることは、不要に囲われたscpeを汚さないことを意味する。

Anonymous vs. Named

callback parameterとしてfunction expressionをよく知っている。

setTimeout( function(){
console.log("I waited 1 second!");
}, 1000 );

これは anonymous function expressionと呼ばれる。なぜならfunction()… がidentifierに名前を持たないからだ。function expressionはanonymousになることができるが、function declarationは名前を省略できない。JSのグラマーに反する。

anonymous function expressionは素早く、簡単にタイプでき、多くのライブラリーやツールがこの寛容的なコードスタイルを推奨する傾向にある。しかし、いくつか欠点がある。

  1. anonymous functionはstack traceに表示する有用な名前がないため、デバッグがより難しくなる。
  2. 名前がないと、functionがそれを参照する必要がある場合、再帰などのために廃止されたarguments.callee参照が残念ながら要求される。自己参照を必要とする別の例は、event handler functionが起動後に自身を結びたくない場合だ。
  3. anonymous functionsは読みやすく、理解しやすいコードを提供するのに役立つことがおおい名前を省略する。記述的な名前は、問題のコードをself-document化するのに役立つ。

inline function expressionはパワフルで使いやすい。anonymous と namedの問題は、それを損なうものではない。
function expressionに名前を提供することは、これら全ての欠点を効果的に解決できるが、明らかなマイナス面を持たない。いつもfunction expressionが名前を持つのが最善の方法だ。

setTimeout( function timeoutHandler(){ // <-- Look, I have a name!
console.log( "I waited 1 second!" );
}, 1000 );

Invoking Function Expressions Immediately

var a = 2;

(function foo(){

var a = 3;
console.log( a ); // 3

})();

console.log( a ); // 2

()のペアを包むという長所によってexpressionとしてのfunctionを持っているので、もう1つ()を最後に追加し、(function foo(){ .. })()のようにすることで、そのfunctionを実行することができる。最初の()ペアはexpressionのfunctionをつくり、2つ目の()はfunctionを実行する。

これはよくあるパターンで、数年前にコミュニティはこの言葉をIIFE(Immediately Invoked Function Expression)だと認めた。

もちろんIIFEは名前を必要としない。IIFEの最も一般的な形は、anonymous function expressionに使うことだ。IIFEに名前をつけることは、anonymous function expression よりも前述の利点があるため、採用することを推奨する。

var a = 2;

(function IIFE(){

var a = 3;
console.log( a ); // 3

})();

console.log( a ); // 2

従来のIIFEフォームには、いくつかのバリエーションがある。(function(){ .. }()) 最初のフォームでは、function expressionは()で囲われていて、invoking()のペアがそのすぐ後の外側にある。
2つ目のフォームは、invoking() ペアが、外側の()で囲われたペアの内側に写っている。

2番目の記述は下記のようになるはず。

var a =2;
function IIFE() {
var a = 3;
console.log( a );
};
undefined
var a =2;
(function IIFE() {
var a = 3;
console.log( a );
}());

これら2つのフォームは同じ機能だ。これは好ましい文体的な選択だ。

かなり一般的なIIFEの他のバリエーションは、実際には単にfunctionを呼び、argument(引数)を渡すという事実を使うだけだ。

 a = 2;

(function IIFE( global ){

var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2

})( window );

console.log( a ); // 2

window object referenceに渡すが、parameterをglobalと名付け、global 参照とnon-global参照の明確な文体的描写ができる。もちろん望んでいる包まれたscopeから何かを渡すことができ、parameterの名前は相応しいものならなんでも構わない。これは主に文体の選択だ。

このパターンの他の応用は、デフォルトのundefined identifierが謝ってvalueを上書きし、予期せぬ結果を引き起こすという懸念に対処する。

undefinedとparameterを名付けることで、argumentにvalueを渡すことなく、undefined identifierが実際にコードブロック内の未定義のvalueであることを保証することができる。

undefined = true; // setting a land-mine for other code! avoid!

(function IIFE( undefined ){

var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}

})();

IIFEの別のバリエーションは、実行するfunctionが呼び出された後2番目に与えられるものと、それに渡すパラメータの順序を逆にする。このパターンはUMD(Universal Module Definition)プロジェクトで使われる。少し長いが、理解をしやすい人がいる。

var a = 2;

(function IIFE( def ){
def( window );
})(function def( global ){

var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2

});

def function expressionはsnippetの後半で定義され、snippet前半で定義されたIIFE functionに、パラメーター(別名def)として渡される。

Blocks As Scopes

functionはscopeで最も一般的なユニットであり、流通している大部分のJSの設計アプローチも最も広まっているので、他のscopeのユニットや、他のscopeのユニットの使い方は、よりよく、綺麗に維持できるようになる。

JS以外の言語はblock scopeをサポートしていて、それらの言語を使っているディベロッパーはその考えに慣れている一方で、JSだけで働いている人は、コンセプトの違いを感じる。

しかし、たとえblock-scoped fashionのコードを1行も書いたことがなくても、JSで最も一般的なイディオムをおそらく知っているだろう。

for (var i=0; i<10; i++) {
console.log( i );
}

for-loop headの中に直接 i variableを宣言する。なぜなら i をfor-loopコンテクストの中だけで使うという意図があるからで、variableが実際に囲われたscope(functionまたはglobal)にscopeしているという事実を無視している。

可能な限り近く、ローカルにvariableをそれらが使われる場所で宣言する。

var foo = true;

if (foo) {
var bar = foo * 2;
bar = something( bar );
console.log( bar );
}

bar variableはif-statementのコンテクスト内のみで使用している。そのため、if-blockの中で宣言することに、一種の意味がある。しかし、variableをどこで宣言するかは、いつvar を使うかと関係がない。なぜなら、それらはいつも囲われたscopeに属しているからだ。このsnippetは本質的にに”fake” block-scopingで、文体上の理由から、そのscope内の別の場所で誤って ba を使わないために、自己強制に頼っている。

block scopeはfunctionのhiding informationから前者の”Principle of Least Exposure”に手を広げるツールで、コードのblockのhiding informationだ。

for (var i=0; i<10; i++) {
console.log( i );
}

なぜ for-loopでのみ使われる予定の i variableをもつfunction scope全体が汚すのか。

もっと重要なことに、ディベロッパーは、意図した目的意外でvariableを誤って使うことをチェックすることを好む。例えばもし間違った場所でそれを使おうとすると、未知のvariableについてのエラーが発行される。i variableのためのblock-scopingは、for-loopのためだけに利用でき、もし i が他のfunctionからアクセスすると、エラーを起こす。これはvariableが混乱しやすい、もしくは維持するのが珍しいときには使われないようにするのに役立つ。

しかし、JSはblock scopeの機能は持っていない。

with

構造に悩まされるが、block scopeの形態の一例で、objectから作られたscopeは、with statementの生存中だけ存在し、囲われたscopeの中に入らない。

try/catch

ES3のJSがtry/catch clauseでvariable 宣言を指定して、catch blockにblock scopeを設定するという事実はほぼ知られていない。

try {
undefined(); // illegal operation to force an exception!
}
catch (err) {
console.log( err ); // works!
}

console.log( err ); // ReferenceError: `err` not found

err はcatch clauseの中にのみ存在し、他の場所で参照しようとするとエラーを投げる。

Note:
この挙動が指定され、すべての通常のJS環境(古いIEを除く)で実際にはあてはまるが、もし同じscopeに同じidentifier名でエラーvariableを宣言する2つ以上のcatch clauseがある時、多くのlinterは不平を言う。これは実際には再定義ではない。なぜならvaiableが安全にblock-scopeされるが、linterはこの事実に不平をいうようだ。

これらを避けるために、いくつかのdevにはcatchにvariable err1, err2と名付ける。他のdevはvariable名が重複していないか調べるためにlintingをオフにする。

catchのblock-scopingの性質は、学術的な事実で無駄に感じるが、どれだけ有用かは Appendix B を参照。

let

これまでの特徴を見ると、JSのblock scope機能はおかしな挙動が多い。

ES6はそれを変え、letというvariable宣言の別の方法として、varと並んでいるキーワードを導入する。

let キーワードはvariable宣言をそれが含まれているいかなるblock(通常 {..}ペア)scopeに付加する。言い換えると、 let はvariable宣言のために、blockのscopeを暗にハイジャックする。

var foo = true;

if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}

console.log( bar ); // ReferenceError

letを使い、variableを既存のblockにつけるのは、いくらか暗黙的だ。もしどのblockにvariableがscopeされているかに注意しなければ、混乱する可能性がある。そしてblockの周りに動く癖があり、他のblock内で包むといった、コードを発展、開発するためである。

block-scopingのためにexplicit blockを作ることは、それらの懸念をどうにかすることができ、variableがどこにつけられているのか、いないのかをより明白にする。通常explicitコードはimplicitやsbutleコードよりも好まれる。explicit block-scopingスタイルは、簡単に達成でき、他の言語でblock-scopingがどのように働いているのか、より自然に適合する。

var foo = true;

if (foo) {
{ // <-- explicit block
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}

console.log( bar ); // ReferenceError

letが結びつくための任意のblockを作成するには、statementが有効な文法のどこにでも{..}のペアを含めるに過ぎない。この場合、if-statementの内部にexplicit blockを作った。ポジションと囲まれたif-statementの全てのblockをあとでリファクタリングの意味に影響なく、リファクタリングの後ろに全てのblockを移動することが簡単になる。

Note:
explicit block scope の他の表現方法は Appendix B参照。

chapter 4 ではhoistingを扱う。これは宣言が起こっているscope全体に存在すると見なされていることを示す。

しかし、letとともにされた宣言は、それらが現れるscope全体にhoistしない。そのような宣言は、宣言 statementまでblock内に存在することはありません。

{
console.log( bar ); // ReferenceError!
let bar = 2;
}

Garbage Collection

block-scopingが有益な他の理由は、メモリを再利用するための、closureとgarbage collectionに関係している。closureメカニズムについての詳細はchapter 5 参照。

function process(data) {
// do something interesting
}

var someReallyBigData = { .. };

process( someReallyBigData );

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );

click functionはhandler回収をクリックし、 someReallyBigData variable を全く必要としない。つまり、論理的に、process(..) が実行されたあと、big memory-heavy data structure はgarbage collection されることができる。しかし、click functionはscope 全体にclosureを持つので、JSが周辺の構造を維持しなければならない可能性はかなりある。(実装に依存する)

block-scopingはこの問題を解決でき、engineが someReallyBigDataを維持する必要がないことをはっきりとさせる。

function process(data) {
// do something interesting
}

// anything declared inside this block can go away after!
{
let someReallyBigData = { .. };

process( someReallyBigData );
}

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );

variableをローカルに結ぶためにexplicit blockを宣言することは、コードツールボックスに加えられる強力なツールだ。

let Loops

特にletが輝くのは、for-loopの場合だ。

for (let i=0; i<10; i++) {
console.log( i );
}

console.log( i ); // ReferenceError

for-loopヘッダーにある let がfor-loop bodyのi に結びつくだけでなく、実際には、loopの各繰り返しごとに結びつき、前のloop反復の終わりからのvalueを確実に際割り当てする。

反復ごとの結びつきについての別の例は

{
let j;
for (j=0; j<10; j++) {
let i = j; // re-bound for each iteration!
console.log( i );
}
}

Chapter 5 のclosureで、さらに詳しく取り扱う。

let 宣言は囲われたfunctionのscopeよりも、任意のblockにつき、既存のコードがfunction-scopeされたvar宣言にhiddenされた関係を持つ場所に落ち、var をletに置き換えると、コードをリファクタリングするときに、さらなる注意が必要となる。

var foo = true, baz = 10;

if (foo) {
var bar = 3;

if (baz > bar) {
console.log( baz );
}

// ...
}

これをこのようにリファクタできる。

var foo = true, baz = 10;

if (foo) {
let bar = 3;

if (baz > bar) { // <-- don't forget `bar` when moving!
console.log( baz );
}
}

block scopeの別のスタイルについてはAppendix B 参照。

const

letに加えて、constがES6では導入された。これもまたblock-scoped valueを作るが、valueは固定される。のちにvalueを変更しようとすると、エラーになる。

var foo = true;

if (foo) {
var a = 2;
const b = 3; // block-scoped to the containing `if`

a = 3; // just fine!
b = 4; // error!
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

Review (TL;DR)

functionはJSのscopeユニットで最も一般的だ。他のfunctionで宣言されたvariableとfunctionは、他全てのenclosing scopeから本質的に隠される。これはいいソフトウェアで意図して設計される原則だ。

しかしfunctionは決してscopeの唯一のユニットではない。block-scopeはvariableとfunctionが囲われたfunctionだけというよりも、コードの任意のblock(一般的には{..}ペア)に属す。

ES3からtry/catch structureがcatch clauseにblock scopeを持ち始めた。

ES6では、let キーワード(varの親戚)がいかなる任意のコードblockにもvariable宣言を許可することを導入した。if(..){ let a = 2; }はifの{..} block scopeを本質的にハイジャックするvariable a を宣言し、そこにつく。

block scopeをvar function scopeの完全な置き換えとはみなしていない。どちらの機能も共存しており、ディベロッパーはより読みやすく維持しやすいコードを作成するのに適切な場所には、function-scopeとblock-scope両方の技術を使用することができる。

【わからなかったこと】

Global “Namespaces” と Module Management で何を説明しているかよくわからなかった。

【感想】

なかなか難しかった。
let や const などが使えるかどうかでかなり書き方が変わりそうだが、そのような機能も後から追加されるということがわかった。

--

--

Tatsuya Asami
Tatsuya Asami

Written by Tatsuya Asami

Front end engineer. React, TypeScript, Three.js

Responses (1)