JavaScript
es2015

JavaScriptの基礎知識【初心者向け】

概要

N番煎じネタ、あまり踏み込んだネタもやらず、クライアントAPIやNodeにしかないものによらない、初心者向けの一般的なネタのみにする予定

基礎編

var, let, const

スコープ 代入など
var 関数スコープ 代入・再宣言ともに
let ブロックスコープ 代入/再宣言不可
const ブロックスコープ 代入・再宣言ともに不可 

varは関数スコープなので同一関数内だと同じ名前は再代入になって書き換えられる、これは思わぬバグをうむのでvarの使用は推奨されない。
+=++なども代入になるためにconstに使用してしまうとエラー(TypeError)になる。
constは代入もできないため宣言時に値を確定させないとエラー(SyntaxError)になる。

error.js
const i=0;
i++; // TypeError
const undef; // SyntaxError
const undef2=undefined; // 実はOK

スコープの優先

同じ名前の変数はより狭いスコープのものが優先される。

scope.js
const c=0;
var v=0;
{
    const c=1; // スコープの優先で再宣言にならない
    var v=1; // ブロックスコープで縛られないので外のvを書き換える
    console.log(c, v); // 1, 1
}
console.log(c, v); // 0, 1 // 外にあるcなので0

スコープの優先度はバグを作りやすいので注意が必要。

プリミティブ型とObject型

JavaScriptではプリミティブ型以外はすべてObject型になります。プリミティブ型はimmutable(変更不可)1でObject型で参照型です(後述)

プリミティブ型

  • null
  • undefined
  • 真偽値
  • 文字列
  • 数値
  • シンボル (es2015で追加)

の6個です。

constなObject型

JavaScriptのconstは代入不可であるが変更可である。

参照型としてのObject型

Object型は参照型であり関数の引き渡しは参照渡しである。

Object.js
const obj={ a: 'hoge', b: 'hoge' };
obj.b='fuga'; // 変更可能
const swap=(a)=>{
   const tmp=a.a;
   a.a=a.b;
   a.b=tmp;
}
swap(obj); // objは参照渡しなので呼び出し元も変更される

console.log(obj); // { a: 'fuga', b: 'hoge' }

このようにJavaScriptでは変更されないObjectを作ることはできないので、プログラマーがObjectの状態に常に気にする必要がある。

Classは糖衣構文

糖衣構文だから使うな、ではなく糖衣構文あることを理解して使うべしということを強調しておく。
JavaScriptのClassはObjectでクラスを実現するための糖衣構文なのでプロパティは編集可能である。
また、Class自身はfunctionでインスタンス化された変数はobjectである。

Class.js
const MyClass=class{}
const myClass=new MyClass();

myClass.hoge='hoge';

console.log(typeof MyClass); // function
console.log(typeof myClass); // object

等価演算子

プリミティブ型

プリミティブ型は値が等しいかどうかの判定をする。==は型変換をするが、===は型変換をしない厳密な判定になる。
JavaScriptの型変換では思わぬ値に変換される場合があるので==は使わないことを強く推奨する。
(非存在を判定するためのif( val==null ) console.log(var, 'は存在しません');は数少ない例外の一つ)

参照型(オブジェクト)

オブジェクトは参照値が等しいか調べる2ためオブジェクトの中の値は一切関係ない。
中身を見るようなequal関数を作っても以下のように中身がオブジェクトとプリミティブなのかによって変わる。

equalObj.js
const obj1={ a: 'hoge', b: 'fuga' };
const obj2={ a: 'hoge', b: 'fuga' };
const obj3=obj1;

const equal=(obj1, obj2)=>{
    if( Object.keys(obj1).length!==Object.keys(obj2).length ) return false;
    for( const [ key, val] of Object.entries(obj1) ){
        if( obj1[key]!==obj2[key] ) return false;
    }
    return true;
}

console.log('obj1 == obj2 ', obj1==obj2); // false
console.log('obj1 == obj3 ', obj1==obj3); // true
console.log('equal(obj1, obj2) ', equal(obj1, obj2)); // true

const arr1=[ obj1, obj2 ];
const arr2=[ { ...obj1 }, { ...obj2 } ]; // スプレッド構文による代入
const arr3=[ obj1, obj2 ];

console.log('arr1 == arr2 ', arr1==arr2); // false
console.log('arr1 == arr3 ', arr1==arr3); // true
console.log('equal(arr1, arr2) ', equal(arr1, arr2)); // false 中身のオブジェクトの参照値は違う
console.log('equal(arr1, arr3) ', equal(arr1, arr3)); // true 

このようにJavaScriptでオブジェクトの比較をするには型によって再帰をするなどの処置が必要で非常に注意が必要である。

型判定

typeof演算子

typeof.js
console.log(typeof undefined); // undefined
console.log(typeof null);      // object
console.log(typeof 0);         // number
console.log(typeof 'hello');   // string
console.log(typeof true);      // boolean
console.log(typeof Symbol());  // symbol
const arrFunc=()=>{};
console.log(typeof arrFunc);   // function
console.log(typeof function(){}); // function
console.log(typeof {});           // object
console.log(typeof []);           // object

null"object"である
プリミティブに対しては関数のみ"function"がありアロー関数も"function"になる。

Array.isArray(value)

配列かどうか判定する、typeofでは配列かオブジェクトかは判定できないのでコチラを使う。

Object.prototype.toString.call()

組み込み型に対して内部プロパティ[[Class]]を使って判定する方法[object [[Class]] ]の形で帰ってくるので適当に切り取って結果を得る

toString_call.js
onst getType=(obj)=>{ return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); };

console.log(getType(undefined)); // undefined
console.log(getType(null));      // null

console.log(getType(function(){}));       // function
console.log(getType(function * (){}));    // generatorfunction
console.log(getType(async function(){})); // asyncfunction
console.log(getType(new Map()));          // map
console.log(getType(class{}));            // object

toLowerCaseを用いて小文字にしている。function*async functionもきちんと判定できる。
独自実装のクラスはObjectになる。

独自クラスの判定

Object.constructorもしくはinstanceofを使う

JavaScriptの数値関係の判定

NaN判定

グローバルなNaNは使わない。Number.isNaNを使う

InfinityはNaNに含まれない

有限な数かはNumber.isFiniteを使う。

Number.isInterger

JavaScriptには整数と実数の区別がないためこれらの判定はNumber.isIntergerを使う

Number.isSafeInterger

数値には分解能である数以上は連続の整数にならなくなるその限界の数の範囲内であるかを返す。
限界の整数値はNumber.MAX/MIN_SAFE_INTERGERで定義されている。

型判定のまとめ

JavaScriptで完全で安全な型判定をするのは難しい。その場で必要な分だけするようにしよう。そのためにもいくつかの典型的な方法は知っておいたほうがいいだろう。

スプレッド構文残余パラメーター

両方共、...です。使う場面によって呼び名が変わりますがそのほうが機能が分かりやすいので是非、別のものとして覚えてください3
スプレッド構文は配列リテラル4や関数呼び出しで使った場合で配列を展開して連続のパラメーターにします。Math.sum(...arr)Math.min(...arr)のように使います。
レストパラメーターは関数定義の仮引数に使うことで残りのパラメーターを配列のようにまとめます。

rest_parameters.js
const echo=(...args)=>{ console.log(args); }

echo(); // [] から配列
echo('a', 'b'); // [ 'a', 'b' ]
echo({ x: 1, y: 2}); // [ { x: 1, y: 2} ]

のように配列でラップしたようになります。

分割代入

const [ a, b, c ]=[...elem ];const {a, b}={ a: 1, b: 2}の様な形式で受けられる。undefinedの場合の既定値も設定できる。
オブジェクトの方はimport/export等でよく使われる。

文字列操作

String.trim()

前方と後方にある空白文字を削除する。忘れがちだけどブラウザからユーザーインプットを受ける時に意外と使える。

String.split(separator, limit=undefined)

separator(文字列/正規表現)で分割する、separatorは削除される

const arr=' 1, 2, 3, 4, '.split(',')
console.log(arr) //[ ' 1', ' 2', ' 3', ' 4', ' ' ]

数値に変換するにはArray.map(callback)を使う。

String.replace(regexp|substr, newSubstr|function)

第一引数は(文字列/正規表現)、第二引数で関数による操作ができる。
第一引数が文字列の場合、最初の文字だけが置換される

replaceAll的なもの5

正規表現を使ってグローバルフラッグgをつける

replaceAll_regExp.js
"a b c".replace(/ /g, '-')  // "a-b-c"

split-joinイディオム

replaceAll_split_join.js
"a b c".split(' ').join('-')  // "a-b-c"

joinはArrayについたメソッドである

String/Arrayに共通するメソッド

includes(elem, position=0)

String版Array版booleanを返す。Array版は第二引数に負の数を設定することが可能。

slice(start, end=this.length)

第一引数は負の場合は最後からの要素を取り出す、第二引数は省略可(最後まで)

Arrayについたメソッド

Arrayには破壊的メソッドと非破壊的メソッドがある。Objectが参照渡しのため破壊的メソッドを呼ぶと関数の外の配列にも影響する。

destruction.js
const add=(arr)=>{
    arr=arr.concat([ 'new elem' ]); // 新しい配列を作ってarrに代入
    console.log('add arr =', arr);
}

const add2=(arr)=>{
    arr.push('new elem'); // arrに要素を追加
    console.log('add2 arr =', arr);
}

const arr=[ 'hello', 'world' ];
add(arr); // add arr = [ 'hello', 'world', 'new elem' ]
console.log(arr); // [ 'hello', 'world' ] 外のarrは変わらない
add2(arr); // add2 arr = [ 'hello', 'world', 'new elem' ]
console.log(arr); // [ 'hello', 'world', 'new elem' ] 外のarrにも要素が追加される

Array.concat([ ...elem ])

引数は配列で2つの配列を繋げて新しく構築された配列を返す。

Array.splice(index, howMany, [ ...elem ])(破壊的)

spliceは継ぎ合わせるの意味でindexからhowManyを取り除き[ ...elem ]の配列を入れる。
howManyは省略可でその場合はindexから最後まで取り除かえる。
[ ...elem ]が省略された場合は何もたされない、sliceは新しく構築したものを返すがspliceは破壊的ヴァージョンとしても使える。

Array.push([ ...elem ])(破壊的)

[...elem]を末尾に足すメソッド、足すので配列の要素数は増える。戻り値は新しいArray.length。

Array.pop()(破壊的)

末尾の要素を取り出だすメソッド、取り出すので配列の要素は減る。空の場合はundefinedが帰る。

Array.shift()(破壊的)

先頭の要素を取り出だすメソッド、取り出すので配列の要素は減る。空の場合はundefinedが変帰る。

Array.unshift([ ...elem ])(破壊的)

[...elem]を配列の先頭に足すメソッド、足すので配列の要素数は増える。戻り値は新しいArray.length

Array.some(function)

function(elem){ return (true/false); }を満たす要素があるかないかを返すメソッド。
Array.includes(elem)は値を渡すがsomeは関数を渡せる。

Array.every(function)

function(elem){ return (true/false); }すべての要素が満たすかどうかを返すメソッド

Array.filter(function)

function(elem, index, self){ return (true/false) }でフィルターされた部分集合、元の配列より小さい配列、を返す。
functionの引数は省略可能(以下の関数プログラミング的メソッドも同様)

Array.map(function)

function(elem, index, self){ return any; }による写像、元の配列と同じlengthを持つ配列、を返す。

Array.reduce(function, initialValue)

function(accumulator, value, index, self){ return any; }(accumulatorは一つ前に返された値)、で作られるある値を返す。
functionの引数がfilter、mapと違うので注意。

あとがき

普段、記事はストックばっかり増えてまとまらないのでアドベントカレンダーに参加して期限を切ってだそうと思いました。
最初はもっとよく使う小技集にするつもりでしたが、書いてるうちに基礎的知識になりました。基礎的知識集という意味ではまだまだ足りないものも多いですが力尽きたのでこれで出します、ちょくちょく追加するかもしれません。
明日は、@wshito さんの記事です。


  1. Primitive (プリミティブ)(MDN) 

  2. プリミティブの参照はJavaScriptでは触れないためプリミティブ型の参照値を比べるはできない。 

  3. 「スプレッド演算子」という表現を使わない 

  4. ES2018からはオブジェクトリテラルでも使える。 

  5. 【JavaScript】検索文字列を全置換する方法【replaceAll】