初めに
何か月もかけてやっと一通り基礎を勉強してきました。残りの断片的な知識は一つの文章にまとめたいと思います。
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、value1
がtrue
に変換できる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
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()
のthis
はprint()
からスコープを継承してないため、globalに所属している。(実行環境によって名前が違う。Node.jsではglobal object、ブラウザではwindow objectです。)
なのでsetNewName()
のプロパティname
はobj
のname
を上書きしたのではなく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 DevToolsでfor
ループ系列のほかに、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
どれも早かった...!
ちなみにそれぞれfor
やforEach
と比べたら、
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) // {}
まずはここまで、書きたいものがあればまた更新します!