LoginSignup
227

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-02-23

この記事は、プログラマーになって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】名前空間のイロハ

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
227