【初級】知ってるつもりだったJavaScriptの基礎知識

  • 72
    いいね
  • 2
    コメント

この記事は、プログラマーになって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は、関数のいかなる場所で宣言した変数も 内部的に、先頭で宣言されたことになる
  • しかも宣言部分だけが先頭に移動し、 代入部分は移動しない

というルールが存在する。。マジか:fearful:

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で実装されていたプロトタイプベースの
オブジェクト指向のシンタックスシュガーなので、もともとの実装を知ることは重要。

クラス定義

  1. コンストラクタを宣言した関数を作る
  2. 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です。

prot.png

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'

参考
JavaScriptの「this」は「4種類」??

名前空間

名前空間を使うことで、グローバルスコープの汚染を防ぐことができる。
実態は空のオブジェクト。

宣言。
論理演算子 || を使って、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)); // ③
  1. this(globalオブジェクト)にUtilオブジェクトを格納
  2. 静的メソッドを宣言
  3. 即時関数の引数にthis.Utilを渡す
  4. Utilのメソッドを使用する

参考:
【脱初心者JavaScript】名前空間のイロハ