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

以下の記事を参照し、JSの関数とスコープについてまとめます。

まず、JSは関数をベースにしたスコープを作ります。

関数スコープ(function scope)は、関数に帰属します。この設計は、JSの変数は動的(どんな型も内包できる)性質を十分に活用できるものになります。

一方で、いくつかのJSの性質には、注意しないと落とし穴につながるようなものもあります。

Hiding In Plain Scope

基本的な関数は、次のようなものです。

  • 関数を宣言、次にコードをその中に入れる。

その反対に中にあるコードを書いてから、その周りを関数で囲む方法も有効な手段となります。

では、例を見てみます。

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

var b;

b = a + doSomethingElse( a * 2 );

console.log( b * 3 );
}

doSomething( 2 ); // 15

この結果、doSomethingElse関数を新しくdoSomething関数で包むことで、関数と変数を包んだ方の関数(ここではdoSomething関数)のスコープ内にする事ができます。

関数や変数の定義をする場所で、スコープが変わる、アクセスできる範囲が変わることになります。これをprivateにするといいます。

Collision Avoidance

2つの違った機能をもつ同じ名前の識別子は、意図しない処理を引き起こす原因となります。

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関数をforループで回していますが、変数iの名前が同じなために、終わらないループ処理になります。

この場合の対処法としては、bar関数の変数をvarで宣言します。そうすることで、forループの変数はbar関数のiにはアクセス出来なくなるので、上書きがされなくなります。

Global ネームスペース

特によく起こる名前の衝突は、グローバルスコープ内で起こります。

対処として、各ライブラリーは関数や変数をオブジェクトのプロパティとして、保持します。

使用する際はcoolLibracy.dosomething()などの用に呼ぶことで、名前の衝突を防ぎます。

Module Management

その他、衝突を防ぐ手段として、モジュールアプローチがあります。これはそれぞれの依存関係を、管理するツールを使うことで、識別子をグローバルではなくプライベートに管理をすることを促し衝突を防ぎます。

Function as scopes

ここでは即時関数、つまり()で関数を囲うことでもう一つのスコープで関数を、覆っています。つまり、関数名はそのスコープ内でのみアクセス可能となり、外のスコープにはその名前は影響を与えません。

また、そもそも関数の定義には識別子が必要となります。どういうことかというと

// OK
var a = function(){}
//It cant be
function() {}

上の関数expressionはOK、下の関数定義はダメです。

無名関数は実用的でよく使われているものですが、以下の天で注意が必要になります。

  • 名前がないことで、デバックをする際に挙動を追いにくい点
  • 再帰処理を行いたいときに、名前がないと、自分自身を指定して参照が出来ない点
  • 名前は、その名前自体が関数の機能を表せるので、コード可読性を無名より上げられる点

基本的には、常に関数に名前をつける習慣がベスト・プラクティスといえます。

IIFE(即時関数について)

即時関数は、幾つかのアプローチで使われています。

ここでは以下2つの例を用いて見てみたいと思います。

1,IIFEに引数を渡す

var a = 2;

(function IIFE( global ){

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

})( window );

console.log( a ); // 2

上の例では、引数にwindow(object global var)を渡すことで、グローバルスコープ内の変数aにアクセスしています。

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を渡しています。と同時に、

引数の()の中で関数を定義しています。

関数は一つの変数であると考えると、この仕組も理解しやすいと思います。

Block Scope(ブロックスコープについて)

他の言語でのブロックスコープとは少し機能が違っていますが、JSでもこのアプローチを取ることができます。

例を見てみます。

var foo = true;
if (foo) {
var bar = foo * 2;
//bar = something( bar );
console.log( bar );
}
//var bar can be seen in global scope
console.log(bar);

上のbar変数をグローバルスコープ内で参照することができます。つまりbar変数は、ブロックのスコープではなく、グローバルスコープ内で変数定義されていることがわかります。

次の例を見てみます。

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

この場合、変数はブロックのスコープ内で宣言されたとされます。つまり外のスコープからのアクセス、影響はありません。

try/catch構文を使ったときのスコープ

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

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

この場合、変数errはcatch内(inside catch scope)でのみアクセス可能になります。

また、例えば同じスコープ内で同名のエラー変数を宣言するcatch節をいくつか書いてみる。その場合、実際には変数はブロックスコープ内での定義になるので、変数の再定義とはならないです。

しかし、Lintはそれを変数の再定義と見ます。対処としては、err変数の識別子をそれぞれ別にするのが有効です。

Let

ES6から導入された変数の宣言方法です。let宣言をした時に、それがどんなブロック内であっても、ブロックスコープ内の宣言として扱われます。

例えば

var foo = true;

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

console.log( bar ); // ReferenceError

この場合、letはif文のブロックスコープでのみ、アクセス可能です。

ただし、このlet宣言をする際は、明示的にブロックを示す方がコードの視認性が増します。上の例で言えば

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

letを使用してプロックスコープを指定する際には、このような書き方がベスト・プラクティスと言えます。

let宣言時の注意としては、hoisting(巻き上げ)は起きません。つまりlet宣言がされるまでは、変数宣言は実質されていないといえます。

また、letはJSが本来もつクロージャー機能を、正しく使いたい時にも用いられます。

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 );

この例の場合、process関数が通った後も、someReallyBigData変数は保持されたままなります。では、これをletを使って明示的にあるプロックスコープ内で宣言しると、次のようなsentanceになります。

{
let someReallyBigData = { .. };

process( someReallyBigData );
        //このプロセス関数が処理を終えた時点で、変数
}

すると、JSエンジンはこの変数データを処理が終えた後も保持する必要性がないことを、理解します。

letを用いた変数宣言は汎用性が高いため、次のようなループ処理にも用いられます。

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

変数iとjはループでの繰り返しが行われる度に、ブロックスコープに結び付けられています。

つまり関数のブロックを使わないで、let宣言で、ブロックスコープ機能を使えることがポイントと云えます。

また、var宣言をlet宣言で置き換えるときは、let変数の特性上いくつかの注意が必要になります。

const

ES6はconstを用いた変数も、ブロックスコープ設定を行います。ただし、constは宣言後の値の変更はできません。変数と言うよりは、定数という方が正しそうです。

まとめ

特定のスコープで変数を囲む方法は、次の通りです。

  • 関数で囲む
  • ブロックスコープ(let, catch, for等)を用いる

また、letがvarを置き換えるというのではなく、どちらも正しく使用するのがbest practiceです。

関数、ブロックスコープ2つを適切に使用することも、よりメンテナンス性をもったコード生成に大事なことと言えるでしょう。

Like what you read? Give Tuyoshi Akiyama a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.