[js: JavaScript Note] Basic knowledge notes of JavaScript. #js
JavaScript という言葉は狭義には Mozilla が仕様を策定し実装しているスクリプト言語を指す。 このスクリプト言語は Ecma インターナショナルで ECMAScript (ECMA-262) として標準化されており、多くのウェブブラウザ等はこの標準化された ECMAScript を実装している。(Wikipedia)
WEB 上でインタラクティブな表現をする為に開発されたオブジェクト指向のスクリプト言語。現在ブラウザ上で動作(ブラウザ上で解釈・実行される)する唯一のプログラミング言語。Ajax による非同期通信や HTML5 ウェブプラットフォームの普及、近年の SPA などリッチクライアントアプリの流行を受け急成長した。また、それとは別に Node.js など新たなプラットフォームの出現により、サーバサイドでの実行環境も整備されつつあり、フロントエンド~バックエンドまで利用ケースが拡大している。
[基礎知識] JavaScriptの歴史
JavaScript: The First 20 Years - JavaScriptの歴史については「JavaScript: The First 20 Years」を読む
JavaScript 25 周年
以下のような歴史をたどっており、名前も仕様もかなりこんがらがっとる。Web リファレンスの検索時は「一体いつの情報なのか?」に注意。
MDN - Mozilla Developer Network
標準ビルトインオブジェクト
EcmaScript
Node.js
なんかわかんないコトはオフィシャルなマニュアルを参照。
Google流JavaScriptにおけるクラス定義の実現方法(ES6以前)
秘匿化に向けたJavaScriptの旅
不要なクラス宣言、やめちゃおっか?
言語に依らずクラスを独自実装したくなるなら FW / ライブラリ導入を検討した方がいい。
あとカプセル化 (private メンバ) 欲しいだけなら モジュールパターン も検討していい。
// モジュールパターンで秘匿性を頑張る
export const users = (() => {
const _users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
];
const all = () => {};
const find = (id) => {};
const add = (id) => {};
const remove = (id) => {};
// _users は秘匿されている
return { all, find, add, remove };
})();
そもそも js は関数が第一級オブジェクトなので、関数で解決できるものが多い。react なんかの一般的な js ライブラリ / FW も最近は関数型ちっくに実装するので、独自でクラスを実装していく必要性があまりない。
int, str, bool, null
)
int
, str
など基本的な型は全てオブジェクトの prototype
が用意されておりインスタンス化できるが プリミティブのままでも存在できるvar str = "hello";
と var str = new String("hello");
が別物になるArray.prototype
のようにプロトタイプ ( 親クラスみたいなヤツ ) が存在するPHPと比べてとくに注意すべきは undefined
。存在チェックは if (value == null)
が null
と undefined
をいっぺんにチェックできてよさげ?困ったら (variable) ? 'あるよ' : 'ないよ'
みたいに ()
で評価しちゃうのが早いかも。
// Is falsy ?
console.log( Object ? true : false); // true
console.log( undefined ? true : false); // false
console.log( null ? true : false); // false
console.log( true ? true : false); // true
console.log( false ? true : false); // false
console.log( 0 ? true : false); // false
console.log( 1 ? true : false); // true
console.log( -1 ? true : false); // true
console.log( '' ? true : false); // false
console.log( 'a' ? true : false); // true
console.log( [] ? true : false); // true
console.log( {} ? true : false); // true
// Is blank array ?
console.log( [].length ? true : false); // false
console.log( ['a'].length ? true : false); // true
// Is blank object ?
console.log( Object.keys({}).length ? true : false);
console.log( Object.keys({a:'a'}).length ? true : false);
// __get() Property of blank object
let obj = {value:'hoge'}
console.log(obj.key); // Becomes 'undefined' rather than error.
in - MDN
Object.prototype.hasOwnProperty() - MDN
[Javascript] オブジェクトが指定されたプロパティを保持しているかどうかの判定
基本的に両者は同じだが Object.prototype.hasOwnProperty()
は子孫の prototype まで遡らず「判定対象の、そのオブジェクトにあるかどうか?」を判定する。対して in
は子孫の prototype まで遡る。以下では toString()
実装の有無について判定に違いが出る。
// in
const obj = { a: 1, b: "xxx" }
console.log('a' in obj) // true
console.log('x' in obj) // false
console.log('toString' in obj) // true
// hasOwnProperty()
console.log(obj.hasOwnProperty('a')) // true
console.log(obj.hasOwnProperty('x')) // false
console.log(obj.hasOwnProperty('toString')) // false
const falsy = null
const var1 = falsy || 'Default Value'
console.log(var1) // Default Value
const string = 'Not Falsy'
const var2 = string || 'Default Value'
console.log(var2) // Not Falsy
他スクリプト言語と同様の動的型づけのほかに、プリミティブ → オブジェクトへの変換が起こる。string
がプリミティブな文字列だった際に string.substr()
のように定義していないはずのプロパティやメソッドにアクセスしようとした時 string
はプリミティブな文字列型から String Object
に変換されアクセスが続行される
JavaScript は配列も「 Array オブジェクト」を prototype に持つオブジェクトなので、メソッドやプロパティを持っている。また PHP の場合は「配列」と「連想配列」が内部的には同じ「連想配列」として処理されるが、JavaScript をはじめ多くの言語では 配列は純粋な列挙データ となり、添え字は必ず 0 始まりになる ( PHP では配列に [2 => 'fuga', 14 => 'piyo']
のような状態が存在するが一般的な配列でこの状態はありえない ) 。
const newArray = [1, 2, 3]
console.log(newArray) // [1, 2, 3]
console.log(newArray.length) // 3
console.log(newArray[0]) // 1
// Array like なオブジェクトから配列を生成 - Array.from()
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from
const arrayFromString = Array.from('hello')
console.log(arrayFromString) // ['h', 'e', 'l', 'l', 'o']
// 配列への要素追加 - Array.prototype.push()
const animals = ['pigs', 'goats', 'sheep']
animals.push('cow')
console.log(animals) // ['pigs', 'goats', 'sheep', 'cow']
// 配列への要素追加 - Array.prototype.concat()
// push() と違ってこちらは非破壊 ( 引数にとった要素を破壊せず新規要素を生成 )
const array1 = ['a', 'b', 'c']
const array2 = ['d', 'e', 'f']
console.log(array1.concat(array2)); // ['a', 'b', 'c', 'd', 'e', 'f']
// 配列要素へクロージャーを適用し、新たな配列を返す - Array.prototype.map()
const numbers = [1, 4, 9, 16]
const results = numbers.map(function(number) {
return number * 2
})
console.log(results) // [2, 8, 18, 32]
JavaScript の {}
は PHP でいう「連想配列」と混同されがちだが、これは「オブジェクト」であり中身は全てプロパティ・メソッドになる。Array
なのか Object
なのかで利用できるメソッドが変わる ( prototype
が違うので ) ため、この違いをしっかり認識すること。
var myObject = {
value: 10,
show: function() {
console.log(this.value)
}
}
myObject.value = 2
myObject.show() // 2
オブジェクトのプロパティやメソッドに myObject.myProperty
のようにアクセスできる。この際に返り値がオブジェクトであれば ( オブジェクトでなかったらキャストされる ) 再びドットつなぎで次のオブジェクト・メソッドに渡すことができる。この鎖のような連結した書き方をドット記法・チェーン記法と呼ぶ。
this.property
this.method()
var arr = [1, 2, 3, 4, 5]
var result = arr.slice(0,4).slice(0,3)
配列への添え字アクセス array[0]
や、オブジェクトのプロパティアクセス object['hogeProperty']
に用いる。また、メソッドチェーンでは呼び出す際のキーに変数を用いた動的な呼び出しを行えないが、ブラケット記法であれば可能。
const myObject = {
name: 'John',
}
const index = 'name'
console.log(myObject.index) // ng
console.log(myObject[index] // John
オブジェクトのキー名を動的にしたいときはこうじゃ。
const key = 'my-key'
const value = 'my-value'
const obj = {
[`template_literal_${key}`]: value
}
なんてことない普通の for
。主に配列や回数指定ループに利用する。
let sum = 0
let array = [1, 3, 4, 7]
for(let i = 0; i < array.length; i++) { // 配列の長さ分の繰り返し
sum += array[i]
}
console.log(sum) // 15
Iterable な Object からキーを取り出す。反復処理に不必要なキーでも引っ張てきちゃうので注意 参考 。
let obj = { foo : 'hello', bar : 'world' }
for ( let key in obj ) {
console.log( key + '->' + obj[key] ) // 'foo->hello', 'bar->world'
}
Iterable な Object に対して列挙可能なコレクションを抜き出しながらループ処理を行う。利用可能なオブジェクトは Array, Map, Set, String, TypedArray など。予め列挙可能なアイテムがオブジェクト内に定義 ( Symbol.iterator ) されている必要がある。
また NodeList は Iterable なので列挙可能だが HTMLCollection は Not Iterable なので for of で回せない など、オブジェクトによって利用可能なものとそうでないものがあるので注意。 - 参考 。
let score = [70, 65, 55, 80, 100]
for (let s of score) {
console.log(s) // 70 65 55 80 100
}
es6 からの新演算子 ...
で 配列ライクなオブジェクト ( 正確には for of で展開できるオブジェクト ) を個々の値に展開する。純粋な配列の展開だけではなく Iterable Object なら何でも内包する反復要素全てを展開できるのがポイント 。ここで展開されたものから新たな配列を [...items]
みたいな感じで作れるので 何か → 配列への変換がらくちん になる理解でいいのかな。
function myFunction(x, y, z) { ... }
var args = [0, 1, 2];
// 配列を関数の引数として使用したい場面では
// 慣習的に Function.prototype.apply が使用される
myFunction.apply(null, args);
// ES2015 の スプレッド演算子 を利用すると以下のようになる
myFunction(...args);
// 可変長引数としても利用可能
const sum = (...nums) => nums.reduce((p, c) => p + c, 0); // reduce() - https://goo.gl/PnENoA
console.log(sum(1, 2, 3, 4, 5)); // 15
// オブジェクトの部分更新代入とか
const user = { model: 'user', id: 1, name: 'john', age: 19 };
const updatedUser = {
...updatedUser, // updatedUser の key value を展開して同じ object に
age: 20, // ↑ ののち age キーだけを更新
};
const data1 = [1, 2, 3];
const data2 = ['d', 'e', 'f'];
// push
data1.push(...data2); // ループや Array.prototype.push.apply(data1, data2) を使わなくてよくなる
console.log(data1); // [1,2,3,"d","e","f"]
// unshift
data1.unshift(...data2);
console.log(data1); // ["d", "e", "f", 1, 2, 3]
// merge
const merged = ['あ', ...data1, 'い', ...data2, 'う'];
console.log(merged); // ["あ",1,2,3,"い","d","e","f","う"]
// 分割代入 - https://goo.gl/oL114d
let [a, b, ...other] = [1, 2, 3, 4, 5];
console.log(a); // 1
console.log(b); // 2
console.log(other); // [3, 4, 5]
// 文字列 → 配列
const word = 'JavaScriptプログラミング';
const converted = [...word]; // .split() や Array.from(word) で生成しなくてよくなる
console.log(converted); // ["J","a","v","a","S","c","r","i","p","t","プ","ロ","グ","ラ","ミ","ン","グ"]
// 配列から重複を取り除く - Set Object https://goo.gl/RWGqkU
const data = ['a', 'b', 'c', 'a', 'b', 'd'];
const dist = [...(new Set(data))];
console.log(dist); // ["a","b","c","d"]
// Set オブジェクトは与えられた Iterable Object から一意の値のみコレクションしたオブジェクトを返す
// 上記では返ってきた Set オブジェクトから配列生成している
// 配列コピー
const ary = ['Pen', 'Pineapple', 'apple'];
const myAry = [...ary]; // Array.prototype.slice.call を使わなくて良くなる
console.log(ary === myAry); // false → 新たな Array オブジェクトとして生成され...
console.log(myAry); // ["Pen","Pineapple","apple"] → 中身はコピーされている
// 配列コピー時、中のオブジェクト要素参照は保たれる
const fruits = [
{ 'banana': 100 },
{ 'cherry': 200 }
];
const myFruits = [...fruits];
fruits["0"].banana = 300;
console.log(myFruits); // [{"banana":300},{"cherry":200}] → 変わってるね!
// HTMLCollection から配列つくって配列系メソッドでループ操作する
const elems = document.getElementByClassName('checkboxClassName');
const filtered = [...elems].filter(el => el.checked && el.classList.contains('foo'));
setCheckBox('fruits', 'apple', 'banana');
// クロージャー ( 無名の関数オブジェクト ) を生成し即実行する手法
// 読み込み時に即実行され、かつグローバル空間を汚染しないため
// es6 以前のモジュール分割ができなかった時代に重宝された
(function () {
var scopedVar = 'hello!';
function scopedFunc () {
alert('world!')
}
console.log(scopedVar) // hello!
scopedFunc() // Display alert 'world!'
})()
// グローバルでは undefined になる
console.log(scopedVar) // undefined
scopedFunc() // undefined
es6 より追加された機能で function () {}
式の別の書き方で {}
スコープ内で this, arguments, super, new.target
などを束縛しない ( 上位スコープの this
などがそのまま利用できる ) 。また内容によって return
や引数を省略した記述が可能。
var materials = [
'Hydrogen',
'Helium',
'Lithium',
'Beryllium'
]
console.log(materials.map((material) => {
return material.length
}))
// Array [8, 6, 7, 9]
// 省略記法
console.log(materials.map(material => material.length));
// なんでか意味わかんないけど parentheses で囲えばいいみたい
(p) => ({ foo: 'bar' });
オブジェクトの定義スコープ内で「自身オブジェクト」を参照する擬似演算子。基本的に this
は常に現在のスコープから見た「呼び出し元オブジェクト」を参照する。
// object 定義内の this
var myObject = {
value: 10,
show: function() {
console.log(this.value); // this は現在スコープを呼び出す myObject オブジェクトを指す
}
}
myObject.show(); // 10
// global で宣言された function 内の this
function show() {
console.log(this); // this は現在スコープ function の呼び出し元であるグローバル window を指す
this.value = 1; // value はグローバル変数になる (window.value)
}
show(); // this は グローバルオブジェクト window をさす
this
は 現在スコープによってその参照を変えてしまうが アロー関数 を利用することでその束縛を回避できる。
window.onload = function (){
var node = this.document.querySelector('#node')
node.addEventListener('click', (evt) => {
console.dir(this)
// this は関数スコープに束縛されず Event オブジェクトではなくグローバルの window オブジェクトを指す
})
}
JavaScript は純粋オブジェクト指向につき Node.js
環境下を除いたブラウザ実行環境下では、グローバルに暗黙の最上位オブジェクト window
が存在する。
// トップレベル ( 暗黙の window )
var hoge = "fuga";
window.foo = "bar";
console.log(this.hoge + "+" + this.foo); // fuga+bar
(function(){
console.log(this.hoge + "+" + this.foo);
})(); // fuga+bar
// 関数内(≠メソッド) > トップレベル参照
var func = function() {
console.log(this.foo);
};
func(); // undefined (window.foo)
JavaScript のモジュール管理は死ぬほどややこしい歴史があるが、平たく言えば「別の JS ファイルなどのリソースからオブジェクトやらなんやら引っ張ってくる、あるいは引っ張ってもらえるようにする」ための仕組み。Browserify や CommonJS など様々なモジュール解決のツールや API が存在したが、現在は nodejs
環境下の require()
API と、es6 構文である import
および export
で落ち着いてる ( ぽい ) 。
require()とは何か?何が便利なのか
ES6のexportについて モダンなJSの話──importとexport
[意訳]初学者のためのJavaScriptモジュール講座 Part1
// import
import Hoge from './Hoge.js'
const hoge = new Hoge() // hello!!
//Hoge.js for import
export default class Hoge {
constructor () {
this.hello();
}
hello () {
console.log("hello!!")
}
}
const Hoge = require('./Hoge.js')
const hoge = new Hoge() // hello!!
//Hoge.js for require()
module.exports = class Hoge {
constructor () {
this.hello();
}
hello () {
console.log("hello!!")
}
}
JavaScript は純粋オブジェクト指向言語なので、String や Array などのデータは全てオブジェクトとして実装されている。これと同様に JavaScript は HTML 操作においても、ブラウザが HTML を解釈する際に利用する DOM インタフェースを利用するために実装された各種オブジェクト をとり扱うことで実現している。
DOM とは Document Object Model の略で、HTML および XML ドキュメントへアクセスするための API 。ブラウザは HTML を解釈する際にその内部のドキュメントを「ツリー構造のデータ集合」として取り扱い DOM ツリー を生成する。このツリーの1つ1つのオブジェクトは Node と呼ばれ、親子関係を持つ。JavaScript は このツリーを経由して、HTML の内容や表現を変更できるようになっている。これら仕様の標準化は W3C によって行われている。
JavaScript でいうところの document
は DOM ツリーのエントリーポイント ( ルート ) であり、DOM API の Document インタフェースを継承したオブジェクト。通常、ブラウザを操作するためのグローバルオブジェクト window
配下にぶら下がっている ( document.body
のようにアクセスする場合、暗黙のトップレベルオブジェクト window
を省略したことになる → window.document.body
) 。
これに対して window はブラウザへのアクセスのためのオブジェクト。JavaScript はブラウザをプラットフォームとしたプログラミング言語なので、履歴 ( History ) や アドレスバー ( Location ) に対するアクセスを可能とする API オブジェクト ( ブラウザオブジェクトなどと呼ばれる ) を標準で保持している。
こちらは DOM API の Element インタフェースを継承したオブジェクト。HTML の一要素(ノード)を document.getElementById()
や document.querySelector()
なんかで取得した場合は DOM API の Element
と HTMLElement
インタフェースを継承した Element オブジェクトということになる(ややこしいんですけど...)。DOM ツリー内の特定ノード。大体の場合はまぁタグ要素のことと思っておけばいい。
document.forms
や node.children
や document.getElementsByClassName()
や document.getElementsByTagName()
なんかで取得できる 要素集合 (ノード集合) を HTMLCollection
と呼ぶ。これらは上記 Element
オブジェクトの集合であり 配列ライクなオブジェクト みたいな表現をされる。HTMLCollection
は配列ライクなのだが Not Iterable
なためループ処理時に Array.from()
にぶっこんで配列キャストするひと手間が必要。また、ノードが変更された際は動的に変化するという ライブな特徴を持つ 。
上記に対して、document.querySelectorAll()
では NodeList
と呼ばれる 静的な(ライブでない)要素集合 が取得できる。HTMLCollection
と違い内包する Element Node
を含む全てタイプのノードを取得するのが特徴。また、こちらは Iterable
なため for of
とかでさっさとループできたりする。ややこしいんですけど...?
NodeListとHTMLCollectionも別物なので気を付けよう。
HTMLCollection vs NodeList
ParentNode.childrenをfor...ofループするときの注意点
配列ライクなオブジェクトをforEachするときのイディオム
getElementsByTagName()
および getElementsByClassName()
について、ブラウザ間で返り値が異なり、Chrome は NodeList を FireFox は HTMLCollection を返すという意味不明な仕様がある。それぞれループ処理にぶっこむ作法が違うので混同すると結構危険なことになる。
querySelectorAll()
で毎回 NodeList
をとるか、どっちが来ても大丈夫なように配列キャストをするのが今風みたい。
Event - MDN
イベントリファレンス
DOM イベントリファレンス - Drafted
addEventListener
ブラウザには DOM ツリーの他に「マウスでクリックした」「リロードした」などの イベント が存在する。JavaScript はこのようなブラウザで起こる様々なイベントに対して、イベントがトリガーした際の「コールバック」を「リスナー」として登録する ... といった「イベント駆動型プログラミング」に適している。
<table id="outside">
<tr><td id="t1">one</td></tr>
<tr><td id="t2">two</td></tr>
</table>
// t2 のコンテンツを変更する関数
function modifyText() {
var t2 = document.getElementById("t2");
if (t2.firstChild.nodeValue == "three") {
t2.firstChild.nodeValue = "two";
} else {
t2.firstChild.nodeValue = "three";
}
}
// イベントリスナーを table に追加
var el = document.getElementById("outside");
el.addEventListener("click", modifyText, false);
以下 jQuery のイディオムを Native JavaScript で書いたらどうなるの集。
$(document).ready(function(){...})
は DOMツリー完了。window.onload = function(){....}
は 全体読込完了。jQueryなしDOMツリー完了は以下。
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded')
})
// ※Riot.jsなど仮想DOM系はこれで捕まらないよ
window.addEventListener('load', function() {
console.log('loaded')
})
// jQuery val()
document.querySelector('#my-input').value
document.querySelector('#my-input').value=1
// jQuery attr()
el.getAttribute('foo')
el.setAttribute('foo', 'bar')
// readonlyとかは el.readOnly = true とか可
// jQuery data()
el.getAttribute('data-foo')
el.dataset['foo'] // ie11~
// jQuery
$('.class')
// Native - jQueryぽくかけます
document.querySelector('#id')
document.querySelectorAll('.class') document.querySelectorAll('a[target=_blank]')
// Native-oldPattern document.getElementsByClassName('class')
// jQuery
$el.find('li')
// Native
el.querySelectorAll('li')
// jQuery
$el.css({ color: "#ff0011" })
// Native
el.style.color = '#ff0011'
// jQuery
$el.addClass(className) //追加
$el.removeClass(className) //削除
$el.hasClass(className)//有無
// Native
el.classList.add(className)
el.classList.remove(className)
el.classList.contains(className)
// jQuery
$el.html()
$el.html(htmlString)
// Native
el.innerHTML
el.innerHTML = htmlString
// jQuery
$el.prepend("<p>hello</p>")
$el.append("<p>hello</p>")
$el.remove()
// Native
el.insertAdjacentHTML("afterbegin","<p>hello</p>")
el.insertAdjacentHTML("beforeend","<p>hello</p>")
el.parentNode.removeChild(el)
// jQuery
$el.clone()
// Native
el.cloneNode()
// jQuery
$(document).ready(function(){
$('#node').on('click',function(){
console.dir($(this))
})
})
// es6
window.onload = function (){
this.document.querySelector('#node')
.addEventListener('click',function(evt){
console.dir(this)
})
}
誰よりも先にイベントリスナを呼び出す 通常 addEventListener で登録されたイベントリスナは登録された順番に実行される
但し useCapture 引数を true で渡すと誰よりも先に実行される
// target.addEventListener(type, listener[, useCapture]);
document.addEventListener('click', function() { alert('2'); });
document.addEventListener('click', function() { alert('1'); }, true);
document.addEventListener('click', function() { alert('3'); });
// select に option 追加
var select = document.querySelector('#select')
var option = document.createElement('option')
option.setAttribute('value', 'value属性に入れる値')
option.innerHTML = '要素に入れる文字列'
select.appendChild(option)
document.getElementById("#target").dispatchEvent(new Event('change'))
// 旧iOS対応
const el = document.getElementById('#target')
const evt = document.createEvent('HTMLEvents')
evt.initEvent('change', true, true)
el.dispatchEvent(evt)