2021年12月21日 JavaScript (JS Primer) 関数とスコープ
静的スコープ:
ある変数がどの値を参照するかは静的に決まる
メモリ管理の仕組み: 参照されなくなったデータはガベージコレクションにより解放されます。
クロージャーとはこの2つの仕組みを利用して、関数内から特定の変数を参照し続けることで関数が状態を持てる仕組みのことを言います。
クロージャーがなぜ動くのか
const createCounter = () => { let count = 0; return function increment() { // `increment`関数は`createCounter`関数のスコープに定義された`変数`count`を参照している count = count + 1; return count; }; }; // createCounter()の実行結果は、内側で定義されていた`increment`関数 const myCounter = createCounter(); console.log(myCounter()); // => 1 console.log(myCounter()); // => 2
- 変数を保持するにはincrement関数の外側で変数countを定義します。createCounter関数スコープにあるcount変数を参照しているためです。
- 変数に格納してcreateCounter関数を複数回呼び出すことで挙動が確認できます。
- createCounterの結果をmyCounterに代入する事で、createCounterの返り値の1を参照するものがあるため解放されずに保持されます。
コード例2
function closure(initVal){ var count = initVal; // 100 var innerFunc = function() { return ++count; // 1加算 } return innerFunc; } var myClosure = closure(100); console.log(myClosure()); // 101 console.log(myClosure()); // 102
- closure関数はinnerFuncを返します。
- innerFuncは変数であるinitValを参照します。これによりinitValの値はガベージコレクションにより関数内ではメモリが開放されません。
- 結果はmyClosureに格納します。
- myClosureはグローバル変数なため、グローバルオブジェクトが存在し続ける限り解放されることがないので、ローカル変数countも破棄されません。
- countは破棄されないので、closure呼び出し時に代入された値が保持されることよってcountは加算されていきます
メモ
- クロージャーとは関数が特定の変数を参照することで関数が状態を持っていることを指します。
- createCounter関数を実行するたびに、それぞれcountとincrement関数が定義されます。
参考
2021年12月18日 JavaScript (JS Primer) 関数とスコープ
クロージャー
- クロージャーとは「外側のスコープにある変数への参照を保持できる」という関数が持つ性質を指します。
- ほぼ全ての関数はクロージャーとも言えます。厳密にいうと、特定の変数を参照することで関数が状態を持っていることをそう呼びます。
ガベージコレクション
JavaScriptではガベージコレクションが採用されています。 ガベージコレクションとは、どこからも参照されなくなったデータを不要なデータと判断して自動的にメモリ上から解放する仕組みのことです。
- 関数の返り値が変数をひとつでも参照している場合はガベージコレクションによりメモリが開放されることはありません。
ここまで読んで何かに例えた方が記憶に定着できるのかと感じたのでポケモンに例えてみました。 クロージャーはモンスターボールに例えると良いかもしれません。 野生のポケモン(変数)は戦闘を終えたらガベージコレクションによって解放されて、そのポケモン(変数)は使えなくなってしまいますが、モンスターボール(関数という名のクロージャー)に入れてしまえば、その野生のポケモン(変数)は解放されずに再利用できます。
静的スコープ
const x = 10; // *1 function printX() { // この識別子`x`は常に *1 の変数`x`を参照する console.log(x); // => 10 } function run() { const x = 20; // *2 printX(); // 常に10が出力される } run();
- printXの関数スコープに変数xが定義されていない
- ひとつ外側のスコープ(グローバルスコープ)を確認する
- ひとつ外側のスコープにconst x = 10;が定義されているので、識別子xはこの変数を参照する
上記の流れとなり、run関数内でconst x = 20; を定義したとしても、printXの返り値に影響はありません。コラムで書いてありましたが、動的スコープの言語では影響してしまい、printXの返り値は20になります。
メモリ管理の仕組み
let x = "before text"; // 変数`x`に新しいデータを代入する x = "after text"; // このとき"before text"というデータはどこからも参照されなくなる // その後、ガベージコレクションによってメモリ上から解放される
after textという値が再代入されるタイミングでberore textという値はこのガベージコレクション(ガベージは日本語でゴミという意味ですので認識しておくと覚えやすいです)によってメモリ上から解放されるわけです。 特に意識せずともこの仕組みは理解しているのではないかと感じました。
関数の場合
function printX() { const x = "X"; console.log(x); // => "X" } printX(); // この時点で`"X"`を参照するものはなくなる -> 解放される
関数を呼び出してXを参照したときに解放されます。 しかし、関数には解放されないパターンもあります。
function createArray() { const tempArray = [1, 2, 3]; return tempArray; } const array = createArray(); console.log(array); // => [1, 2, 3] // 変数`array`が`[1, 2, 3]`という値を参照している -> 解放されない
関数の呼び出しをarrayという定数に代入しています。 この場合、関数内のtempArrayの中身の配列のメモリはこの関数を呼び出した後でも、arrayという定数を出力する事で残っているので、ガベージコレクションは働いていないパターンとなります。
この2つを理解した上でprimerでは以下の記述があります。
クロージャーは「静的スコープ」と「参照され続けている変数のデータが保持される」という2つの性質によって成り立っています。
JavaScriptの関数は静的スコープとメモリ管理という2つの性質を常に持っています。そのため、ある意味ではすべての関数がクロージャーとなりますが、ここでは関数が特定の変数を参照することで関数が状態を持っていることを指します。
つまり、JSにおいては全ての関数がモンスターボールの性質を持っているという事です。
参考
2021年12月17日 JavaScript (JS Primer) 関数とスコープ
const, let , var再復習
const
値の再代入も同じ変数名での再定義もできません。
let
値の再代入はできるが、同じ変数名での再定義はできません。
var
値の再代入も、同じ変数名での再定義もできます。 しかし、値が簡単に書き換え可能なので、ほとんど使用しません。
値の再代入 | 同じ変数名での再定義 | |
---|---|---|
const | ❌ | ❌ |
let | ⭕️ | ❌ |
var | ⭕️ | ⭕️ |
関数宣言と巻き上げ
functionキーワードを使った関数宣言もvarと同様に、もっとも近い関数またはグローバルスコープの先頭に巻き上げられます。 以下の通り、関数の呼び出しは宣言より前に行うこともできます。
hello(); // => "Hello" function hello(){ return "Hello"; }
関数がスコープの先頭に巻き上げられて解釈されて実行されています。
- varで宣言された変数へ関数を代入した場合はundefinedとなり、関数の呼び出しエラーとなります。
[コラム] 即時実行関数
即時実行関数はletやconstとブロックスコープで置き換えられるため、現在では不要なものとなりましたので参考程度に紹介されています。
- 『即時実行関数』はその名の通り『即時実行される無名関数』です。
- グローバルに影響しない、ローカルのスコープを作り出しそのまま処理を実行することができます。
varとletのスコープについて理解が浅かったので深堀りしました。
const | let | var | |
---|---|---|---|
再宣言 | ❌ | ❌ | ⭕️ |
再代入 | ❌ | ⭕️ | ⭕️ |
スコープ | ブロック | ブロック | 関数 |
ホイスティング | エラー | エラー | undefined |
私の場合はスコープの認識が浅く腑に落ちていなかったです。
varではブロック内で宣言をするとグローバルスコープで宣言したかのように振る舞ってしまうため、わざわざ関数を定義する必要があり、即時実行関数を使っていましたが、constとletのスコープはブロックとなったため、即時実行関数を定義する事なく、グローバルスコープを汚さずに済む仕様になりました。
参考
2021年12月16日 JavaScript (JS Primer) 関数とスコープ
[コラム]変数を参照できる範囲を小さくする
変数のスコープとは、その変数が参照できる範囲のことです。
スコープには、大きく分けて以下の2種類があります。
- グローバルスコープ → ページ全体でどこからでも参照できます。
- ローカルスコープ → ページ内の部分的な範囲のみ参照できます。
変数はできるだけ利用するスコープ内に定義する方が良いです。現在のスコープの変数を参照するつもりがグローバル変数を参照したり、その逆も起きることがあるからです。 あらゆる変数がグローバルスコープにあると、どこでその変数が参照されているのかを把握できなくなります。 これを避けるシンプルな考え方は、変数はできるだけ利用するスコープ内に定義するというものです。
グローバルスコープにはグローバルスコープに定義する必要性があるものだけにすれば良いという事です。
以下はアンチパターンです。
function doHeavyTask() { // 計測したい処理 } const startTime = Date.now(); doHeavyTask(); const endTime = Date.now(); console.log(`実行時間は${endTime - startTime}ミリ秒`);
このコードでは、計測処理以外で利用しないstartTimeとendTimeという変数がグローバルスコープに定義されています。
プログラム全体が短い場合はあまり問題になりませんが、プログラムが長くなっていくにつれ影響の範囲が広がっていきます。 この場合は関数のブロック内で変数を定義する方が好ましいです。
// 実行時間を計測したい関数をコールバック関数として引数に渡す const measureTask = (taskFn) => { const startTime = Date.now(); taskFn(); const endTime = Date.now(); console.log(`実行時間は${endTime - startTime}ミリ秒`); }; function doHeavyTask() { // 計測したい処理 } measureTask(doHeavyTask);
その他 理由
- 変数名の衝突が発生しやすくなるため、注意を払う必要があります。(保守性が落ちてしまう)
- javascriptの新しいプラグインを入れた場合などに競合が発生し、予期せぬ動きになる場合があります。
復習 メモ
- コールバック関数とは関数の引数に指定出来る別の関数です。
- 関数を呼びだす際に関数を引数に渡せる(値として扱える)のはファーストクラスファンクションであるためです。
関数スコープとvarの巻き上げ
letはvarを改善する目的で導入された構文ですが、既存のコードや既存のライブラリなどではvarが利用されている場面もあるため、varの動作を理解する必要があります。
letとvarの違い
変数を宣言する前にその変数を参照する
// var宣言より前に参照してもエラーにならない console.log(x); // => undefined var x = "varのx"; // letの場合はエラーになります。 console.log(x); // => ReferenceError: can't access lexical declaration `x' before initialization let x = "letのx";
- letの場合・・・ReferenceErrorの例外が発生して参照できません。
- varの場合・・・暗黙的にundefinedを返します。
変数xを参照するコードより前に変数xの宣言部分が移動しているためです。
上記のvarの挙動をコード例として記述すると以下のようになります。
// 解釈されたコード // スコープの先頭に宣言部分が巻き上げられる var x; console.log(x); // => undefined // 変数への代入はそのままの位置に残る x = "varのx"; console.log(x); // => "varのx"
undefinedになる理由は宣言部分だけが巻き上げが行われていて、変数への代入はそのままの位置に残るためです。
var pokemon = 'ピカチュー'; //グローバル変数 function show(){ console.log(pokemon); var pokemon = "ポリゴン" } // ポリゴンを再代入 巻き上げが起こるためundefined show();
varを使わなければいい話なのですが、既存のコードや既存のライブラリなどではvarが利用されている場面もあるためこのような違いを認識しておく必要があります。
参考
2021年12月14日 JavaScript (JS Primer) 関数とスコープ
この章では関数とスコープの関係を中心に、スコープとはどのような働きをしていて、スコープ内では変数の名前から取得する値がどのように決まるかを見ていきます。
スコープとは
スコープとは変数や関数の引数などを参照できる範囲を決めるものです。
function fn() { const x = 1; // fn関数のスコープ内のため`x`は参照できる console.log(x); // => 1 } console.log(x); 上記はスコープの外のためReferenceErrorとなる
メモ
- 関数の{}内で定義した変数(x)をスコープの外で呼び出そうとした場合のエラー
ReferenceError: x is not defined - 同じスコープ内で同じ変数名は使用出来ません。(SyntaxError)
- 関数によるスコープのことを関数スコープと呼びます。
ブロックスコープ
{}←これで囲われた範囲をブロックスコープと呼びます。関数のブロックに限らず、ブロック内で宣言された変数は、スコープ内でのみ参照でき、スコープの外側からは参照できません。if文などのブロックもそうです。
メモ
スコープチェーン
内側から外側のスコープへと順番に変数が定義されているか探す仕組みのことをスコープチェーンと呼びます。
{ OUTERブロックスコープ const x = "x"; { INNERブロックスコープからOUTERブロックスコープの変数を参照できる console.log(x); // => "x" } }
- INNER,OUTER,一番外側のスコープという順に探索してどこにもなかったら
ReferenceError
を返す。 - 外側からは内側の変数を参照出来ません。
- それぞれのスコープに定義されている変数を優先して参照します。INNERに定義した変数をINNERで出力する場合はINNERの変数を出力し、OUTERで定義した変数をOUTERで出力する場合はOUTERの変数が優先で出力されます。
{ // OUTERブロックスコープ const x = "outer"; { // INNERブロックスコープ const x = "inner"; // 現在のスコープ(INNERブロックスコープ)にある`x`を参照する console.log(x); // => "inner" } // 現在のスコープ(OUTERブロックスコープ)にある`x`を参照する console.log(x); // => "outer" }
グローバルスコープ
グローバルスコープとは名前のとおりもっとも外側にあるスコープで、プログラム実行時に暗黙的に作成されます。
グローバルスコープで定義した変数はグローバル変数と呼ばれ、グローバル変数はあらゆるスコープから参照できる変数となります。
グローバルスコープには自分で定義したグローバル変数以外に、プログラム実行時に自動的に定義されるビルトインオブジェクトがあります。
このように内側のスコープで外側のスコープと同じ名前の変数を定義することで、外側の変数が参照できなくなることを変数の隠蔽(shadowing)と呼びます。
// ビルトインオブジェクトは実行環境が自動的に定義している // どこのスコープから参照してもReferenceErrorにはならない console.log(isNaN); // => isNaN console.log(Array); // => Array console.log(String); // => String console.log(Function); // => Function
これらビルドインオブジェクトは同じ名前で変数定義すると上書きされてしまいます。
// "Array"という名前の変数を定義 const Array = 1; // 自分で定義した変数がビルトインオブジェクトより優先される console.log(Array); // => 1
この事を変数の隠蔽(shadowing)と呼びます。
この問題を回避する方法としては、むやみにグローバルスコープへ変数を定義しないことです。グローバルスコープでビルトインオブジェクトと名前が衝突するとすべてのスコープへ影響を与えますが、関数のスコープ内では影響範囲がその関数の中だけにとどまります。
ビルトインオブジェクトと同じ名前を避けることは難しいです。 なぜならビルトインオブジェクトには実行環境(ブラウザやNode.jsなど)がそれぞれ独自に定義したものが多く存在するためです。 関数などを活用して小さなスコープを中心にしてプログラムを書くことで、ビルトインオブジェクトと同じ名前の変数があっても影響範囲を限定できます。
まとめ
- 基本的にはブロックなどでスコープの影響範囲を制御しない場合はグローバルスコープに影響していることになります。
- グローバル変数はあらゆるスコープで参照出来るため便利です。
- ビルドインオブジェクトをshadowingしてしまわないように注意が必要です。
復習
ビルドインオブジェクトとは
JavaScriptで以下のようなあらかじめ定義されているもので、宣言なしで使用することができるものです。
for of と for in
どちらも配列に繰り返し処理をすることができます。
- for of は要素
- for in はindex
const array = [1, 2, 3]; for (const value of array) { console.log(value); } // 1 // 2 // 3 const array = [1, 2, 3]; for (const value in array) { console.log(value); } // 0 // 1 // 2
forEach
配列を関数の引数にクレーンのように一つずつ要素を入れます。
Array.forEach((x) => {繰り返しの処理}); // => Arrayの中の配列の要素を一つずつ引数に渡して中身の処理をします。
参考
2021年12月13日 JavaScript (JS Primer) ラッパーオブジェクト
JavaScriptでは、プリミティブ型の値に対してプロパティアクセスするとき、自動で対応するラッパーオブジェクトに変換されます。
const str = "string"; // プリミティブ型の値に対してメソッド呼び出しを行う str.toUpperCase(); // `str`へアクセスする際に"string"がラッパーオブジェクトへ変換され、 // ラッパーオブジェクトはStringのインスタンスなのでメソッドを呼び出せる // つまり、上のコードは下のコードと同じ意味である (new String(str)).toUpperCase();
明示的に作成したラッパーオブジェクトからプリミティブ型の値を取り出すこともできます。
const stringWrapper = new String("文字列"); // プリミティブ型の値を取得する console.log(typeof stringWrapper); console.log(stringWrapper.valueOf()); // => "文字列" - 検証 - const str = stringWrapper.valueOf() console.log(typeof str); // => string
JavaScriptには、リテラルを使ったプリミティブ型の文字列とラッパーオブジェクトを使った文字列オブジェクトがあります。
- プリミティブ型の値からラッパーオブジェクトへの変換は自動的に行われるため、基本的はプリミティブ型のデータにはリテラルを使います。
プリミティブ型の値がなぜメソッド呼び出しできるのか
それはJavaScriptではプリミティブ型に対応したラッパーオブジェクトというものが存在しているためです。 プリミティブ型の値のプロパティへアクセスする際に、自動的にラッパーオブジェクトへ変換されることでメソッド呼び出しなどが可能となっています。
プリミティブ型に対してメソッドを使える事にはこういった背景があるという認識だけを持ち、普段コードを書くときには、リテラルを使うと良いでしょう。
検証(インスタンスであるか)
instanceof演算子は、オブジェクトがどのコンストラクタから生成されたかを判別することが出来ます。
// プリミティブの文字列は"string"型 const str = "文字列"; console.log(typeof str); // => "string" console.log(str instanceof String); // => false
上記で見る限り、プリミティブな文字列はインスタンスではないことが分かります。
次のコードではnew演算子を用いた場合の実行結果です。
const str = new String("文字列"); console.log(typeof str); // => "object" console.log(str instanceof Object); // => "true"
以上の結果からnew演算子を用いた場合、インスタンスが生成されているという認識が正しいと言えます。
オブジェクトがどのオブジェクトか知りたいときの検証方法
Object.prototype.toString.call(new String("文字列")); // => [object String] // 省略してこのようにも書けます。 toString.call(new String("文字列")); // => [object String]
上記のようにObject.prototype.toString.call(検証対象);
又は、toString(検証対象)
とすると検証できます。
以下はNumberを扱った場合の例です。
const yano = 1; console.log(typeof yano); // => "number" console.log(yano instanceof Number); toString.call(yano); 出力結果 number false [object Number]
- yanoに数値を代入します。
- typeof演算子で見るとnumberと出力されます。これはデータ型がnumberのプリミティブな値であるということを意味しています。
- instanceof Numberではfalseと出力されます。このように出力されるのは上の説明の通りNumberのインスタンスではなく、yanoはただのプリミティブな数値であるからです。
- 最後のcallでNumberとして返り値が出力されるのは一時的に生成されたラッパーオブジェクトが呼び出されているためです。
ラッパーオブジェクトというのはstring, number, booleanの3つのプリミティブな値に対してメソッドを呼び出した際に、メソッド実行用に一時的に生成されるオブジェクトです。その為、コンストラクタを使わずにtoStringを呼び出す事が出来ます。
豆知識
このようなプリミティブ型からオブジェクト型への変換はボックス化(ボクシング)、逆にオブジェクト型からプリミティブ型への変換はボックス化解除(アンボクシング)と呼ばれます。
「JavaScriptはすべてがオブジェクトである」と言われることがあります。 プリミティブ型はオブジェクトではありませんが、プリミティブ型に対応したラッパーオブジェクトが用意されています(nullとundefinedを除く)。 そのため、「すべてがオブジェクトのように見える」というのが正しい認識となるでしょう。
結論
長々と深堀りしましたが、結論は プリミティブ型のデータにはリテラルを使ってもラッパーオブジェクトへの変換が自動的に行われることで、メソッドが使えるため、基本的にはnew演算子は使用せずリテラルを使えば良いでしょう。
参考
2021年12月11日 JavaScript (JS Primer) ラッパーオブジェクト
undefined と null を除いたものに対してはそれぞれラッパーオブジェクトが用意されています。
ラッパーオブジェクト | プリミティブ型 | 例 |
---|---|---|
Boolean | 真偽値 | trueやfalse |
Number | 数値 | 1や2 |
String | 文字列 | "文字列" |
Symbol | シンボル | Symbol("説明") |
プリミティブ型の値に対して対応するラッパーオブジェクトで用意されているメソッドを呼び出すと、自動的にプリミティブ型の値が対応するラッパーオブジェクトに変換され、その上でラッパーオブジェクトのメソッドが呼び出されます。
// プリミティブの文字列は"string"型 const str = "文字列"; console.log(typeof str); // => "string" // ラッパーオブジェクトは"object"型 const stringWrapper = new String("文字列"); console.log(typeof stringWrapper); // => "object"
上記はtypeof演算子を使って違いを表しています。 一般的に生成されたインスタンスはstring型です。 new演算子で生成されたインスタンスはobject型となります。
メモ
- JavaScript の new は、class からインスタンスをつくるものではありません。
- new 演算子を使用すると、オブジェクト型やコンストラクタ関数を持つ組み込みオブジェクト型のインスタンスを作成することができます。
- コンストラクタとはクラスをnewした瞬間に実行される関数のことです。
- Rubyでコンストラクタと言えば、インスタンスを生成した際に1度だけ実行されるinitializeメソッドのことです。