0
1

More than 1 year has passed since last update.

JavaScript基礎知識の拾い集め

Last updated at Posted at 2022-08-25

初めに

何か月もかけてやっと一通り基礎を勉強してきました。残りの断片的な知識は一つの文章にまとめたいと思います。

Precedence & Associativity

優先度と結合性、Operator precedence
(日本語版と英語版の順位は違う。ここでは英語版に準ずる)
優先度:優先順位の大きいほうから実行する。
結合性:優先順位が同じである場合は実行の方向性を決める。

let a = 5 * 2 + 6 / 3 - 2
console.log(a) // 10
// Addition (+): 11, left-to-right
// Subtraction (-): 11, left-to-right
// Multiplication (*): 12, left-to-right
// Division (/): 12, left-to-right

人間である私たちにとって、乗算または除算してから加算や減算をするという物事の順序があるように、プログラムの世界もプロセスの優先順位が決められている。

下の例を見ていきたいと思います。

let b = 1
let c = 2
let d = 3
b = c = d
console.log(b) // 3
console.log(c) // 3
console.log(d) // 3
// Assignment (=): 2, right-to-left
// d => c => b

アサインは右手(Right-hand side)の値を左手の変数に入れるということで、b = c = dもそのルールに沿ってd => c => bでリアサインしていきます。結果としては変数dの値3になった。

1 + '2'のような数値と文字列の組み合わせが要注意です。

let e = 1 + '2'
console.log(e) // 12 // string
e = 1 + '2' + 3
console.log(e) // 123 // string
e = '2' + 3
console.log(e) // 23 // string

+で型変換が起こってしまうが、結合性の方向とは関係ありません。)
正しく計算してくれることはまずありません。おまけに型変換(coercion)で文字列になってしまう。実際に型変換の影響がこれだけではなく、時に深刻な問題を起こしてしまうので気を付けましょう。

Default value by logical OR (||)

論理和、Logical OR (||)
基本構文:

value1 || value2

結合性がleft-to-right、value1trueに変換できるtruthy valueの場合は先に返す。これはShort-circuit(短絡評価)と言います。

function sayHi(name) {
  name = name || 'Anonymous'
  console.log(`Hi, ${name}`)
}
sayHi('Taro') // Hi, Taro
sayHi() // Hi, Anonymous

この特性を利用してデフォルト値'Anonymous'を設置し、nameがfalsy valueだったら自動的に代入します。
(下のようにデフォルト引数でも同じことできる。)

function sayHi(name = 'Anonymous') {
  console.log(`Hi, ${name}`)
}
sayHi() // Hi, Anonymous

||は必ず値を返すので、value1がfalseに変換できるfalsy valueの場合はvalue2が何でも返します。

let f = false || []
console.log(f) // [] // falsy value

falseはもちろん、[]もfalseに変換できる。このとき||value2がfalsy valueでも返します。

ほかの演算子と一緒に使うとどうなるでしょうか。

f = true || false && false
console.log(f) // true
// Logical OR (||): 3, left-to-right
// Logical AND (&&): 4, left-to-right
// false && false => false, true || false => true

優先順で処理していきますね。

部分の順位を変えたいなら数学計算のように()を付けましょう。

f = (true || false) && false
console.log(f) // false
// Grouping (()): 18, n/a
// (true || false) => (true), true && false => false => return value2
f = true && false // false => return value2
console.log(f) // false

&&は計算結果がtrueの場合はvalue1を返す。falseの場合はvalue2を。

let g = 1 && 0 // truthy && falsy => false => return value2
console.log(g) // 0
g = 0 && [] // falsy && falsy => true => return value1
console.log(g) // 0

どちらもtruthy valueの場合は集合(set)の概念で判断する。

g = 1 && 2 // 1 && 2 => Disjoint sets(false) => return value2
console.log(g) // 2
g = [123] && 123 // Disjoint sets(false) => return value2
console.log(g) // 123 
g = 1 && '1' // Disjoint sets(false) => return value2
console.log(g) // '1'

g = (1 > 0) && (-1 < 0) // true && true => return value1
console.log(g) // true

Logical AND (&&)
Falsy
Truthy

Function: First class object

Discover the power of first class functions

  • 関数はオブジェクトの一種。
  • 関数を値として、変数、オブジェクトや配列に格納できる。
// first class
let firstClass = function () {
  console.log('firstClass')
}
firstClass() // firstClass

let functions = {
  sayHey: function (name) {
    console.log(`Hey, ${name}`)
  },
  sayBey: function (name) {
    console.log(`Bey , ${name}`)
  }
}
functions.sayHey('Taro') // Hey, Taro
functions.sayBey('Taro') // Bey, Taro
let item = 'sayHey'
functions[item]('Jiro') // Hey, Jiro

let arr = [functions, function () { console.log('fn in arr') }]
arr[0][item]('Saburo') // Hey, Saburo
arr[1]() // fn in arr

(値の保管場所によって呼び出す形式が少し違いますが。)
関数は()で実行するので()がないと関数自体が値として扱う。
()つけると実行することによってreturnした値を扱う、という意味になります。(returnの値を指定しない場合はundefinedになる。)

  • 関数はほかの関数の引数として入れることができる。
  • 関数の返り値として、ほかの関数を返すことができる。
function add(a, b) {
  return a + b
}
function doSomething(fn, num1, num2) {
  return fn(num1, num2) // add(1, 2)
}
console.log(doSomething(add, 1, 2)) // 3
  • 関数はオブジェクトのようにpropertyを有する。

オブジェクトのプロパティを見つけるためにhasOwnProperty()Object.getOwnPropertyNames()を使いましょう。

let obj = {
  myId: 1,
  add: add,
  objOwnMethod: function () {
    console.log('objOwnMethod')
  }
}
console.log(obj.hasOwnProperty('myId')) // true
console.log(obj.hasOwnProperty('add')) // true
console.log(obj.hasOwnProperty('objOwnMethod')) // true
console.log(Object.getOwnPropertyNames(obj)) // [ 'myId', 'add', 'objOwnMethod' ]

しかし関数のオブジェクトというのは、オブジェクトといくつの違いがあります。

function demoForProperties(age) {
  this.age = age
  console.log(this)
}
console.log(demoForProperties.hasOwnProperty('age')) // false
console.log(Object.getOwnPropertyNames(demoForProperties))
// [ 'length', 'name', 'arguments', 'caller', 'prototype' ]
demoForProperties() // Object [global]

関数自体は何が入っても(入らなくても)必ず[ 'length', 'name', 'arguments', 'caller', 'prototype' ]などのプロパティを有している。

例のようにthisでプロパティageを提示しageが生成しましたが、ここのthisはグローバルなので、ageはグローバルに所属するプロパティです。

let demo = new demoForProperties(18)
console.log(demo.hasOwnProperty('age')) // true
console.log(Object.getOwnPropertyNames(demo)) // [ 'age' ]

console.log(demo) // demoForProperties { age: 18 }

関数をコンストラクタとしてインスタンスを創るとthisはインスタンスに参照し、インスタンスがageプロパティが生成して所有する。(インスタンスはオブジェクト。)

補足:Callback function

function print(fn) {
  fn()
}
print(function () {
  console.log('Hello!')
})
// Hello!

関数の中にほかの関数を入れて呼び出すのがコールバックって言います。

about this

JavaScriptではthisは動的で環境やメソッドによって違う参照先になります。
下のように、オブジェクトobjに、三つのところでthisをコンソールしてみたら、

const obj = {
  name: 'It is obj',
  print: function () {
    this.name = 'It is print fn'
    console.log(this)

    function setNewName(newName) {
      this.name = newName
      console.log(this) // Object [global]
    }

    setNewName('It is newName fn')
    console.log(this)
  }
}

obj.print()
// { name: 'It is print fn', print: [Function: print] }
// Object [global] // create new property 'name' in global
// { name: 'It is print fn', print: [Function: print] }

thisがImplicit bindingの状態では、自分のいる環境に所属しています。
obj.print()を呼ぶとき、一番目のthisはImplicit bindingでobjに所属している。

二番目はsetNewName()関数でのthisをコンソールした結果です。setNewName()thisprint()からスコープを継承してないため、globalに所属している。(実行環境によって名前が違う。Node.jsではglobal object、ブラウザではwindow objectです。)
なのでsetNewName()のプロパティnameobjnameを上書きしたのではなくglobalに新しいプロパティnameを創り、値をアサインしました。

三番目のthisは一番目と変わらずprint()に所属するのでobjをコンソールしました。

もしsetNewNane()はアロー関数に変えたら、

...
    const setNewName = (newName) => {
      this.name = newName
      console.log(this)
    }
...
obj.print()
// { name: 'It is print fn', print: [Function: print] }
// { name: 'It is newName fn', print: [Function: print] }
// { name: 'It is newName fn', print: [Function: print] }

アロー関数は自分のいる環境のスコープを継承するので、thisの参照先がobjに統一され、プロパティnameの上書きがうまく行きました。

もう一つの方法があります。

const obj = {
  name: 'It is obj',
  print: function () {
    const saveThis = this // *

    this.name = 'It is print fn'
    console.log(this)

    function setNewName(newName) {
      saveThis.name = newName  // *
      console.log(this) // Object [global]
    }

    setNewName('It is newName fn')
    console.log(this)
  }
}

obj.print()
// { name: 'It is print fn', print: [Function: print] }
// Object [global]
// { name: 'It is newName fn', print: [Function: print] }

thisの値をほかの変数に格納したら、別の環境でもプロパティの操作をすることができます。
ほかにもcall()apply()でExplicit binding、bind()でHard bindingなどのthisの参照を変えるバインドメソッドが利用できます。

Object.prototype.toString()

Object.prototype.toString()
型チェックするときによくtypeof使いますが、Object.prototype.toString()ならより詳細な情報が返してくれます。

let a = []
console.log(typeof a) // object
console.log(Object.prototype.toString.call(a)) // [object Array]

a = new Promise(resolve => { })
console.log(typeof a) //object
console.log(Object.prototype.toString.call(a)) // [object Promise]

a = undefined
console.log(typeof a) //undefined
console.log(Object.prototype.toString.call(a)) // [object Undefined]

a = null
console.log(typeof a) //object
console.log(Object.prototype.toString.call(a)) // [object Null]

functional programming

これは自分がプログラミングの勉強をしてから触れた、一番核心になる概念と言っていいくらいです。

以下は配列の走査のデモコードです。

let arr1 = [1, 2, 3]

function mapItem(arr, fn) {
  let newArr = []
  for (let i = 0; i < arr.length; i++) {
    newArr.push(fn(arr[i]))
  }
  return newArr
}

let arr2 = mapItem(arr1, function (item) {
  return item * 2
})
console.log(arr2) // [ 2, 4, 6 ]

let arr3 = mapItem(arr2, (item) => item > 2)
console.log(arr3) // [ false, true, true ]

mapItem()の引数が配列とコールバックで、コールバックの走査が終わったら新しい配列を返す。

コールバックの引数が一つならまたいいけど、複数以上になったらmapItem()を書き直すしかないかな?

let checkLimiter = function (limiter, item) { // two parameters
  return item < limiter
}

// let arr4 = mapItem(arr1, checkLimiter)
// console.log(arr4) // [ false, false, false ]
// the second parameter 'item' is undefined

バインドメソッドを使ったら実行環境を変えてさきに引数を埋め込みたり、

// by bind()
let arr4 = mapItem(arr1, checkLimiter.bind(this, 3))
console.log(arr4) // [ true, true, false ]

let checkLimiterSimplified = (limiter) => {

  return function (limiter, item) {
    return item < limiter
  }.bind(this, limiter)

}

let arr5 = mapItem(arr1, checkLimiterSimplified(3))
console.log(arr5) // [ true, true, false ]

あるいはバインドした関数をラッパーして値として返したりする。もしラッパー関数の引数が複数である場合は、外側で配列にして入れたら...で展開したりするのもできます。

この例では、ほかにクロージャを用いてもいいです。

// by closure
let checkLimiterSimplified = (limiter) => {
  return function (item) {
    return item < limiter
  }
}

let arr6 = mapItem(arr1, checkLimiterSimplified(3))
console.log(arr6) // [ true, true, false ]

(引数が多くなったらbind()を使った方がいいかもしれません。)

which is faster: for/for...of/forEach

Which is faster: for, for…of, or forEach loops in JavaScript
最近パフォーマンスのこと調べててこの文章のテーマに興味を惹かれて、Chrome DevToolsforループ系列のほかに、map()filter()reduce()もテストしてみました。

const arr = [...Array(5000).keys()];

// for
console.time('for');
for (let i = 0; i < arr.length; i++) {
}
console.timeEnd('for');

// for...of
console.time('for...of');
for (let value of arr) {
}
console.timeEnd('for...of');

// forEach loop
console.time('forEach');
arr.forEach((value) => {
});
console.timeEnd('forEach');

// for: 0.212ms
// for...of: 0.178ms
// forEach: 0.078ms

// forEach > for...of > for

結論から言うと、参考文章とほぼ同じ結果なんですが。
驚いたのはES6の高階関数たちの結果↓です。

// map
console.time('map');
arr.map((x) => x);
console.timeEnd('map');

// filter
console.time('filter');
arr.filter((x) => x);
console.timeEnd('filter');

// reduce
console.time('reduce');
arr.reduce((x, y) => x + y);
console.timeEnd('reduce');

// map: 0.154ms
// filter: 0.141ms
// reduce: 0.101ms

どれも早かった...!
ちなみにそれぞれforforEachと比べたら、

const arr = [...Array(5000).keys()];
// map
console.time('map');
arr.map((x) => String(x));
console.timeEnd('map');

console.time('map vs. for');
let newArr1 = [];
for (let i = 0; i < arr.length; i++) {
  newArr1.push(String(arr[i]));
}
console.timeEnd('map vs. for');

console.time('map vs. forEach');
arr.forEach((x) => String(x));
console.timeEnd('map vs. forEach');

// map: 0.175048828125 ms
// map vs. for: 0.406982421875 ms
// map vs. forEach: 0.137939453125 ms
// filter
console.time('filter');
arr.filter((x) => (x >= 0));
console.timeEnd('filter');

console.time('filter vs. for');
let newArr2 = [];
for (let i = 0; i < arr.length; i++) {
  if (arr[i] >= 0) {
    newArr2.push(arr[i]);
  }
}
console.timeEnd('filter vs. for');

console.time('filter vs. forEach');
arr.forEach((x) => (filter >= 0));
console.timeEnd('map vs. forEach');

// filter: 0.1337890625 ms
// filter vs. for: 0.42578125 ms
// filter vs. forEach: 0.08984375 ms
// reduce
let total = 0;
console.time('reduce');
arr.reduce((x, y) => (x + y));
console.timeEnd('reduce');

console.time('reduce vs. for');
for (let i = 0; i < arr.length; i++) {
  total += arr[i];
}
console.timeEnd('reduce vs. for');

console.time('reduce vs. forEach');
arr.forEach((x) => (total += x));
console.timeEnd('reduce vs. forEach');

// reduce: 0.0908203125 ms
// reduce vs. for: 0.21484375 ms
// reduce vs. forEach: 0.158935546875 ms

forEachのほうが早かったんですが。けれど高階関数のパフォーマンスも優れていると思います。

for...ofは副作用や予想外のエラーになりやすいのでテストから外しました。)

for...in vs. for...of

for...in
for...of
for...in:列挙可能なプロパティ。(オブジェクト、配列)
for...of:反復処理可能なオブジェクト。(配列、配列風オブジェクト(e.g.Nodelist)、文字列、ジェネレータ...)

// for...in
let obj = {
  a: 1,
  b: 2,
  c: 3,
};
let arr = [4, 5, 6];

for (let key in obj) {
  console.log(key);
};
// a
// b
// c
for (let index in arr) {
  console.log(index);
};
// 0
// 1
// 2
// for...of
for (let value of arr) {
  console.log(value);
};
// 4
// 5
// 6

for (let value of obj) {
  console.log(value);
};
// TypeError: obj is not iterable

// iterable object
let obj1 = {
  from: 1,
  to: 5,
  *[Symbol.iterator]() {
    for (let i = this.from; i <= this.to; i++) {
      yield i;
    };
  },
};

for (let value of obj1) {
  console.log(value);
};
// 1
// 2
// 3 ...

for...inはプロパティを呼び出す、for...ofは反復できるオブジェクトの値を呼び出す。
使い分けもとても便利そうに見えますが、調べればほとんどこの二つのメソッドをおすすめしないのはなぜでしょうか。

let arr = [];
arr[2] = 2;
for (let prop in arr) {
  console.log(prop)
};
// 2
for (let value of arr) {
  console.log(value)
};
// undefined
// undefined
// 2

for...inはempty slotだからプロパティ(インデックス)が見つけられない、for...ofはempty slotなのにundefinedとして出力。
empty solt

Array.prototype.foo = 1;
let arr = [1, 2];
for (let prop in arr) {
  console.log(prop)
};
// 0
// 1
// foo

let obj = {
  a: 1,
  b: 2,
  c: 3,
};
Object.prototype.newProp = 'I am new property.';
for (let prop in arr) {
  console.log(prop)
};
// 0
// 1
// foo
// newProp

function getName(name) {
  this.name = name;
}
let test = new getName('Mick');
for (let prop in test) {
  console.log(prop)
}
// name
// newProp

(組み込み関数の.prototypeを弄ることおすすめしません。)
上のようにプロトタイプチェーンに新しいプロパティ作ると、for...inが所有していないプロパティも出力してしまいました。

オブジェクトならもちろんObject.prototype.hasOwnProperty()メソッドで所有プロパティのチェックができますが、配列に対してはどうしようもないです。

for (let prop in test) {
  if (Object.prototype.hasOwnProperty.call(test, prop)) {
    console.log(prop);
  };
};
// name

new.target

関数やクラスがnewで呼び出されたかを検出するメソッドです。

function Person(name) {
  if (!new.target) {
    return new Person(name)
  }

  this.name = name;
}
let mick = Person('Mick');
console.log(mick); // Person { name: 'Mick' }

Singleton

関数やクラスのインスタンスが一つしかないという意味です。
この用語に対しては認識はまだ浅いが、巨大なるインスタンスが一つしか存在しない、またはインスタンスのすべてが同じオブジェクトに参照するという。

const Singleton = function () {
  // when we create instance return the value
  if (typeof Singleton.cache === 'object') return Singleton.cache;

  // set property 'cache' in first invocation
  this.p = 'public';
  Singleton.cache = this;
};

let obj1 = new Singleton();
let obj2 = new Singleton();
console.log(obj1 === obj2); // true

Singleton.cache = 'Reference changed';
console.log(Singleton.cache);

これでインスタンスが同じオブジェクトに参照しますが。cacheは外部のアクセスによりいつでも上書きされることができるので、cacheを隠す必要があります。

const Singleton = (function () {
  let cache;
  return function () {
    if (typeof cache === 'object') return cache;

    this.p = 'public';
    cache = this;
  };
}())
let obj1 = new Singleton();
let obj2 = new Singleton();
console.log(obj1 === obj2); // true
console.log(Singleton.cache); // undefined

クロージャの特性で関数の内部関数にアクセスできない、IIFEの即時実行で静的スコープで包んで、グローバルスコープから同じ変数へのアクセスを防ぐ。

class Singleton {
  constructor() {
    // ...
  }

  method() {
    // ...
  }
}

let objSingleton = new Singleton();
Object.freeze(objSingleton);

export default objSingleton;

ちょっと違うやり方でモジュールからクラスを直接利用するのではなく、Object.freeze()で凍結されたインスタンスを使用します。

optional chaining ?.

ネストされたオブジェクトにアクセスするとき、存在しないプロパティに遭遇してしまってもエラーをなげずにコードを進める安全な方法です。

let user = {};
console.log(user.address.street);
// TypeError: Cannot read properties of undefined (reading 'street')

存在しないプロパティにアクセスしてしまったら普通はエラーが出てそのあとの実行が中断されてしまいます。

let user = {};
console.log(user.address ? user.address.street : undefined);
// undefined
console.log(user.address && user.address.street && user.address.name);
// undefined

条件演算子?か論理結合&&で解決できるが、ネストの深さに連れてコードも長くなってしまいます。

console.log(user?.address?.street);
// undefined

オプショナルチェイニング?.なら簡潔に済みます。
?.の前の変数は定義されてなければなりません。)

let userAdmin = {
  admin() {
    console.log('Admin');
  }
};
let userGuest = {};

userAdmin.admin?.(); // Admin
userGuest.admin?.(); // nothing happens

関数の存在をチェックして実行するのもできます。
(以下はほかの使い方。)

let key = 'firstName';

let user1 = {
  firstName: 'Mick',
};
let user2 = null;

console.log(user1?.[key]); // Mick
console.log(user2?.[key]); // undefined

// if user1 exists, delete property 'firstName'
delete user1?.firstName;
console.log(user1) // {}

まずはここまで、書きたいものがあればまた更新します!

0
1
0

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
0
1