参考
【JS】ガチで学びたい人のためのJavaScriptメカニズム
あらすじ
現場でrailsを触ってlaravelを触って今はtypescriptとreact-routerで実装することが多くなってきたが、今までJavascriptをがっつり勉強してなかったので復習も兼ねて上記講座を受けてみようと思った。
とある先輩から「エンジニアじゃない人間が聞いてもわかる程度の説明にしろ」という言葉をいただいたので、できるだけ頑張る。
スコープについて
実行中のコードから値と式が参照できる範囲
- グローバルスコープ
- スクリプトスコープ
- 関数スコープ
- ブロックスコープ
- モジュールスコープ
グローバルスコープとスクリプトスコープについて
let a = 0;
var b =0;
function c(){}
こうするとchrome開発者ツールのsourceのScope欄に
- Script a: 0
- Global: window (b: 0....c: f c())
のような形で入る。このことから
console.log(b)
はconsole.log(window.b)
と同義。
グローバルオブジェクトはwindowを省略できる。
window自体がグローバルスコープ
一般的にはスクリプトスコープもグローバルスコープと呼ばれる
関数スコープとブロックスコープ
関数スコープ
function f(){
//この部分が関数スコープ
}
function f(){
const b = 0;
}
console.log(b);
// Uncaught ReferenceError
これは関数スコープ内でb
が宣言されているため、外では使えない。
ブロックスコープとは
{
//ここのこと
}
ルールとしてはletまたはconstを使うこと
var
を使うとブロックスコープが適用されない。
関数もブロックスコープが適用されないので注意。
↑厳格モード、モジュールで使用可能
{
const b = 0;
console.log(b);
// 出力
// 0
const c = 0;
}
console.log(c);
// 出力
// ReferenceError
以下の場合はvarとfunctionはブロックスコープが適用されない。
{
var a = 'hoge';
function b(){
console.log('d is called');
}
}
console.log(a);
b();
// 出力
// hoge
// d is called
ブロックスコープの用途としてはif
やfor
と一緒に使う
スコープと実行コンテキストのまとめ
スコープ
実行中のコードから見える範囲
実行コンテキスト
コードが実行される状況
レキシカルスコープ
プログラムの文脈ではソースコードのどこに何を書いているかという意味。
コードを書く場所によって参照できる変数が変わるスコープのこと。
コードを記述した時点で決定するのでレキシカルスコープという。
let a = 0;
function fn1(){
let b = 1;
function fn2(){
let c = 3;
}
fn2
}
fn1
上記は全て出力できるa, fn1>b,fn2,>cのような関係。
let a = 0;
function fn1(){
let b = 1;
}
fn1
function fn2(){
let c = 3;
}
fn2
上記のように外に出すとエラーになる。
これがレキシカルスコープ
let a = 0; // グローバルスコープ
function fn1() {
let b = 1; // fn1のスコープ
function fn2() {
let c = 2; // fn2のスコープ
console.log(a); // グローバルスコープの変数にアクセス可能
console.log(b); // 外側のfn1スコープの変数にアクセス可能
console.log(c); // 自身のスコープの変数にアクセス可能
}
fn2();
// console.log(c); // エラー:cにはアクセスできない
}
fn1();
// console.log(b); // エラー:bにはアクセスできない
関数コンテキストの外部変数=レキシカルスコープである。
レキシカルスコープとは、実行中のコードからみた外部スコープのことである。
上記の例だとcのレキシカルスコープは
- 自身のスコープ(c)
- fn2の関数スコープ
- fn1の関数スコープ(b)
- グローバルスコープ(a)
と言える。
上記はスコープチェーンと言える。
※スコープチェーンは複数のスコープが連なっている状態
おまけ
let a = 2;
window.a = 4;
function fn1(){
console.log(a);
}
この場合は2が出力される。スコープは内側から探すので、グローバルオブジェクトのwindowはletのスクリプトスコープより外側にあるのでletの方が入るという仕組み。
クロージャーについて
クロージャーとはレキシカルスコープの変数を関数が使用している状態
function fn1(){
let b = 1;
function fn2(){
console.log(b);
}
}
fn1
fn2のbの状態をクロージャーと呼ぶ。
クロージャー(プライベート変数)について
let num = 0;
function increment() {
num += 1;
console.log(num);
}
上記の実装をするとnumを好きなタイミングですり替えられる
let num = 0;
function increment() {
num += 1;
console.log(num);
}
increment();
increment();
num = 0;
increment();
// 出力例
// 1
// 2
// 1
上記を避けるためにプライベート関数を使う
function incrementFactory() {
let num = 0;
function increment() {
num += 1;
console.log(num);
}
return increment;
}
const increment = incrementFactory();
increment();
increment();
increment();
このようにすると、const宣言した際にincrementFactory()
が実行され、
以下のincrement()
はconst宣言したincrementを実行する流れになる。
こうすることで初回に0がセットされ、以降のincrement()は戻り値のincrementが実行されるので1,2,3,4と処理が出力される。
num = 0
はこの実装だとスコープ外なのでnumも代入できないので強い。
クロージャー(動的な関数生成)について
function addNumberFactory(num) {
function addNumber(value) {
return num + value;
}
return addNumber;
}
const add5 = addNumberFactory(5);
const result = add5(10);
console.log(result);
上記はadd5
宣言でaddNumberFactory(num)のnumに5が入り、
以降のadd5(10)ではaddNumberFactory(5)、addNumber(value)が返っている状態。
Factoryに渡す値によって生成される値が変わるので動的と呼ばれる。
即時関数
即時関数とは、関数定義と同時に一度だけ実行される関数
let result =
(function(仮引数){
return 戻り値;
})(実引数)
即時関数の特徴は実行結果が呼び出し元に返却される
()をつける理由は、function hoge()
と基本は命名しないといけないが、
↑(関数式の場合は中で利用しない限りは名前をつけなくてよい)
ご指摘ありがとうございます!
補足:
- 関数宣言 例
function hello(name) {
return `Hello, ${name}!`;
}
// 名前なしはエラー、
function(name) { // SyntaxError
return `Hello, ${name}!`;
}
- 関数式 例
// 名前なしOK
const goodbye = function(name) {
return `Goodbye, ${name}!`;
};
即時関数は一度だけ実行する際に名前をつけるのが面倒な場合、
名前を省略して()を繋げることができる。
let c = (function() {
let privateVal = 0;
let publicVal = 10;
function privateFn() {
console.log('privateFn is called');
}
function publicFn() {
console.log('publicFn is called: ' + privateVal++);
}
return {
publicVal,
publicFn
};
})()
この場合、privateVal
とprivateFn()
は直接外からアクセスできない。
また、privateVal
はpublicFn()
とクロージャーの関係である。
呼び出された初回でprivateValが実行されるので、複数publicFn()
を呼ぶと0,1,2,3と増えていく。