スコープとは
- 変数、関数、クラスによって外部からアクセスできる範囲を意味する。
- スコープには作りによって、関数スコープとブロックスコープ、場所によってはグローバルスコープとローカルスコープに分けられる。
スコープのルール
- 外側のスコープをグローバルスコープ、それ以外はローカルスコープという。
- ローカルスコープからグローバルスコープへのアクセスはできるが、逆はできない。
- グローバルスコープよりローカルスコープの方が優先順位は高い。
- 矢印関数は関数スコープではなく、ブロックスコープ扱いになる。
例文
まずは以下のコードを実行するとどうなるでしょう。
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は継承ができていることがわかります。