JavaScript初心者が失敗しがち/間違えがちなこと8選

フロントエンドを構築するにあたって必須となるプログラミング言語、JavaScript。

専業プログラマ以外も扱うことが多く、ブラウザさえあれば動作を確認できるという点で比較的とっつきやすいプログラミング言語といえます。しかしJavaScriptの言語仕様は、多くの初心者を混乱させる落とし穴も多く潜んでいるのです。

今回はJavaScript初心者が失敗しがち/間違えがちなことを8つピックアップしてみました。

1. 型の違いで想定外の挙動になってしまう

JavaScriptは「動的型付き言語」と呼ばれる、実行時の値によってデータ型が決まる言語のひとつです。動的型付き言語では、プログラマはコード中に型を書く必要がないので、初心者には「型」という用語自体がピンと来ない人もいるかもしれませんね。

JavaScriptのデータ型には「真偽値」「数値」「文字列」「シンボル」「undefined」「null」の6つのプリミティブ型と、オブジェクトがあります。オブジェクトとはデータと機能の集合であり、このオブジェクトにもまた型が存在します(オブジェクト型)。

これらの型をコード中に書くことはありませんが、「いま扱っている値の型がなにか」を意識しないままだと、思わぬエラーが出ることがあります。

例えば、HTML上に複数あるinputタグのうち、ふたつ目のinputタグのvalue属性値を取得するつもりで以下のコードを書いたとします。(なおコード中ではJavaScriptの代表的なライブラリ「jQuery」を使用しています)

// 間違ったコード
$("input")[1].val()

このコードを実行すると、エラーとして「Uncaught TypeError: $(…)[1].val is not a function」というメッセージがコンソールに出力されます。val()メソッドはたしかにHTML要素のvalue属性値を取得するメソッドのはずですが、なぜでしょう。

答えは、val()が「オブジェクト型がjQueryのオブジェクトを持つメソッド」だからです。

今回のケースでは、$(“input”)[1] はjQueryのオブジェクトではなく、HTMLInputElementオブジェクトなので、次のように書くのが正しいです。

// 正しいコード
$("input")[1].value

このように、型が違えば使用できるメソッド・プロパティも違います。

JavaScriptにおける型の判定にはtypeof演算子やinstanceof演算子などがありますが、もっとも標準的な型判定としてObject.prototype.toString.call()を利用するのが良いでしょう。

Object.prototype.toString.call()を使えば、オブジェクト型を取得できます。

// "[object Object]"が返却
Object.prototype.toString.call($("input"))

// "[object HTMLInputElement]"が返却
Object.prototype.toString.call($("input")[0])

// "[object NodeList]"が返却
Object.prototype.toString.call(document.querySelectorAll("input"))

// "[object HTMLInputElement]"が返却
Object.prototype.toString.call(document.querySelectorAll("input")[0])

なおjQueryのような外部ライブラリのオブジェクトや自作オブジェクトは、Object.prototype.toString.call()では[object Object]としか取得できません。

これは、Object.prototype.toString.call()が内部プロパティ[[Class]]を読み取っているためです。内部プロパティは書き換えられないので、標準で組み込まれているオブジェクト以外はすべて[object Object]と表示されます。

外部ライブラリや自作のオブジェクトの型を判定する場合は、instanceof演算子を使用して判定を行います。

const obj = $("input")
if (obj instanceof jQuery) {
  // true : jQueryオブジェクト
} else {
  // false : jQueryオブジェクトではない
}

instanceof演算子は「左辺のオブジェクトのプロトタイプチェイン」が「右辺のコンストラクタのprototypeプロパティ」を含むかどうかを判定し、真偽値を返却します。

  • プロトタイプチェイン:JavaScriptにおける継承モデル。オブジェクトが継承する「他のオブジェクトの参照(プロトタイプ)」の連鎖を意味する
  • コンストラクタ:新しいオブジェクトを作成する際に初期化を行う関数。JavaScriptでは関数もオブジェクトとして扱われているため、コンストラクタの扱いが一般的なプログラミング言語のそれと若干異なる
  • prototypeプロパティ:すべての関数オブジェクトが持つプロパティ。prototypeプロパティに設定したメソッドは、コンストラクタ関数がnewキーワードで新規作成したオブジェクトから参照できる

JavaScriptの経験が浅いうちは、これらの技術用語を理解できないかもしれません。そのときは、ひとまず「オブジェクトの型を判定しているのだな」と覚えておいて、あとでこれら用語の意味や使い方を学習するのもいいでしょう。

2. プロパティとメソッドを混同してしまう

オブジェクトはデータと機能の集合です。

JavaScriptにおいて、オブジェクトのデータは「プロパティ」、機能は「メソッド」と呼ばれています。

  • プロパティ(データ):文字列や数値などの値のみならず、オブジェクトも紐づけられる
  • メソッド(機能):function式やfunction宣言などで定義された、ひとまとまりの処理

初心者にありがちなのが、プロパティとメソッドを混同して使用し、思い通りに処理が実行されないケースです。

 /* inputタグのvalue属性値を取得するコード */

// 間違い(valueはメソッドではなくプロパティ)
document.querySelector("input").value()

// 正しい
document.querySelector("input").value

// 間違い(jQueryのval()はプロパティではなくメソッド)
$("input").val

// 正しい
$("input").val()

一見、簡単に修正できるミスですよね。しかしJavaScriptは、こういうちょっとしたミスの際に出力されるエラーが分かりにくいという、初心者泣かせなポイントがあります。

上記コードのうち、間違った書き方をしたふたつのコードを試しに実行してみましょう。

// valueはメソッドではなくプロパティ
document.querySelector("input").value()

// 出力されるエラー
// TypeError: document.querySelector(...).value is not a function

こちらは分かりやすいですね。エラー自体も「valueはメソッド(function)ではない」と書かれています。

問題は、次のコードを実行した場合です。

// valはプロパティではなくメソッド
$("input").val

// エラーが出力されない

このコードの場合、エラーが出力されません。そのためバグを見逃す可能性が高いのです。

なぜエラーが出ないのでしょうか。答えは、JavaScriptにおいて「メソッドは必ずしも実行しなくてもいい」からです。上記コードでは、「jQueryのval()メソッドを実行せず、ただ参照している」状態になっているのです。

試しに$(“input”).valをコンソールに出力してみましょう。

console.log($("input").val)

// 出力される内容
// ƒ (n){var r,e,i,t=this[0];return arguments.length?(i=m(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,k(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=k…

なにやら長い文字列が出力されましたね。これは、jQueryのval()メソッドのコードの内容そのものです。

JavaScriptにおいてメソッドは「Function型のオブジェクト」として扱われています。そのため、このように括弧をつけないでメソッドを参照した場合、メソッドを実行するのではなく、通常のオブジェクトの扱いと同様にただそのオブジェクトの値を取得してしまうのです。これは文法的に誤りではないので、エラーが出ません。

3. 処理の実行タイミングが早すぎてHTML上の要素を取得できない

JavaScriptはHTMLをDOM(Document Object Model)として扱います。

DOMはその正式名称のとおり、HTMLに記述されたドキュメントを、JavaScriptで扱えるオブジェクトとして構築されたものです。JavaScriptからHTML上の操作を行う場合、DOMの値を変化させることで間接的にHTMLを操作できます。

初心者にありがちなミスのひとつが、このDOMの取得タイミングです。例えば次のようなコードがあったとします。

See the Pen 正しいDOM取得順 by sig_Left (@sig_left) on CodePen.0

このコードは、操作するinputタグとspanタグの後にscriptタグが書かれているので、DOM操作が可能です。

しかし、scriptタグを書く位置を先頭にするとどうなるでしょう。

See the Pen 正しくないDOM取得順 by sig_Left (@sig_left) on CodePen.0

scriptタグのあとにinputタグとspanタグが書かれていますね。上記コードの場合、DOMが構築される前にJavaScriptが実行されるので処理に失敗します。

id = “target” のHTML要素が処理実行段階ではDOMに存在しないため、document.querySelector(“#target”)の値がnullになるのです。nullはvalueプロパティを持たないので、コンソールに「Cannot read property ‘value’ of null」というエラーメッセージが出力されます。

このように、JavaScriptからHTML上の要素を操作する場合は、処理を実行するタイミングでDOMが構築されているかを気にする必要があります。

なお、scriptタグの位置が操作したいHTML要素のタグよりあとに記述されている場合でも、次のように書くことでDOMを取得できます。

See the Pen loadイベントでDOM構築後に実行 by sig_Left (@sig_left) on CodePen.0

window.onloadはグローバルオブジェクト「window」のloadイベントに対応するイベントハンドラです。window.onloadに設定された関数は、DOMの構築やリソースの読み込みが完了したloadイベント発生時に実行されるようになります。イベントハンドラを利用することで、処理の実行タイミングを制御できるのです。

4. ハイフンがつくCSSプロパティはJavaScriptではキャメルケースになる

margin-bottomやbackground-colorのように、CSSプロパティの名前にはよくハイフンが使われています。JavaScriptからCSSを操作する際、気をつけなければならないのが「CSSプロパティ名のハイフン置き換え」です。

CSSは本来、プロパティ名を「ケバブケース」で扱いますが、JavaScriptのオブジェクト内においてCSSプロパティ名は「キャメルケース」に変換されます。

  • キャメルケース:単語と単語の区切りを大文字の英語で表現する(例:marginBottom)
  • ケバブケース:単語と単語の区切りをハイフンで表現する(例:margin-bottom)
  • スネークケース:単語と単語の区切りをアンダーバーで表現する(例:margin_bottom)

JavaScriptから、HTMLのインラインCSSを取得するコードを見てみましょう。

See the Pen CSSプロパティ名はJSでキャメルケースに変換される by sig_Left (@sig_left) on CodePen.0

インラインCSSはJavaScriptにおいて、DOMツリー上の「HTML要素のstyleプロパティ」に設定されます。上記コードのとおり、このときstyleプロパティにはCSSプロパティ名がキャメルケースに変換された状態で紐づいています。

これは、JavaScriptの文法では「オブジェクト.プロパティ名」でプロパティを指定するとき、ハイフンが間に挟まれていると、それが「プロパティ名のハイフン」ではなく「演算子(マイナス)」に認識されてしまうことが原因です。「オブジェクト[“プロパティ名”]」形式で定義することはできますが、推奨されていません。

オブジェクトのプロパティとしてCSSを操作する場合には、プロパティ名がキャメルケースに変換されていることを忘れないよう気をつけましょう。

(なおjQueryにおける「引数がふたつのcss()メソッド」など、CSSを「メソッドの引数に文字列として指定する」タイプのものは、CSSプロパティ名をケバブケースで指定するようになっています。どのパターンのときにどの形式の命名になっているかにも留意しましょう)

5. 一部ブラウザで非実装の仕様がある

JavaScriptの言語仕様は、ブラウザがどう実装しているかに依存しています。

ECMAScriptというJavaScriptの標準規格がありますが、古いブラウザでは最新の標準仕様の実装を追いきれず、一部のメソッドや構文などが未実装のままのケースがあります。そのためJavaScriptでは、作成したコードが一部のブラウザで正常に動作しないというバグが発生することがあります。

特にInternet Explorerは、現在も多くのJavaScriptの言語仕様が取り込まれていません。にもかかわらず、作成するアプリやシステムの要件に「Internet Explorerでも正常に動くこと」と指定されているケースが多いのが現場の実情です。使用するメソッドや構文がInternet Explorerで対応しているかどうかは、よく確認しておく必要があります。

また、Internet Explorerのような古いブラウザでなくとも、最新の言語仕様については各ブラウザがすべて対応できているとは限りません。昨今JavaScriptは1年に一度のペースで言語仕様を更新しており、年々新たなメソッドや構文が追加されています。

▲ECMAScript2018で追加された正規表現のdotAllフラグは、2019年現在でもまだ実装されていないブラウザが多い

さらにはWeb上で音声を扱う「Web Audio API」や、現在実験的に実装が進められているVR操作用の「WebVR API」など、ごく一部のブラウザのみで実装されているAPIの存在もあります。

JavaScriptを扱う場合、使用する機能がブラウザで対応しているかどうかに気をつけましょう。

6. if文内で比較と代入を間違えてもエラーが出ない

JavaScriptにおいて等号の比較演算子は「==」、代入の演算子は「=」です。イコールの数だけで意味が変わってしまうため、ときには演算子を書き間違えることもあるでしょう。

このとき厄介なのが、if文内で比較と代入を間違えてもエラーが出ないということです。

var num = 1

// 正しく等号を書いた場合
if (num == 3) {
  console.log("num is 3")
} else {
  console.log("num is not 3")
}
// 出力される内容:num is not 3

// 間違えて代入した場合
if (num = 3) {
  console.log("num is 3")
} else {
  console.log("num is not 3")
}
// 出力される内容:num is 3(エラーは出ない)

なぜ間違えて代入してもエラーが出ないのでしょう。このときのnumの値を見てみましょう。

var num = 1
if (num = 3) { } else { }
console.log(num)
// 出力される値:3

もともと1として宣言されていた変数numが3に変わっています。条件判定で間違えて書いた代入も、式としては正しく処理されてしまうのです。構文的に誤りであるとは見なされないため、エラーは出ません。そのため間違いを見逃してしまうことがあります。

代入が通常どおり式として実行されるので、上記コードの if (num = 3) は if (3) と同じ意味になります。JavaScriptにおいて0以外の数値は条件判定でtrueと見なされるため、上記コードではelse文ではなくif文内のブロックに処理がうつったのです。

7. obj.strがobj[“str”]として認識されてしまう

オブジェクトのプロパティにアクセスするには、以下のふたつの表記法があります。

  • ドット表記法:オブジェクトとプロパティ名をドットでつなぐ
  • ブラケット表記法:括弧内の文字列でプロパティ名を指定する
// オブジェクトobjのプロパティ"foo"にアクセスする

// ドット表記法
obj.foo

// ブラケット表記法
obj["foo"]

これらの表記で間違えやすいのが、「変数に格納された文字列のプロパティ名を作りたい」というケースです。

// オブジェクト
var obj = {}

// 作成したいプロパティ名を格納
var str = "foo"

// 正しい ( 変数strの値"foo"がプロパティ名になる {foo: "something"} )
obj[str] = "something"

// 間違い ( 変数strではなく文字列"str"として解釈される {str: "something"} ) 
obj.str = "something"

このように、ドット表記法ではプロパティ名に変数を指定することはできません。ドットで繋げた文字が、そのままプロパティ名として解釈されてしまうので、「変数str」ではなく「文字列str」として認識されてしまうのです。

変数に格納された文字列をプロパティ名に使用したい場合、ブラケット表記法で書かなければなりません。ブラケット表記法ではプロパティ名を「括弧内の文字列型の値」で指定するので、以下のように変数を指定することもできます。

var obj = {}
var str = "foo"

// 変数名を指定 {foo: "something"}
obj[str] = "something"

// ブラケット表記法は文字列型の値でプロパティ名を指定
console.log(obj["foo"]) // "something"が出力される

変数内の文字列をプロパティ名にしたい場合は、ブラケット表記法を使いましょう。

なおブラケット表記法では括弧内が文字列型の値であればいいので、式でプロパティ名を算出することもできます。

var obj = {}
var str = "foo"
var post = "_age"

// ブラケット表記法は括弧内で式も書ける
obj[str + post] = 28

// 出力される内容: {foo_age: 28}
console.log(obj)

8. thisが示す値が文脈によって変わってしまう

JavaScriptにおける「this」の扱いは、他言語と比較して少々特殊です。

通常、プログラミング言語において「this」は、自身が所属するオブジェクトを指します。主にインスタンスメソッド内で記述され、所属するオブジェクトの他のメソッドやフィールド(JavaScriptにおけるプロパティ)を参照するために使用されます。

一方JavaScriptでは、thisは関数の呼ばれ方によって示す値を変えます。一般的な意味合いのthisと同様の挙動を示すものもありますが、そのほかさまざまなパターンがあります。

以下でひとつずつ見ていきましょう。

オブジェクトのメソッド内のthis

// オブジェクトのメソッド内のthisは「呼び出したオブジェクト」を示す
String.prototype.foo = function() {
  return "オブジェクトのメソッド内のthis:" + this
}
console.log("文字列1".foo())
// オブジェクトのメソッド内のthis:文字列1
console.log("文字列2".foo())
// オブジェクトのメソッド内のthis:文字列2

上記コードでは、Stringコンストラクタのprototypeプロパティにfooメソッドを追加しています。String.prototypeに追加したメソッドは、Stringを継承する全てのオブジェクトで使用可能になるため、”文字列”.foo()という形式でメソッドを実行できます。

このとき、foo()内のthisはメソッドを実行した「”文字列1″」や「”文字列2″」を示します。オブジェクトのメソッド内に記述したthisは、自身を呼び出したオブジェクトを示すのです。

アロー関数のthis

アロー関数とは、ECMAScript2015で追加された比較的新しいJavaScriptの関数定義構文です。function式より短く関数定義を書ける構文ですが、thisを宣言した場所で固定するという特徴があります。

// アロー関数のthisは、定義したそのスコープのthisの値のまま固定される
String.prototype.hoo = () => "アロー関数のthis:" + this

console.log("文字列1".hoo())
// アロー関数のthis:[object Window]
console.log("文字列2".hoo()) // アロー関数のthis:[object Window][/code]

宣言した箇所のthisで固定されるため、通常の「オブジェクトのメソッド内のthis」とは異なり、誰に呼ばれてもthisの値が変わりません

コンストラクタ関数のthis

コンストラクタ関数はnewキーワードで新しいオブジェクトを作成します。

コンストラクタ関数内のthisは、この「新しく作成するオブジェクト」を示します。

// コンストラクタ関数のthisは「新規作成するオブジェクト」を示す
var Member = function(name, address) {
  this.name = name
  this.address = address
  console.log("コンストラクタ関数内のthis:", this)
}
var sig_Left = new Member("sig_Left", "okinawa")
// コンストラクタ関数内のthis: ▶︎ Member {name: "sig_Left", address: "okinawa"}

var sig_Right = new Member("sig_Right", "aichi")
// コンストラクタ関数内のthis: ▶︎ Member {name: "sig_Right", address: "aichi"}

DOMイベントハンドラのthis

DOMイベントハンドラとは、ブラウザ上のクリックやマウスオーバーなどのイベントを検出して関数を実行するものです。

DOMイベントハンドラ内では、thisは「イベントを発火させたHTML要素に対応するDOM」を示します。

// DOMイベントハンドラのthisは「イベントを発火させたDOM」が入る
document.querySelector("button").addEventListener("mouseover", function(e) {
  console.log("DOMイベントハンドラのthis:" + this)
})
// DOMイベントハンドラのthis:[object HTMLButtonElement]

インラインイベントハンドラのthis

インラインイベントハンドラとは、HTMLのbuttonタグのonclick属性や、inputタグのonchange属性、onblur属性などを示します。通常のDOMイベントハンドラと異なり、HTML上に書くものです。

See the Pen インラインon-イベントハンドラ内のthis by sig_Left (@sig_left) on CodePen.0

HTML上のonclick属性内に書く ”もっとも浅い階層” のthisには、「イベントが発火したDOM」が割り当てられています。しかしそれより深い階層(割り当てたfunction内)では、thisはグローバルオブジェクトであるWindowを示していることに気をつけてください。

これはもっとも浅い階層がDOMのonclickメソッドとして所属しているのに対し、そのメソッド内のfunctionはどのオブジェクトにも所属していないことが原因です。

同じ挙動はイベントハンドラ以外でも再現できます。

String.prototype.hoge = function() {
  console.log("浅い階層:", this)
  
  // オブジェクトのメソッド内で、どのオブジェクトにも属していないfunctionを定義すると
  const h = function() {
    console.log("入れ子になったfunction:", this)
  }
  h()
}
"hogehoge".hoge() 
// 浅い階層: ▶︎ String {"hogehoge"}
// 入れ子になったfunction: ▶︎ Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

浅い階層ではオブジェクトに属しているので、メソッド内で定義したfunctionも同じくオブジェクトに属していると思いがちです。しかしこのように、メソッド内であっても明示的に所属するオブジェクトを指定しない限り、thisはグローバルオブジェクトに変換されるのです。

strictモード時のthis

JavaScriptにはstrictモードという、より最適化された構文のみを許可するモードがあります。通常のJavaScriptでは、オブジェクトに属していないfunctionのthisは自動的にグローバルオブジェクトに変換されますが、strictモードではthisの自動変換は行われません。

// strictモード時の「オブジェクトに属していないfunction」
function func() {
  "use strict"
  return this
}
console.log(func())
// undefined

グローバルオブジェクトへの自動変換が行わないため、オブジェクトに属していないfunctionのthisは「undefined」のままです。

また、(これはあまり実務では意識しなくても良いことですが)通常モードのJavaScriptで行われているthisの「プリミティブ型の値からオブジェクトへの変換(文字列からString型オブジェクトへの変換など)」も、strictモードでは行われなくなります。

// strictモードなし
String.prototype.non_strict_exe = function() {
  return typeof this  // typeofは「データ型」を取得する(※オブジェクト型ではない)
}
console.log("str".non_strict_exe())
// 出力される内容:object
// (プリミティブ型の文字列が、Stringオブジェクトに自動変換されている)

// strictモードあり
String.prototype.strict_exe = function() {
  "use strict"
  return typeof this
}
console.log("str".strict_exe())
// 出力される内容:string
// (プリミティブ型のまま、変換されない)

まとめ

動的型付きプログラミング言語であるJavaScriptは、言語仕様について深く知らずとも「なんとなく」で書けることが多い言語です。

しかし業務で出くわすJavaScriptの想定外な挙動の多くは、JavaScript独自の言語仕様を理解していないと何が原因でエラーになったのかを明らかにするのが難しいでしょう。

JavaScriptの言語仕様を正確に理解して、エラーが起きたときに「何をどうすればいいか」を自分で組み立てられるようになれば、初心者の一歩先にいけるでしょう。

こちらもおすすめ!▼

SHARE

RELATED

  • お問い合わせ
  • お問い合わせ
  • お問い合わせ