この記事は、プログラマーになって1年間Swiftばかりを書いてきた僕が
JavaScriptをやることになり1ヶ月。数々のハマったポイントを自分なりにまとめたものになります。
なお、JSを勉強するにあたって、JavaScript本格入門が大変参考になりました。
この記事も主にこの本から学んだことが中心です。
定数・変数
ES2015で、let, const宣言が追加された。
宣言 | 説明 |
---|---|
const | 定数。再代入不可。ブラケットでブロックスコープを作ることができる |
let | ブラケットでブロックスコープを作ることができる |
var | 変数(極力使わない) |
なんとJSにはブロックスコープがなかった。
console.log("var宣言");
for (var i = 0; i < 5; i++) {
console.log(i);
}
console.log(i); // forブロックの外側からiを参照できてしまう
let宣言を使うことで、JSでもブロックスコープを使えるようになった。
console.log("let宣言");
for (let i = 0; i < 5; i++) {
console.log(i);
}
console.log(i); // Uncaught Reference Error!
const, letの使い分け
基本constを使う。再代入したらエディタがエラーを出してくれるので、エラーが出たらletを使用する。
nullとundefinedの違い
JavaScriptにはnullだけではなく、undefinedという概念が存在する。定義の違いは下記
定義 | |
---|---|
null | 値が存在しない |
undefined | 未定義 |
nullチェック
JavaScript
let elm = document.getElementById('hoge');
let undy; // 未定義の場合、暗黙的にundefinedが格納されている
if (elm === null) return; // id="hoge"が存在しないのでガード成功
if (elm === undefined) return; // elmの定義はされているので、チェックを抜けてしまう
if (undy === undefined) return; // undyは未定義なのでガード成功
alert("ちくしょう!来やがったな!");
nullかundefinedならガードしたいのであれば、
厳密じゃない等価演算子(==)を使ってnullと比較することでチェックできる。
if (undy == null) return;
jQuery
jQueryオブジェクトのnullチェックを行う場合、たとえそのDOM要素が存在しなくともjQueryオブジェクトは生成されるため、単純なオブジェクトのチェックだとnullチェックできない
let $elm = $('.hoge');
if ($elm == null) return; // チェックを抜けてしまう
jQueryオブジェクトの配列要素0にアクセスすることで、DOM要素自体のチェックが可能
let $elm = $('.hoge');
if (!$elm[0]) return;
参考:
JavaScriptやjQueryでの変数が「空かどうか」のチェック方法
ライフサイクル
ブラウザでロードされるタイミングで呼ばれるイベントハンドラが存在する
JavaScript
DOMContentLoadedの方がwindow.onloadより早い。
初期化処理は基本、このイベント内で行う
画像の大きさなどを取得したい場合は、window.onloadを使用する
イベント | 定義 |
---|---|
DOMContentLoaded | DOM生成時 |
window.onload | ページ読み込み完了時(画像なども含む) |
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM読み込み完了');
}, false);
window.onload = function() {
console.log('ページ読み込み完了');
};
jQuery
$(function() {});
は$(document).readyの省略形
$(document).ready(function() {
console.log('DOM読み込み完了');
});
$(function() {
console.log('DOM読み込み完了');
});
関数リテラル
JavaScriptの関数は、オブジェクトの一種。
関数リテラルとは
関数は、変数に代入することが可能。
関数リテラルと言う。
let testFunc = function(){処理};
testFunc();
変数の巻き上げ(hoisting)
- JavaScriptは、関数のいかなる場所で宣言した変数も 内部的に、先頭で宣言されたことになる
- しかも宣言部分だけが先頭に移動し、 代入部分は移動しない
というルールが存在する。。マジか
var宣言の場合
var yourname = 'global';
function say() {
console.log('君の名は' + yourname); // ① 君の名はundefined
var yourname = 'local';
console.log(yourname); // ② local
}
マジでした。
varの例だと、①が'global'だと思いきや、ローカルの yourname
が巻き上がって
君の名はundefined
になる。
let宣言の場合
let yourname = 'global';
function say() {
console.log('君の名は' + yourname); // ① ReferenceError: yourname is not defined
let yourname = 'local';
console.log(yourname);
}
let宣言の場合、巻き上げは発生しますが君の名はundefined
にならず、
ReferenceErrorという実行時エラーを投げてくれます。
なお、MDN曰く
let宣言の場合、ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone (TDZ)" の領域の中にいる
仕様なため、エラーを投げられるようです。
※ コメントいただき、ありがとうございます!
関数の場合
function foo() {
bar(); // ① 'bar'
function bar() {
console.log('bar');
}
}
function foo2() {
bar(); // ② ReferenceError: bar is not defined
let bar = function () {
console.log('bar');
}
}
巻き上げは関数でも同様に起こる。
- function命令①の場合は、後方参照が可能。
- 関数リテラルの②の場合は、
bar
部分だけ巻き上がっているため、後方参照が不可。
対処としては、関数内の変数は先頭で定義すること とのこと
詳しくは、下記の記事をご参照ください。
知らないと怖い「変数の巻き上げ」とは?
クロージャ
「ローカル変数を参照している関数内関数」のことをクロージャと言う。
クロージャを日本語訳すると、"関数閉包"。
function closure(memberCount) {
let counter = memberCount;
return function inner() { // ①
return counter += 1;
}
}
let myClosure = closure(10);
// ②
myClosure(); // 11
myClosure(); // 12
上記の例のinnerはクロージャ。
①の時点で、counterをインクリメントした値が返ってくるように見えるが、実際はインクリメントする関数innerが返って来ている。
②の時点で、クロージャがまだローカル変数を参照しているので、メソッド呼び出し後、破棄されるはずのcounterの参照が保持されている。
関数内関数が親の変数を参照できることを、キャプチャと言う
即時関数
即時関数は、宣言と実行が同時に行える関数のこと
2値の合計を返してくれる関数リテラルを即時実行関数で表現する
let count1 = 100;
let count2 = 200;
let result = function (param1, param2) {
return param1 + param2;
}(count1, count2); // 関数末尾の()が即時実行の命令。引数2つを渡して実行!
console.log(result); // 300が出力される。
resultにはparam1とparam2を足した無名関数が格納されているように見えるが、即時関数を使うことで実際の結果だけを取得できている
クロージャを使うと何が嬉しいのか
JSにはprivateな変数を宣言する命令がないため、クロージャを活用するとグローバルスコープを汚染しない書き方ができる。
// アンチパターン
let index = 0;
let xxx..
let yyy..
let zzz...
・
・
// 200行目
function counter() {
console.log(index++);
}
グローバル変数をむやみに定義してしまうと、下記のような弊害がある(JSに限った話じゃないですが)
- グローバル変数の状態を後から追うとき対象範囲がコード全体になる
- 変数の競合が起きやすい and 名前を考える時間が余分にかかる
let counter = (function() {
let index = 0;
return function() {
console.log(index += 1);
};
}()); // 即時関数
counter(); // 1
counter(); // 2
counter(); // 3
console.log(index); // index is not defined
クロージャと即時関数を使うことで、indexを
- プライベート変数にすることができた
- indexのことを意識する範囲を狭くできた
Class
ES2015から他言語同様、class構文が書けるようになった。
他言語でおなじみのprivate publicなどのアクセス修飾子はなく、すべてのプロパティがpublicになる
class Member {
// コンストラクタ
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
// ゲッター
get firstname() {
return this._firstname;
}
// セッター
set firstname(val) {
this._firstname = val;
}
// メソッド定義
getName() {
return this.lastname + this.firstname;
}
}
// インスタンス化
let hiroshi = new Member('ひろし', '山田');
console.log(hiroshi.getName()); // 山田ひろし
console.log(hiroshi.firstname); // ひろし
(ES2015以前)プロトタイプベースのオブジェクト指向
JavaScriptのclass構文は、classという概念が存在するわけではなく、実態はfunction。
もともとJSで実装されていたプロトタイプベースの
オブジェクト指向のシンタックスシュガーなので、もともとの実装を知ることは重要。
クラス定義
- コンストラクタを宣言した関数を作る
- new演算子でインスタンス化
new演算子の構文は new constructor[([arguments])]
であり、
constructor = 自分で定義した関数といえる。
// コンストラクタを宣言した関数
let Member = function(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
// メソッド定義
this.getName = function() {
return this.lastname + this.firstname;
}
}
// new演算子でコンストラクタ関数のインスタンス化
let mem = new Member('ひろし', '山田');
Prototype
コンストラクタ内でメソッドを定義する問題点
インスタンスが生成されるたびに、メソッドのコピーを生成されてしまう
上記例だと、 getName()
メソッドの実態がインスタンス × n個生成されてしまう。
そこでPrototypeオブジェクトを使う
JavaScriptは、関数を生成した瞬間に自動でprototypeというプロパティを用意している。
prototypeプロパティはデフォルトで空のオブジェクトを参照している。
function proto(){}
console.dir(proto.prototype);
定義した覚えのないprototypeオブジェクトが..ある
そしてJSの関数は、すごい..Objectです。
prototypeプロパティにメンバを格納することで、参照型のprototypeオブジェクト
に対してインスタンスも参照することになる。
↓
prototypeプロパティにメソッドを格納することで、無駄なコピーを防ぐことができる。
書き方
let Member = function(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
// prototypeを使ってメソッド定義
Member.prototype.getName = function() {
return this.lastname + this.firstname;
}
オブジェクトリテラルを使った書き方
let Member = function(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
Member.prototype = {
// key: value
setName: function() {
// 処理
},
getName: function() {
// 処理
}
}
this
JavaScriptは呼ばれる状況に応じてthisの参照先が変わる..変わるんです。
場所 | thisの参照先 | |
---|---|---|
1 | 関数内 | グローバルオブジェクト |
2 | イベントリスナー内 | イベントの発生元(buttonとか) |
3 | コンストラクタ | 生成したインスタンス |
4 | オブジェクト内 | 所属しているオブジェクト |
4. の例
let member = {
name: 'hiroshi',
age: 29,
getName: function() {
return this.name;
}
};
console.log(member.getName()); // 'hiroshi'
名前空間
名前空間を使うことで、グローバルスコープの汚染を防ぐことができる。
実態は空のオブジェクト。
宣言。
論理演算子 ||
を使って、MyAppの名前の競合がなかったら、空のオブジェクトを生成する。
let MyApp = MyApp || {}
名前空間にメンバを格納。
MyApp.member = {
firstname: 'hiroshi',
lastname: 'yamada',
age: 29
}
MyApp.member.prototype.getName = function() {
return this.lastname + this.firstname;
}
ファイル間でメソッドを共有
// util.js
(function(Util) {
// ②
Util.formatDate = function(date) {
// 処理
};
}(this.Util = this.Util || {})); // ①
// app.js
(function(Util) {
let today = Date();
// ④
Util.formatDate(today);
}(this.Util)); // ③
- this(globalオブジェクト)にUtilオブジェクトを格納
- 静的メソッドを宣言
- 即時関数の引数にthis.Utilを渡す
- Utilのメソッドを使用する