スコープとは
- 変数、関数、クラスによって外部からアクセスできる範囲を意味する。
- スコープには作りによって、関数スコープとブロックスコープ、場所によってはグローバルスコープとローカルスコープに分けられる。
スコープのルール
- 外側のスコープをグローバルスコープ、それ以外はローカルスコープという。
- ローカルスコープからグローバルスコープへのアクセスはできるが、逆はできない。
- グローバルスコープよりローカルスコープの方が優先順位は高い。
- 矢印関数は関数スコープではなく、ブロックスコープ扱いになる。
例文
まずは以下のコードを実行するとどうなるでしょう。
function count() {
for (var i = 1; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, i*100)
}
}
count()
このコードを実行すると、1, 2, 3, ... 9を0.1秒間隔で出力すると思うけど、実は10が9回出力されます。
なぜこのような結果になったのでしょうか。
また、1, 2, 3, ... 9を0.1秒間隔で出力させるためにはどのように修正すれば良いでしょうか。
これを説明するためにはこれから説明するスコープについて知る必要があります。
関数スコープとブロックスコープ
関数スコープ
function someFn() {
if (true) {
var text = 'foo';
}
console.log(text); // foo
}
someFn();
上記のコードを実行すると、foo
が出力されます。
その理由としては、var
で宣言された変数は関数レベルのスコープを持っつことになります。
つまり、if
文を包む関数内の先頭にホイスティングされるため、if
文の外でname
変数を使おうとしてもエラーにならないでしょう。
このように{}
で囲んだブロック内記述したとしても、ホイスティングされて{}
で囲んだブロックの外に宣言されることを関数スコープといいます。
なお、let
やconst
で宣言された場合はどうなるでしょうか。
ブロックスコープ
function someFn() {
if (true) {
const text = 'foo';
}
console.log(text); // [ERR]: "Executed JavaScript Failed:" // [ERR]: text is not defined
}
someFn();
上記のコードを実行するとtext
が見つからないとエラーが発生します。
その理由は、先ほどのvar
キーワードとは違って、let
やconst
で宣言された変数はif
文の中でのみ参照が可能だからです。
今回のコードですと、if
文の外でtext
変数を使おうとしていたので、エラーが発生しました。
このように関数ではなく、{}
で囲んだブロック内で有効な変数を宣言することをブロックスコープ
といいます。
では、最初にご紹介しました10が9回出力される関数を1, 2, 3, ... 9を0.1秒間隔で出力するにはどのように変更すべきでしょうか。
最初の問題の修正
function count() {
for (var i = 1; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, i*100)
}
}
count()
上記のコードからすると、var
で宣言されたi
は1から10まで1づつ増加しながら繰り返されることがわかります。
そしてfor
文の中にはsetTimeout
関数があり、受け取ったi
を0.1秒を掛け算してi
を出力するようにしました。
しかし、上記のコードだと、for
文の外にi
を宣言したことと同様で、setTimeout
関数が0.1秒を待っている間に、for
文は1から10までの処理が完了され、setTimeout
関数がi
を参照しようとするタイミングでは、i
は既に10
になってしまうため、10
が繰り返し表示されることになります。
これを修正するにはスコープを修正するだけになります。
方法1 新しいスコープをfor
文の中に追加する
function count() {
for (var i = 1; i < 10; i++) {
(function(num) {
setTimeout(() => {
console.log(num)
}, i*100)
})(i)
}
}
count()
こうすることで、i
という値を別のスコープであるnum
に代入することで、想定した1, 2, 3, ... 9を0.1秒間隔で出力することができます。
方法2 ブロックスコープを利用する
function count() {
for (let i = 1; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, i*100)
}
}
count()
もう一つの方法としては、var
で宣言したi
をlet
に変更するだけです。
これで、i
はfor
文の中でのみ有効になり、想定した1, 2, 3, ... 9を0.1秒間隔で出力することができます。
classにおけるスコープ
クラスにおいてのスコープは代表的にprivate、protected、publicを上げることができます。
- private: クラスの内部でのみアクセス可能。継承はできない。
- protected: 継承ができるため、クラス内部及び拡張された子クラスでのみアクセス可能。
- public: 外部からのアクセス可能。継承も可能。
これらを実際に検証してみると以下のコードになります。
インスタンス化したパターン
class Base {
private num: number
constructor() {
this.num = 1
}
protected plus() {
return this.num++
}
public minus() {
return this.num--
}
}
const base = new Base()
const num = base.num //num変数にアクセス不可
const plus = base.plus //plusメソッドにアクセス不可
const minus = base.minus //minusメソッドにアクセス可能
クラスBaseをインスタンス化したbaseから、Baseに定義されたplusとminus、また変数numにアクセスしようとすると、publicで定義したminusのみがアクセス可能であることがわかります。
また、継承をしようとすると以下のコードになるかと思います。
継承パターン
class Child extends Base {
numPlus() {
return this.plus() //親クラスのplusメソッドにアクセス可能
}
numMinus() {
return this.minus() //親クラスのminusメソッドにアクセス可能
}
numReturn() {
return this.num //privateに設定されているため、アクセス不可。 エラーが表示。
}
}
継承先では、privateで定義したnumにはアクセスができず、その他のprotectedとpublicで定義したplusとminusは継承ができていることがわかります。