なぜ仕組みを学ぶのか
自分の書いたコードがなぜエラーを出しているのか理解できていますか?
なぜそのような挙動になるのか、なぜReactやVueなどのフレームワークを理解できないのか。
フレームワークを扱うには根幹となるJavaScriptの基礎と仕組みの理解が必要です。
基礎無くしてはプログラムは書けず仕組みの理解がなくてはエラーを解決することは不可能で、より多くの時間を損失することになってしまいます。
JavaScriptの仕組みを理解することで不可解なエラーにも対処でき、他人の書いたコードの挙動を理解することができます。遠回りに見えたとしても仕組みを学び学習の下地を作ることで、効率的に学習を進めることが可能となります。
JavaScriptの仕組み
実行コンテキストとグローバルコンテキスト
コンテキストとは、コードを実装する際の文脈・状況という意味で用いられます。
カンタンにいうと、「そのコードがどのような状況で実行されているか」を表す言葉となります。
グローバルコンテキスト
関数やif文などの中に入っていない。
jsファイルにおける何も囲まれていない最上層の場所にコードが記載されているものがグローバルコンテキストであると理解しても良いかもしれません。
実際にはもっと複雑ですが、最初の段階であればこの理解でいいかと思います。
グローバルコンテンツ内であれば、以下の3つが参照できます。
- 実行中のコンテキスト内の変数・関数
- グローバルオブジェクト
- this
関数コンテキスト
関数に囲まれた状態で実行されるコードは関数コンテキストとなります。
関数コンテキスト内であれば以下が参照できます。
- 実行中コンテキスト内の変数・関数
- arguments
- super
- this
- 外部変数
コールスタック
「今実行中の関数はどの関数から実行されて」などの、実行履歴のようなものを格納するもの。
function a () {
}
function b () {
a();
}
function c () {
b();
}
c();
この関数cを実行した場合
コールスタックはグローバルコンテキスト → c → b → aの順番で実行され、積み上げられます。
コールスタックの仕組みは「後入れ先出し」となっており、a → b → c → グローバルの順番で消滅するようになっています。
これはChromeであれば開発者ツールのSourcesタブからコールスタックが確認できる。
ホイスティング
コンテキスト内で宣言した変数や関数の定義がコード実行前に配置されること。
たとえば関数定義よりも前に関数を実行したとしても、ホイスティングの仕組みにより実行前には既にメモリ上に定義した関数の領域が取られているため実行される。
a(); // "a"
function a() {
console.log("a");
}
しかしconstやletを使った場合の関数定義はホイスティングされない。
a(); // エラー
const a = function () {
console.log("a");
}
ホイスティング(varの場合)
以下のコードはvarを使っている。
この場合ホイスティングが行われ変数bのメモリ領域だけ確保される。(この時点ではundefined)
console.log(b); // undefined
var b = "b";
上記のコードはホイスティングの仕組みにより以下のように実行されている
var b; // undefinedが定義(メモリ領域の確保)
console.log(b); // undefined
b = 0; // 初めて値がセットされる
ホイスティング(let,constの場合)
let,constを利用した場合、ホイスティングは機能しないためエラーが発生する。
console.log(b); // エラー
let b = 0;
ブロックスコープ
中括弧内で書かれた変数などの参照は、その中括弧内でしか参照できない。
forやfunction、ifなどでも中括弧を書いた時点でその中身はブロックスコープが生成される。
function a (){
const name = 'Josh';
}
console.log(name); // エラー
しかし例外としてvarとfunctionを使った場合は、ブロックスコープが生成されない。
{
var a = 1;
function b () {
console.log("b");
}
}
console.log(a); // 動く
b(); // 動く
※ ブロックスコープは{}(中括弧)を書くだけで生成される。
レキシカルスコープ
コードを書く場所によって参照できる変数が変わるスコープのことを指します。
これには以下2つの意味があります。
- ① 実行中のコードからみた外部スコープのこと
- ② どのようにしてスコープを決定するかの仕様のこと
クロージャーを使ったプライベート変数の定義
- クロージャーとは、レキシカルスコープの変数を関数が使用している状態をクロージャーと言います。
- プライベート変数は、関数外部で参照できない変数のことを指します。
例えば、以下のようなコードがあるとします。
このコードでは変数numがグローバル変数であるため、長文コードのどこかで知らぬうちに上書きしてしまうといった予期せぬ結果を招きかねません。
let num = 0; // グローバル変数
increment(); 1
increment(); 2
increment(); 3
function increment() {
num = num + 1;
console.log(num);
}
そのため、クロージャーを使いnumをプライベート変数として定義し処理を行います。
function incrementFactory() { // Factoryは何かを生成する時につける名前
let num = 0; // incrmentFactory()を実行した時1回だけnumの初期化が行われる
function increment() {
num = num + 1;
console.log(num);
}
return increment; // increment関数を呼び出し元に返す。この場合、値ではなく関数が返される。
}
const increment = incrementFactory(); // increment関数が返ってきている
increment(); // 1
increment(); // 2
クロージャーを使った動的な関数生成
クロージャーを使うことで以下のように、セットした値を保持しながら加算処理を行うことが可能です。
function addNumberFactory(num) {
function addNumber(value) {
return num + value;
}
return addNumber; // addNumber関数を返す。値ではなく関数を返す
}
const add5 = addNumberFactory(5); // numに5がセットされた状態のaddNumber関数が帰ってくる
const add10 = addNumberFactory(5); // numに10がセットされた状態のaddNumber関数が帰ってくる。add5とは別物
const result = add10(10); // add10のvalueに10をセット
console.log(result);
即時関数
即時関数とは、「関数定義と同時に一度だけ実行される関数」のことです。
以下のように書くことで、関数を定義すると同時に実行することができます。
let result = (function(name) { // 引数nameをとる
console.log(name);
})("Josh"); // 実引数"Josh"を渡す
即時関数を使い、関数スコープ内の関数と変数を外部でも使えるようにする。
以下のコードはpublicValとpublicFn関数をreturnする関数を作成することで、外部呼び出し時にpublicValの値を保持しながら加算処理を行うことができる。
let c = (function() {
let privateVal = 0; // 即時関数は1回しか実行されないので、publicValの値は引き継がれながらインクリメントされる
let publicVal = 0; // 他でも使える変数にする
function privateFn() {}
function publicFn() { // 他でも使える関数にする
console.log(privateVal++);
}
return {
publicVal, // キーと値が同じなら省略できる
publicFn
}
});
// 実行可能
c.publicFn(); // 1
c.publicFn(); // 2
c.publicFn(); // 3
let,constとvar
宣言による機能の違い
最宣言
let a = 0;
let a = 0; // エラー
var b = 0;
var b = 1; // OK
最代入
let c = 0;
c = 1; // エラー
const d = 0;
d = 1; // エラー
ブロックスコープ
{
let e = 0; // ブロックスコープ
var f = 0; // グローバルスコープ
}
ホイスティング
console.log(g); // リファレンスエラー
console.log(h) // undefined
let g = 0;
var h = 0;
暗黙的な型変換
暗黙的な型変換とは、変数が呼ばれた状況によって変数の型が自動的に変換されること。
データ型を確認する
function printTypeAndValue(val) {
console.log(typeof val, val);
}
let a = 0;
printTypeAndValue(a); // number, 0
let b = '1' + a; // string('1') + number(0) = string
printTypeAndValue(b); // string, 10
マイナスは数値でしか使われないため、数値での計算が行われる
let c = 15 - b; // number(15) - string(10);
printTypeAndValue(c); // number, 5
nullは数値の0に暗黙的に変換される
let d = c - null; // number(5) - null
printTypeAndValue(d); // number, 5
trueは数値との計算と使用された場合、1とみなされる
let e = d - true; // number(5) - true;
printTypeAndValue(e); // number, 4
明示的に型を変換する
parseInt('1') // number, 1
等価性、true, false
厳格な等価性と抽象的な等価性
===(厳格な等価性)、==(抽象的な等価性)
厳格な等価性は型まで比較し、抽象的な等価性は型まで比較しない。
let a = '1';
let b = 1;
console.log(a === b); // false
console.log(a == b); // true
抽象的な等価性の挙動
まず比較する対象同士の型が合わされる。
a == b というのは a === Number(b) と同じ、まず型が揃えられる。
function printEquality(a,b) {
console.log(a === b);
console.log(a == b);
};
let b = 1;
let c = true;
printEquality(b,c); // false, true;
let e = "";
let f = 0;
let g = "0";
printEquality(e,f); // false, true;
printEquality(g,f); // false, true;
falsyとtruthy
falsyな値とはBooleanで真偽値に変換した場合にfalseになる値のこと。
falsyな値は false, null , 0, undeifined, 0n, NaN "" が該当し、それ以外は全てtruthyとみなされる。
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean(0n)); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN)); // false
let a; // undefined
console.log(Boolean(a)); // false
AND条件とOR条件
&& = AND条件: 両方とも条件が合致
|| = OR条件: 片方どちらかの条件が合致
const a = 0;
const b = 1;
console.log(a && b); // 0
console.log(a || b); // 1
AND条件の動き
- aの値がtruthyか確認する、falsyの場合はaの値を条件式の結果としてそのまま返却する
- aの値がtruthyの値の場合は、bの値を条件式の結果を返却する
const a = 1;
const b = 2;
const c = 3;
console.log(a && b); // 2
console.log(a && b && c); // 3
まとめ
- AND条件では左からみていき、falsyな値があった時点でその値を返す
- falsyな値が全てなかった場合は、比較している一番右の値を返す
OR条件の動き
- aの値がfalsyかtruthyか確認する。
- falsyの場合はbの値を取りに行く
- truthyであればそのままaの値がOR条件の結果として返る
const a = 0;
const b = 1;
const c = 3;
console.log(a || b || c); // 1
まとめ
- OR条件では左からみていき、truthyな値があった時点でその値を返す
- truthyな値が全てなかった場合は、比較している一番右の値を返す
OR条件とAND条件を混在化
()をつかってグループ化を行う
const a = 0;
const b = 1;
const c = 3;
const d = 0;
console.log((a || b) && c); // 3
console.log((a || b) && (c || d)); // 3
AND条件とOR条件の応用例
OR条件の応用例
引数に値がない場合など、引数がfalsyの時に初期値を与える関数を作る。
function hello(name) {
if(!name) {
name = 'Tom'; // これが初期値
}
console.log('Hello' + name);
}
hello(); // undefined
hello('Bob'); // Bob
OR条件の性質を利用し、上記をさらに簡略化する
function hello(name) {
name = name || 'Tom'; // nameがfalsyであればTomがセットされる
console.log('Hello' + name);
}
hello(); // undefined
hello('Bob'); // Bob
そしてこれを、ES6ならデフォルト引数を使うことができる。
function hello(name = 'Tom') {
console.log('Hello' + name);
}
hello(); // undefined
hello('Bob'); // Bob
AND条件の応用例
nameがtruthyの時にhello()を呼び出したいとする
function hello(name) {
name = name || 'Tom';
console.log('Hello' + name);
}
let name = 'Bob';
if(name) {
hello(name); // Hello Bob
}
AND条件の性質を利用し、上記をさらに簡略化する
function hello(name) {
name = name || 'Tom';
console.log('Hello' + name);
}
let name = 'Bob';
name && hello(name); // nameがtruthyの場合のみhello()が実行される
プリミティブ型とオブジェクト
データ型
文字列、数値などの異なる値の型をデータ型という
JavaScriptには8つの型が存在する。
- データ型ではプリミティブ型とオブジェクトが存在する。
- オブジェクトは参照を名前付きで管理している入れ物である
プリミティブ型
- 変数には値が格納される。
- 一度作成するとその値を変更することはできない。
- この性質を「immutable」、不変という意味になる
letを使えば再代入できるので不変じゃないのでは?と思われたかもしれない。
しかしこれは変数aに'Hello'を定義した場合、メモリ空間上に値'Hello'が保存され、変数aはそのアドレスを参照しているだけに過ぎない。
let a = 'Hello'; // メモリ空間に値'Hello'が保存され、そのアドレスを変数aが参照。
変数aに'Bye'を代入した場合新しく値'Bye'がメモリ空間上の別エリアに保存され、変数aが参照先を変更するだけ。
そのため値'Hello'は不変、変更されておらず変数aの参照先だけが変わっている状態になっている。
let a = 'Hello';
a = 'Bye'; // メモリ空間に値'Bye'が保存され、変数aは参照先を'Bye'に変更する。
値が変更されたわけではなくaの参照先が変わっただけにすぎないので、letの再代入は変更とはみなされない。(つまり不変)
オブジェクト
- 変数には参照が格納される。
- 値を変更することができる。
- この性質は「mutable」、可変という意味
オブジェクトはメモリ上でどのように処理されているのか?
let a = {
name: 'Tom';
}
ここでは変数aが「オブジェクトへのアドレス」を参照し、その実体となる{name}を「オブジェクトへのアドレス」が参照している。
そして{name}が値となる'Tom'を参照している。
① 変数a
↓ 参照
② オブジェクトへのアドレス
↓ 参照
③ {name}
↓ 参照
④ 'Tom'
ここで重要なのは、'Tom'という値はどこかに必ず保管されていて、それぞれのオブジェクトはその値を参照しているだけにすぎないということ。
参照とコピー
実際のコード
プリミティブ値のコピー
片方変えてもコピー元は影響を受けない
let a = 'hello';
let b = a;
b = 'bye';
console.log(a, b); // hello , bye
オブジェクトのコピー
片方変えるとコピー元は影響を受ける
let c = {
props: 'hello'
}
let d = c;
d.prop = 'bye';
console.log(c, d); // {prop: 'bye'}, {prop:'bye'}
プリミティブ値のコピー
プリミティブ値をコピーした場合、それぞれの値は独立して存在しているのでどちらかの値を変更してもコピー元の値は影響を受けない。
前述した通り、let a = 'Hello'; で'Hello'を保存した場合、aはのメモリ空間にある'Hello'のアドレスを参照しているにすぎない。
この’Hello’を代入している変数aを変数bに代入したとする。
let a = 'Hello';
let b = a;
すると、この変数aが参照している、'Hello'という値が別のメモリ空間にコピーされる。
この新しく作られた'Hello'に対して変数bがアドレスを参照することになる。
そしてこの変数bに対して'Bye'を再代入した場合には、bの参照先が'Hello'から'Bye'に変更されることになる。
let a = 'Hello';
let b = a;
b = 'Bye';
ここで重要なのは、変数aの参照先である**「値自体」がコピーされているということになる。
そのため、変数bの値を変更した場合には変数bの参照先**が変わるだけで、変数aが保持している参照先が変わることはない。
オブジェクトのコピー
オブジェクトをコピーした場合は、オブジェクトの値を変更した時にコピー元にも影響がある。
まず、以下のようなオブジェクトを作成したとする。
let a = {
prop: 'Hello'
}
- この場合、まず変数aが「オブジェクトへのアドレス」を参照する。
- その後オブジェクトの実体が格納される。( { prop } )
- 2番の{prop}が、'Hello'という値の参照を保持している状態になっている。
ここで変数bに変数aを代入すると
let a = {
prop: 'Hello'
}
let b = a;
「オブジェクトへのアドレス」が別のメモリ空間に対してコピーされることになる。
この際コピーされた「オブジェクトへのアドレス」も現在の「オブジェクトの実体{prop}」を参照する。
これが変数bに格納されている状態です。
この変数bのpropに対して'Bye'という値を格納しなおした場合
let a = {
prop: 'Hello'
}
let b = a;
b.prop = 'Bye';
「オブジェクトの実体{prop}」はもともと値'Hello'に参照が貼られていましたが、これが'Bye'に対して参照が貼り直されることになります。
ここで重要なポイントは、変数aと変数bの「オブジェクトへのアドレス」は同じ「オブジェクトの実体」を参照していることです。
そのためどちらかがpropの値を変更すると、両方に影響があります。
オブジェクトの完璧なコピー
変数dに対して新しいオブジェクトを設定した場合
dは新しい「オブジェクトのアドレス」を参照するようになるため、互いに影響を受けなくなる。
let c = {
props: 'hello'
}
let d = {};
console.log(c, d); // {prop: 'bye'}, {}
まとめ
- プリミティブ値のコピーは参照先の値がコピーされる
- オブジェクトのコピーはオブジェクトへの参照がコピーされる
参照とconst
プリミティブ値の再代入
constで変数を定義した場合、変数aから'Hello'に対して参照が貼られる。
const a = 'Hello';
この時の参照はロックされており、値を変更することはできない。
a = 'Bye'; // エラー
オブジェクトの最代入
以下のコードの場合aからオブジェクトへの参照はロックされているため、参照を貼り直そうとするとエラーが発生する。
const a = {
prop: 'Hello'
}
a = {}; // エラー
a = 'bye'; // エラー
しかしプロパティから値の参照部分については、constではロックされないためプロパティの値を変更することが可能
const a = {
prop: 'Hello'
}
a.prop = 'Bye'; // 変更可能
console.log(b); // {prop: 'Bye'}
参照と引数
参照とコピーのおさらい
以下のコードは変数の参照先の値、もしくは「オブジェクトへのアドレス」のコピーを表す。
let a = b;
プリミティブ型の値はそれぞれ独立しているので、値を変更しても影響を受けない。
let a = 0;
function fn1(arg1) {
arg1 = 1; // 値を変更
console.log(a, arg1); // 0 , 1
}
fn1(a);
オブジェクトは、オブジェクトへの参照が保持されているので片方の値が変更されると、もう片方も影響を受ける。
let b = {
prop: 0
}
function fn2(arg2) {
arg2.prop = 1; // 値を変更
console.log(b, arg2); // {prop: 1}, {prop: 1} // どちらも変更されてしまった
}
fn2(b);
次の関数fn3の中にある、変数bは現状のオブジェクトを保持しているが、arg2は新しいオブジェクトとして新しく宣言しているので互いに独立したオブジェクトを参照しているとわかる。
let b = {
prop: 0
}
function fn3(arg2) {
arg2 = {};
console.log(b, arg2); // {prop: 0}, {} // 互いに影響を受けない
}
fn3(b);
参照と分割代入
分割代入
オブジェクトから特定のプロパティを抽出して宣言を行う。
次のコードはオブジェクトに含まれるプロパティaとbが、変数a,bの初期値として代入される。
let {a, b} = object;
分割代入を行なった場合、まずpropの値'Hello'がコピーされ、別のオブジェクトがその値を参照するので分割代入でコピーした場合、独立した状態になる。
const a = {
prop: 'Hello'
}
let {prop} = a; // 独立した新しい値になる
なので異なる値を参照したとしても、別のオブジェクトがその値を参照するだけなので元のオブジェクトに影響はない。
以下のコードでは、元のオブジェクトは現状を保持したまま、propは新しく再定義できている。
const a = {
prop : 0;
}
let { prop } = a;
prop = 1;
console.log(a, prop); // {prop: 0}, 1 // 互いに独立しているので影響を受けない
豆知識:分割代入の名前を変える場合は再定義する書き方で行う
const a = {
prop : 0;
}
let { prop: b } = a; //
しかし、分割代入でとってきた値がオブジェクトだった場合、オブジェクトの参照が渡されるのでオブジェクトを変更すれば元のオブジェクトに影響があることに注意。
関数の引数で利用した場合
const a = {
prop : 0;
}
function fn(obj) {
let { prop } = obj;
prop = 1;
console.log(obj, prop); // {prop: 0}, 1
}
fn(a);
さらに以下の書き方で分割代入と引数をさらに省略できる。
引数に{prop}と書いて分割代入を行うことで、引数が渡ってきた時点で渡ってきた引数の中からプロパティ名propを抜き出すことができる。
function fn({prop}) {
console.log(prop);
}
参照の比較と値の比較
同じオブジェクトか確認する
変数a,bに入っているのは「オブジェクトへのアドレス」が入っているだけなので、異なる参照先を持っていることになる。
そのため以下はfalseになる。
const a = {
prop: 0
}
const b = {
prop: 0
}
console.log(a === b); // false
console.log(a == b); // false
そのためオブジェクトを比較するには、オブジェクトの中にある値を比較する必要がある
console.log(a.prop === b.prop); // true
変数a,bは値の参照を保持しているので、参照先が同じ場合はtrueとなる
const c = a; // cの参照先がaの参照的と同じになる。
console.log(a === c); // true