はじめに
Twitterでこんな記事を見かけました。知らなくても形にして動かすことはできますが、裏での処理がわからなくてうまく説明できなかったり、何より当然抑えておくべきだと思いましたので書くことにしました。
内容は参考元と同じなので、体に染みたら削除しようと思います。
参考元の著者: Twitter: @tsin1rou Qiita: @tsin1rou
0. 関数の宣言
const bar = (arg) => {
return arg * 2;
};
まずは関数の宣言です。returnの行しかない関数は省略して(引数)=>(返り値)
と書けます。
const bar = (arg) => (arg * 2);
const bar = arg => arg * 2; // 更に()の省略も可
よく見かける謎の記法です。
const bar = x => x + y;
const foo = x => (y => x + y); // 上記と同じ
const piyo = foo(4); // piyo = y => 4 + y;
console.log(piyo(3)); // -> 7
console.log(foo(4)(3)); // -> 7
1. コールスタック
JavaScriptでは関数を呼び出すとコールスタックと呼ばれる部分に呼び出された関数が積み上がっていきます。常に一番上の関数が実行されており、関数の実行が終わって値を返すとスタックから取り除かれ、上から2番目に載っていた関数へと戻っていきます。
const bar = x => x+6;
const foo = x => bar(x * 2) * 3;
-> console.log(foo(5));
上記は例です。上から順に実行され、console.log(foo(5));
の行まで来ました。
コールスタック |
---|
console.log(foo(5)) |
main() |
常にコールスタックの一番上を実行します。今回はfoo(5)
という関数を実行する命令が再び登場するので更に積み上げます。
コールスタック |
---|
foo(5) |
console.log(foo(5)) |
main() |
foo
を実行すると今度はbar
が登場しているので積み上げます。
const bar = x => x+6;
-> const foo = x => bar(x * 2) * 3;
console.log(foo(5));
コールスタック |
---|
bar(10) |
foo(5) |
console.log(foo(5)) |
main() |
一番上にあるbar(10)
を実行します。
-> const bar = x => x+6;
const foo = x => bar(x * 2) * 3;
console.log(foo(5));
16
という返り値を得られたのでbar(10)
をコールスタックから取り除きます。
コールスタック |
---|
foo(5) |
console.log(foo(5)) |
main() |
一番上にあるfoo(5)
の処理に戻ります。bar(10)
=16
ということがわかったので、48
という返り値が求まります。そしてこのfoo(5)
をコールスタックから取り除き、一番上に現れたconsole.log(foo(5))
を実行して、コンソールに48
が表示される流れです。その後はmain()
に従って次の処理へと進んでいきます。
2. プリミティブ型
基本的なデータ型です。全部で6種類あります。
boolean
: true, false
string
: "abcde"文字列
number
: 普通の数字。100
null
undefined
: nullは「無い」状態で、undefinedは「何も定義されていない」状態を表します。
symbol
: ES2015で導入された新しい型。ユニークなIDが生成出来ます。
3. 値型、参照型
先程のプリミティブ型は全て値型です。値型は 「変数をコピーした時に中身もコピーされる」 ということです。
let x = 20;
let y = 'abc';
const a = x;
const b = y;
x = 7;
y = 'def';
console.log(x, y, a, b); // -> 7 "def" 20 "abc"
変数 | 値 |
---|---|
x | 7 |
y | "def" |
a | 20 |
b | "abc" |
参照型
一方、オブジェクトや配列などは参照型です。
const a = [0, 1, 2];
a
にはアドレスがセットされます。
変数 | 値 |
---|---|
a | <#001> |
アドレス | データ |
---|---|
#001 | [0,1,2] |
ここで変数をコピーしてみると...
const b = a;
参照だけがコピーされます。
変数 | 値 |
---|---|
a | <#001> |
b | <#001> |
アドレス | データ |
---|---|
#001 | [0,1,2] |
つまり、a
の中身を変えるとb
の中身まで変わります。
4.型変換
JavaScriptはプリミティブ型に併せて
- Stringへの変換
- Booleanへの変換
- Numberへの変換
の3種類が存在します。
Stringへの変換
String(123) // '123'
String(-12.3) // '-12.3'
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(false) // 'false'
// 暗黙の型変換。 +の片方がStringならもう一方もStringに変換される
String(''+0+1) // '01'
String(0+''+1) // '01'
String(0+1+'') // '1' <- 0+1を先に計算
Boolean
''
0
-0
NaN
null
undefind
false
のみがfalse
に変換されます。他はすべてtrue
です。
Boolean('') // false
Boolean(0) // false
Boolean(-0) // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(false) // false
Boolean({}) // true
Boolean([]) // true
||
&&
!
3つの倫理演算子は暗黙のBoolean型変換を行います。
※ ||
&&
は型変換を内部でのみ行い、返り値は元の値・型がそのまま返ってきます。
// player.nameが設定されていなければ'NoName'
const name = player.name || 'NoName';
// if文で書くとこうなる
let name;
if(player.name){
name = player.name;
} else {
name = 'NoName';
}
Number
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(" 12 ") // 12
Number("-12.34") // -12.34
Number("\n") // 0
Number(" 12s ") // NaN
Number(123) // 123
以下の演算子はNumberへの暗黙の型変換を行います。
- 比較演算子
<
>
<=
=>
- ビット演算子
|
$
^
~
- 算術演算子
-
+
*
/
%
※+
の片方がStringならString。 - 比較演算子
==
!
※両方とも、比較対象がStringならString。
また、==はnullとundefinedの型変換を行わないという特別ルールがあります。
null == 0 // false
5. ==と===の違い
結論から言うと可能な限り===
推奨とのことです。
==
は暗黙の型変換を行うので意図せぬ挙動の原因となるからです。
false == 0 // true
false === 0 // false
0 == "" // true
0 === "" // false
null
とundefined
は==
においてそれぞれにのみ一致します。
null == undefined // true
null === undefined // false
NaN
は自分自身を含めて何とも一致しないというルールがあります。NaN
であるかどうか確認したい場合はNumber.isNaN()
を使用します。
const a = NaN;
console.log(a === a); // false
console.log(a === NaN); // false
console.log(Number.isNaN(a)); // true
typeof
どの型なのか調べる場合に使用します。
typeof 6 // "number"
typeof 'ker' // "string"
typeof {} // "object"
typeof true // "boolean"
typeof undefined // "undefined"
typeof (()=>{}) // "function"
6. スコープ
ifでもforでも関数でも、{}で囲まれてさえいればスコープが発生すると考えればOKのようです。
const bar = 'foobar';
{
console.log(bar); // Uncaught ReferenceError: bar is not defined
const bar = 'foobar';
console.log(bar); // -> "foobar"
}
console.log(bar); // -> "foobar"
JavaScriptでも基本的には上のスコープを見にいくのですが、同名の変数が同じスコープで定義されていると代わりにエラーが出ます。
const bar = 'foobar';
{
console.log(bar); // -> "foobar"
}
{
console.log(bar); // ReferenceError
const bar = 'foobar';
}
これは 「宣言の巻き上げ」 というJavaScriptの仕様で、全ての変数はスコープの一番上で宣言されたのと同じ扱いになります。ただし、明示的に宣言した場合にはエラーが出ません。
{
let bar;
console.log(bar); // -> undefined
bar = 'foobar';
}
{
console.log(bar); // ReferenceError
let bar = 'foobar';
}
7. 文Statementと式Expression
JavaScriptのソースコードはStatementとExpressionという2つの構文から成り立っています。
StatementはJavaScriptにおける文の単位のことで、例えばif文だとif (式) 別の文という形ですね。基本的に「戻り値」がないものがStatementです。Expressionは評価できて値が返るものです。
if (a > b) {
console.log("a is greater than b");
}
const sum = a + b;
-
const
let
-
if(else)
switch
-
for
(do)while
break
continue
-
throw
try
catch(finally)
StatementとStatementの間には;
セミコロンを使います。ただし、{}
で囲まれたブロック文でStatementが終わる際には;
を付けてはいけません。これがif(){}
とかwhile(){}
の後ろに;
が付いていない理由です。
逆にif()bar();
みたいな{}
を使わないifの後ろには通常通り;
が必要です。
8. ブラウザで使用できるタイマー
setTimeoutとsetIntervalは共にブラウザで使用できるタイマーです。
-
setTimeout(callback, delay)
は delayミリ秒以降にcallbackが実行されます。 -
setInterval(callback, interval)
は intervalミリ秒ごとにcallbackが実行されます。
setTimeout, setInterval
console.log('startTime:', new Date());
setTimeout(()=>{
console.log('timeout:', new Date());
}, 1000);
const timeout = setInterval(()=>{
console.log('interval:', new Date());
}, 1000);
setTimeout(()=>{
clearInterval(timeout);
}, 5000);
callbackに引数を渡したい時は、
setTimeout(callback, delay, 引数1, 引数2, ...)
or
setTimeout(()=>{callback(引数1, 引数2, ...)}, delay)
requestAnimationFrame
JavaScriptでアニメーションを作る際に使用します。
requestAnimationFrame(callback)
とすると次の描画の直前にcallbackが実行されます。
これを再帰させて下のコードの様にすると描画ごとに実行される関数の完成です。
const animation = ()=>{
requestAnimationFrame(animation);
// 描画処理
};
requestAnimationFrame(animation);
9. DOMとレイアウトツリー
DOM(ドキュメント・オブジェクト・モデル)。要素のツリー。
DOCUMENT (always the root)
└─HTML
├─HEAD
│ └─...
└─BODY
├─DIV
│ └─...
├─SCRIPT
└─SCRIPT
ブラウザはhtml文章をパースしてDOMを生成します。画面に描画されているのはDOMなので、これを適当に編集すれば画面の表示も変わります。ReactやVueの使用が多いので、直接編集することはあまりないと思います。
要素を取得する
よく使用されるのはgetElementById()
やgetElementsByClassName()
、querySelectorAll()
などです。
// idが"bar"の要素を取得
const element1 = document.getElementById('bar');
// <... class="foo bar baz">のようにクラスに含まれていればまとめて取得。返り値はリスト。
const element2 = document.getElementsByClassName('foo');
// cssのセレクター形式で要素を取得。返り値はリスト。
const element3 = document.querySelectorAll('#bar'); // id="bar"の要素を取得
const element4 = document.querySelectorAll('div.highlighted > p'); // p要素のうち直接の親が'highlighted'クラスを持つものを取得
要素の編集
const element = document.getElementById('...');
// 要素を作成
const foobar = document.createElement('div');
// 要素を追加
element.appendChild(foobar);
// 要素を削除
element.removeChild(foobar);
// 子要素を全削除
while(element.firstChild){
element.removeChild(element.firstChild);
}
// クラスを取得
const classList = element.classList;
if(classList.contains('foobar'){
// 要素にfoobarクラスが設定されていれば走る
}
// クラスを追加・削除・切り替え
classList.add('foobar');
classList.remove('foobar');
classList.toggle('foobar');
イベント関連
addEventListener()
とremoveEventListener()
を使います。
click
イベントの例です。
element.addEventListener('click', (event)=>{
// preventDefault()を呼ぶと通常の動作が行われなくなる。
// 例えばaタグをクリックしても移動しなくなる。
event.preventDefault();
console.log('要素がクリックされました');
});
// removeの方は両方の引数を同じにする必要がある
const clickHandler = (event)=>{...};
element.addEventListener('click', clickHandler);
element.removeEventListener('click', clickHandler);
10. thisとcallとapplyとbind
function
と()=>{}
で何がthis
になるかが異なります。
functionにおいてのthis
this
は呼び出し方に応じて変化します。
呼び出されるまで何がthis
になるか分かりません。
function bar(){
console.log(this);
}
bar(); // -> global
const obj = {
bar
};
obj.bar(); // -> obj
const obj2 = {
foo: function(){
bar();
}
};
obj2.foo(); // -> global
アロー関数においてのthis
コードを見れば何がthis
になるかが分かります (レキシカルなthis)
アロー関数はどこからどうやって呼び出してもthis
は常に同じものをさします。
const bar = () => {
//この段階でthisがglobalなのでどこで呼び出してもglobalになる
console.log(this);
};
bar(); // -> global
const obj = {
bar
};
obj.bar(); // -> global
const obj2 = {
foo: function(){
bar();
}
};
obj2.foo(); // -> global
callとapply
call
もapply
も呼び出す際に何がthis
になるのかを明示的に決定するための関数です。共に第一引数にthis
にしたいものを入れます。
call
は第二引数以降に直接引数にしたいものを入れていきますが、apply
は引数をまとめて配列にしたものを第二引数にするという違いがあります。ただし共にアロー関数のthis
を変えることはできません。
const bar = (arg) => {
console.log(arg);
console.log(this);
};
function foo(arg){
console.log(arg);
console.log(this);
}
const obj = {};
const args = ['arg1', 'arg2'];
bar.call(obj, args);
// -> ["arg1", "arg2"]
// -> global
foo.call(obj, args);
// -> ["arg1", "arg2"]
// -> Object {}
bar.apply(obj, args);
// -> "arg1" (配列が展開されて渡される)
// -> global
foo.apply(obj, args);
// -> "arg1"
// -> Object {}
bind
同じように何をthis
にするかを決められます。一度決めれば永続的に効果を発揮しますが、アロー関数には使えません。
const bar = () => {
console.log(this);
};
function foo(){
console.log(this);
}
const obj = {};
// bindは新しい関数を返す
const bar2 = bar.bind(obj);
const foo2 = foo.bind(obj);
bar(); // -> global (元の関数を変化させない)
foo(); // -> global
bar2(); // -> global (アロー関数には効かない)
foo2(); // -> Object {}
11. newとインスタンスについて
new
はインスタンスを作成するのに使用します。
class Bar {
constructor(args){
console.log(this);
const { name } = args || {};
this.name = name || 'NoName';
}
}
const Foo1 = (args) => {
console.log(this);
const { name } = args || {};
this.name = name || 'NoName';
}
function Foo2(args) {
console.log(this);
const { name } = args || {};
this.name = name || 'NoName';
}
// thisの中身↓
const bar1 = Bar(); // -> TypeError: Class constructor Bar cannot be invoked without 'new'
const bar2 = new Bar(); // -> Object { name:"NoName" }
const foo1_1 = Foo1(); // -> global
const foo1_2 = new Foo1(); // -> TypeError: Foo1 is not a constructor
const foo2_1 = Foo2(); // -> global
const foo2_2 = new Foo2(); // -> Object { name:"NoName" }
new
とclass
は常にセットで使用し、それ以外の場合は使用しないという取り決めが良いようです。
instanceof
インスタンスがどんなクラスを継承してきたのかを確かめるにはinstanceof
を使用します。たくさん継承を重ねていても先祖のどこかに存在すればtrue
になります。
class bar {
constructor(args){
...
}
}
class Foo extends Bar {
constructor(args){
super(args);
...
}
}
const bar = new Bar();
const foo = new Foo();
console.log(bar instanceof Bar); // -> true
console.log(bar instanceof Foo); // -> false
console.log(foo instanceof Bar); // -> true
console.log(foo instanceof Foo); // -> true