初めに
今回はJavaScriptの関数への振り返りです。基礎概念の勉強、やっと一段落だと思います。とてもいいチュートリアルに出会えて心から感謝しています。いつも読んでくださる皆さんもありがとうございます。
これからほかの勉強(ブラウザやWEBの基礎、React.js等々)まだ、もっと、たくさん勉強してまとめていきたいと思います!
今回振り返りの参考文章はこちらです。
Recursion
Recursion vs. for Loop
// recursion and stack
console.time('for Loop');
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
console.log(pow(2, 1000))
console.timeEnd('for Loop');
// for Loop: 4.052ms
console.time('Recursion');
function pow(x, n) {
if (n === 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
console.log(pow(2, 1000))
console.timeEnd('Recursion');
// Recursion: 3.997ms
この書き方では計算量どちらも$O(n)$なので、あまり差がありませんが。
気になるのはメモリのほうです。for
ループ内部コードは毎回実行後GC
により回収されるが、再帰関数は自分自身再利用する時点ではクロージャにもなり、最後の呼び出しから返り値を受け取るまで回収されずメモリリークになりがちな原因の一つでもあります。
クロージャ、再帰関数どちらも重要で避けられない概念です。全要素の簡単な走査ならfor
ループ、一部の検定から再度呼び出しなら再帰関数。
// recursive traversals
let company = {
sales: [ // case1: [{}, {}]
{ name: 'John', salary: 1000 },
{ name: 'Alice', salary: 1600 }
],
development: { // case2: {[{},{}],[]}
sites: [
{ name: 'Peter', salary: 2000 },
{ name: 'Alex', salary: 1800 },
],
internals: [
{ name: 'Jack', salary: 1300 },
]
}
};
function sumSalaries(department) {
if (Array.isArray(department)) { // case1
return department.reduce((previous, current) => {
return previous + current.salary;
}, 0);
} else { // case2
let sum = 0;
for (let sub of Object.values(department)) {
sum += sumSalaries(sub);
}
return sum;
}
}
console.log(sumSalaries(company));
Task
// sum all numbers till the given one
// formula // n * ( n + 1 ) / 2
function sumTo(num) {
return num * (num + 1) / 2;
}
console.log(sumTo(100)); // 5050
// calculate factorial
// recursion
function factorial(num) {
return (num !== 1) ? num * factorial(num - 1) : 1
}
console.log(factorial(5)); // 120
function factorial(num) {
// n = 0 => false
return num ? num * factorial(num - 1) : 1;
}
console.log(factorial(5)); // 120
// Output a single-linked list
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
function printList(list) {
console.log(list.value);
if (list.next) {
printList(list.next);
}
}
printList(list);
function sumList(list) {
let sum = 0;
sum += list.value;
if (list.next) {
sum += sumList(list.next);
}
return sum;
}
console.log(sumList(list)); // 10
// Output a single-linked list in the reverse order
function printReverseList(list) {
if (list.next) {
printReverseList(list.next);
}
console.log(list.value);
}
printReverseList(list);
// 4
// 3
// 2
// 1
//
function printReverseList2(list) {
let arr = [];
let temp = list;
while (temp) {
arr.push(temp.value);
temp = temp.next;
}
for (let i = arr.length - 1; i >= 0; i--) {
console.log(arr[i]);
}
}
printReverseList2(list);
// 4
// 3
// 2
// 1
Rest parameters and spread syntax
分割代入構文からの宣告された変数としての残余引数
残余引数/イテレータできるオブジェクトを展開するスプレット構文
let [...args] = [1, 2, 3]; // rest parameters
console.log(args); // [ 1, 2, 3 ]
console.log(...args); // 1 2 3 // spread syntax
Rest parameters vs. arguments object
残余引数とarguments
オブジェクトの違い。
function sumAll(...args) { // rest parameters
console.log(args); // [ 1, 2, 3 ]
console.log(...args); // 1 2 3 // spread syntax
// arguments object
console.log(arguments); // [Arguments] { '0': 1, '1': 2, '2': 3 }
let sum = 0;
// args is array, it is iterable
for (let arg of args) sum += arg;
return sum;
}
console.log(sumAll(1, 2, 3));
配列風オブジェクトであるarguments
オブジェクト要素へのアクセスは配列と同じです。
function showName(firstName, lastName, ...titles) {
console.log(firstName + ' ' + lastName); // Julius Caesar
console.log(titles[0], titles[1]); // Consul Imperator
console.log(titles.length); // 2
}
showName('Julius', 'Caesar', 'Consul', 'Imperator');
// THe "arguments" variable
function showName() {
console.log(arguments.length); // 2
console.log(arguments[0], arguments[1]); // Julius Caesar
}
showName('Julius', 'Caesar');
Arrow function don't have "arguments"
アロー関数にはarguments
持っていません。
// arrow function don't have "arguments"
let arrowFn = () => {
console.log(arguments);
// ReferenceError: arguments is not defined
}
arrowFn(1, 2, 3);
Spread syntax
イテレータできるオブジェクトもしくは[Symbol.iterator]
プロパティを持つオブジェクトならスプレット構文が使えます。
// Spread syntax
console.log(Math.max(1, 2, 3)); // 3
let arr = [1, 2, 3];
console.log(Math.max(...arr)) // 3
let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];
console.log(Math.max(...arr1, ...arr2)); // 8
console.log(Math.max(...arr1, ...arr2, 25)); // 25
console.log([0, ...arr1, 2, ...arr2]);
// [
// 0, 1, -2, 3, 4,
// 2, 8, 3, -8, 1
// ]
Shallow Copy
スプレット構文からの要素がオブジェクトタイプであれば浅いコピーになります。浅いコピーでオブジェクトタイプのメモリロケーションをコピーするだけなので、厳密等価ではいつもtrue
です。
単純な要素内容の比較なら、JSON.Stringify()
で文字列にしてから比較するのもいいです。
// Copy an array
let arr = [[1], 2, 3];
let arrCopy = [...arr];
console.log(arr[0] === arrCopy[0]); // true
// note: Spread syntax is shallow copy, if value was array/object, they are equal
// use JSON.stringify() to convert to String
console.log(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true
arr.push(4);
console.log(arr); // [ [ 1 ], 2, 3, 4 ]
console.log(arrCopy); // [ [ 1 ], 2, 3 ]
// Copy an object
let obj = { a: 1, b: 2, c: ['3', '4'] };
let objCopy = { ...obj };
console.log(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
console.log(obj === objCopy); // false
console.log(obj.c === objCopy.c); // true // shallow copy
Filter through function
arr.filter(callbackFn()(element))
インラインコールバックのように書くのもいいと思いますが、コールバックならもっと簡潔に書けます。
// filter through function
let comparedArr = [1, 2, 3, 4, 5, 6, 7];
// filter inBetween
function inBetween(a, b) {
return function (x) {
return x >= a && x <= b;
}
}
// Callback function
console.log(comparedArr.filter(inBetween(3, 6))); // [ 3, 4, 5, 6 ]
// Inline callback function
console.log(comparedArr.filter((x) => x >= 3 && x <= 6)); // [ 3, 4, 5, 6 ]
// note: inBetween(3, 6)(x)
// filter inArray
function inArray(arr) {
return function (element) {
return arr.includes(element);
};
}
console.log(comparedArr.filter(inArray([1, 2, 10]))); // [ 1, 2 ]
console.log(comparedArr.filter((x) => [1, 2, 10].includes(x))); // [ 1, 2 ]
// note: inArray([1, 2, 10])(x)
console.log(inArray([1, 2, 10])(1)); // true
console.log(inArray([1, 2, 10])(2)); // true
console.log(inArray([1, 2, 10])(7)); // false
Sort by field
プロパティ別で要素をソートしたいときもありますね。
// sort by field
let users = [
{ name: "John", age: 20, surname: "Snow" },
{ name: "Pete", age: 18, surname: "Pen" },
{ name: "Ann", age: 19, surname: "Hathaway" },
{ name: "Emma", age: 19, surname: "Watson" },
{ name: "Harry", age: 19, surname: "Potter" },
];
function byField(fieldName) {
return (a, b) =>
a[fieldName] > b[fieldName] ? 1 : b[fieldName] > a[fieldName] ? -1 : 0;
}
console.log(users.sort(byField("name")));
console.log(users.sort(byField("age")));
console.log(users.sort(byField("surname")));
ひらがなや漢字のソートならlocaleCompare()
関数が必要です。
(JSON.Stringify()
にしてからソートするのもいいと思います。(別の工夫が必要です。))
Function object & NFE(Named Function Expression)
The name
property
関数や関数式(アロー関数、匿名関数も含め)にはname
プロパティがあります。
function sayHi() {
console.log('Hi');
}
console.log(sayHi.name); // sayHi
// function expression as default value
function foo(anotherFn = function () { }) {
console.log(anotherFn.name);
}
foo(); // anotherFn
// in object
let user = {
sayHi() { },
sayBye() { }
};
console.log(user.sayHi.name); // sayHi
console.log(user.sayBye.name); // sayBye
// in array
let arr = [function test() { }, function () { }];
console.log(arr[0].name); // test
console.log(arr[1].name); // <empty string>
// function expression: arrow function
let arrowFn = () => { };
console.log(arrowFn.name); // arrowFn
// function expression: anonymous function
let anonymousFn = function () { };
console.log(anonymousFn.name); // anonymousFn
The "length" property
length
プロパティはパラメータの個数による決めます。しかし残余引数は含まれていません。(独立した配列になるから。)
function f1(a) { }
console.log(f1.length); // 1
function f2(a, b) { }
console.log(f2.length); // 2
function many(a, b, ...more) { }
console.log(many.length); // 2
この特性を用いて、for...of
とif
条件式を通して自動ハンドラーの実装ができます。
function ask(question, ...handlers) {
let isYes = confirm(question);
// handlers = [() => console.log('You said yes'), (result) => console.log(result)]
for (let handler of handlers) {
// zero-argument function will be called when the answer is positive
// function with arguments will always be called and returns an answer
if (handler.length === 0) {
// Ok => both handlers are called
// cancel => only the second one
if (isYes) handler();
} else {
handler(isYes);
}
}
}
ask(
'Question?',
() => console.log('You said yes'),
(result) => console.log(result)
);
(この参考文章の例が自分にとって少しわかりづらかったのですが、ブラウザで実行させてみたほうが理解が早いと思います。)
Custom properties
関数にカスタムプロパティを持たせて、外部からアクセスすることができます。(this
との違いは後でまとめます。)
function sayHi() {
console.log('Hi');
sayHi.counter++;
// console.log(this.counter === sayHi.counter);
// TypeError: Cannot read properties of undefined (reading 'counter')
}
sayHi.counter = 0;
console.log(sayHi); // [Function: sayHi] { counter: 0 }
sayHi(); // Hi
sayHi(); // Hi
console.log(`Called ${sayHi.counter} times`); // Called 2 times
let test = new sayHi();
console.log(test); // sayHi {}
console.log(test.counter); // undefined
(関数のカスタムプロパティはインスタンスには持たせられない、継承できないプロパティです。)
外部からアクセスできるというのは、勝手に変えられる、情報が暴かれている状態です。いったんカスタムプロパティを内部関数のプロパティにして、内部関数から返り値として返してもらえば外部アクセスを制限できます。
// function object
// closure storing
function makeCounter() {
function counter() {
// store as function counter's own property
return counter.count++;
}
counter.count = 0; // initial value
return counter;
}
// external function is unable to see internal function's property
console.log(makeCounter); // [Function: makeCounter]
let counter = makeCounter(); // returns function counter
console.log(counter()); // 0
console.log(counter()); // 1
counter.count = 10;
console.log(counter()); // 10
vs. this(context)
カスタムプロパティとthis
(コンテキスト)プロパティの比較です。
function thisCounter() {
this.count = 0;
// arrow function doesn't have its own scope("this"),
// it will inherit scope from outer scope
this.add = () => {
return ++this.count;
}
}
console.log(thisCounter); // [Function: thisCounter] // doesn't have function object
let counter2 = new thisCounter();
// with keyword "new", this.property will convert to instance's own property
console.log(counter2); // thisCounter { count: 0, add: [Function (anonymous)] }
console.log(counter2.add()); // 1
console.log(counter2.add()); // 2
カスタムプロパティとは違い、this
はコンテキストの参照を示すプロパティです。インスタンスオブジェクトが創られるたび、this
を通してインスタンスにプロパティを付与する。(this
は動的で、コンストラクタ関数もインスタンスも自身のthis
は常に自身に参照している。)
Task
// task
// Set and decrease for counter
function makeCounter() {
let count = 0;
function counter() {
return ++count;
}
counter.set = (value) => count = value;
counter.decrease = () => --count;
return counter;
}
let counter = makeCounter();
console.log(counter);
// [Function: counter] {
// set: [Function(anonymous)],
// decrease: [Function(anonymous)]
// }
console.log(counter()); // 1
console.log(counter.set(10)); // 10
console.log(counter.decrease()); // 9
// Sum with an arbitrary amount of brackets
function sum(a) {
let currentSum = a;
function innerSum(b) {
currentSum += b;
// for next calling // currying
return innerSum;
}
// custom method
innerSum.showValue = function () {
return currentSum;
}
// innerSum.showValue = () => currentSum;
return innerSum;
}
console.log(sum(1)(2).showValue()); // 3
console.log(sum(5)(-1)(2).showValue()); // 6
console.log(sum(6)(-1)(-2)(-3).showValue()); // 0
Scheduling: setTimeout and setInterval
setTimeout()
:グローバルのタイマーメソッドです。指定されたミリ秒を待ちコールバックを実行する。
clearTimeout()
:setTimeout()
から返した識別子を特定のsetTimeout()
を解除できます。
// Canceling with clearTimeout
let timerId = setTimeout(() => console.log('never happens'), 1000);
console.log(timerId); // Timeout {...}
clearTimeout(timerId);
console.log(timerId); // still exist
setInterval()
:指定されたミリ秒間隔を待ちコールバックを繰り返し実行する。
clearInterval()
:指定されたsetInterval()
の識別子によって特定のsetInterval()
を解除する。
// setInterval
let timerId = setInterval(() => console.log('tick'), 2000);
setTimeout(() => {
clearInterval(timerId);
console.log('stop');
}, 5000);
// tick
// tick
// stop
Task
// task
// Output every second
// Using setInterval
function printNumbers(from, to) {
let current = from;
let timerId = setInterval(function () {
console.log(current);
if (current === to) {
clearTimeout(timerId);
}
current++;
}, 1000);
}
printNumbers(1, 5);
// Using nested setTimeout
function printNumbers(from, to) {
let current = from;
setTimeout(function go() {
console.log(current);
if (current < to) {
// recursion
setTimeout(go, 1000);
}
current++;
}, 1000);
}
printNumbers(5, 10);
Decorators and forwarding, call/apply
call()
:コンテキストthis
の参照を強制的に変えるメソッドです。
オブジェクトからメソッド呼び出しでは下のように参照先を明記すれば簡単にできます。
let obj = {
returnOne() {
return 1;
},
myMethod() {
return this.returnOne();
}
}
console.log(obj.myMethod()); // 1
しかし別のスコープの中では、外部のメソッドを呼び出すと現在のスコープではこのような名前のプロパティ(メソッド)が存在しないというエラーができてきます。
function test(fn) {
let result = fn()
return result;
}
console.log(test(obj.myMethod));
// TypeError: Cannot read properties of undefined (reading 'objMethod')
call()
やapply()
を通せばコンテキストの参照を変え、正しい参照先からメソッドを見つけて利用できるようになる。
// call()
function test(fn) {
let result = fn.call(obj);
return result;
}
console.log(test(obj.myMethod)); // 1
// nested function
function test(fn) {
return function () {
let result = fn.call(obj);
return result;
}
}
console.log(test(obj.myMethod)()); // 1
しかし現在のスコープにすでに同じ変数が存在したら、最短ルートの法則により一番最初に見つけた変数(プロパティ)を利用し、望まない結果が生じるかもしれません。
function test(fn) {
let obj = null;
return function () {
let result = fn.call(obj);
return result;
}
}
// TypeError: Cannot read properties of null(reading 'returnOne')
Transparent caching
参考文章から取った例です。簡単に説明すると、worker
オブジェクトのように関数の結果に及ぼす変数なら、ほかの関数(やプロパティ)に任せば機能分離というコードをシンプルにすることができる。
let worker = {
someMethod() {
return 1;
},
slow(x) {
console.log(`Called with ${x}`);
return x * this.someMethod();
}
};
下のcachingDecorator()
はデコレータの応用例です。デコレータはあるオブジェクトをベースに、新しい処理を追加や変更するためのデザインパターンです。
function cachingDecorator(fn) {
let cache = new Map();
return function (x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = fn.call(worker, x);
cache.set(x, result);
return result;
};
}
console.log(cachingDecorator(worker.slow));
// [Function (anonymous)]
console.log(cachingDecorator(worker.slow)(2));
// Called with 2
// 2
// or
function cachingDecorator(fn) {
let cache = new Map();
return function (x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = fn.call(this, x);
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow);
// use function parameter fn to store original worker.slow
console.log(worker.slow(2));
// Called with 2
// 2
let result = fn.call(this, x);``call()
がthis
の指定で正しく動作できたのは、worker.slow = cachingDecorator(worker.slow);
で、元のworker.slow
メソッドをcachingDecorator(worker.slow)
上書きされたのです。そして元のworker.slow
メソッドは消されたわけではなくcachingDecorator(worker.slow)
引数としてfn
に保存されています。
これでスコープ内で同じ変数が宣告されても正しく参照したり、ベースのメソッドも暴かれることがなくなる。(上書きしないままオブジェクトworker.slow
を入れ替えることで結果が改ざんされたり、データ漏洩する可能性があります。外部への参照は内部への入口とも言えるので。)
下はほかの例です。
let anotherWorker = {
slow(min, max) {
console.log(`Called with ${min}, ${max}`);
return min + max;
}
};
function cachingHash(fn, hash) {
let cache = new Map();
return function () {
let key = hash(arguments);
if (cache.has(key)) {
return cache.get(key);
}
let result = fn.call(this, ...arguments); // spread
cache.set(key, result);
return result;
};
}
function hash(args) {
return `${args[0]}, ${args[1]}`;
}
// add new property in object "anotherWorker"
// note: if we will use anotherWorker.slow in other function, then it shouldn't be covered
// but if we want to protect from being called by other functions or accessed, we should overwrite itself
anotherWorker.slow = cachingHash(anotherWorker.slow, hash);
console.log(anotherWorker.slow(3, 5));
// Called with 3, 5
// 8
console.log(`Again ${anotherWorker.slow(3, 5)}`);
// Again 8
組み込みメソッドをcall()
するなら一定の前提条件があります。
// borrowing a method
function arrCallArgus() {
// console.log(arguments); // [Arguments] { '0': 1, '1': 2 }
// arguments is iterable
return Array.prototype.join.call(arguments, '');
}
console.log(arrCallArgus(1, 2)) // 12
まず別の例を見ていきたいと思います。
function arrCallStr() {
// console.log(arguments); // [Arguments] { '0': 1, '1': 2 }
// arguments is iterable
let str = '123';
return Array.prototype.join.call(str);
}
console.log(arrCallStr()); // 1,2,3
変数str
を配列の組み込みメソッドjoin()
を通して連結する。結果が1,2,3
というのはまず一旦str
を配列([1, 2, 3]
)にして区切り文字指定なしで返され1,2,3
になったわけです。
なのでarguments
か文字列のようにイテレータできるのが前提条件ですが、
// iterable object
function arrCallMap() {
let map = new Map([[0, '1'], [1, '2']]);
// console.log(map); // Map(2) { 0 => '1', 1 => '2' }
return Array.prototype.join.call(map);
}
console.log(arrCallMap()); // print nothing
function arrCallSet() {
let set = new Set([1, 2, 3]);
return Array.prototype.join.call(set);
}
console.log(arrCallSet()); // print nothing
// Number is not iterable
function arrCallNum() {
let num = 123;
return Array.prototype.join.call(num);
}
console.log(arrCallNum()); // print nothing
イテレータできるオブジェクトならcall()
では何も出てきません。
Task
// task
// Spy decorator
function work(a, b) {
console.log(a + b);
}
// Sinon.JS
function spy(fn) {
function wrapper(...args) {
wrapper.calls.push(args);
return fn.apply(this, args);
}
wrapper.calls = [];
return wrapper;
}
work = spy(work);
work(1, 2); // 3
work(4, 5); // 9
for (let args of work.calls) {
console.log(`call: ${args.join()}`)
}
// call: 1, 2
// call: 4, 5
console.log(work.calls); // [ [ 1, 2 ], [ 4, 5 ] ]
// Delaying decorator
function foo(x) {
console.log(x);
}
function delay(fn, ms) {
return function () {
setTimeout(() => fn.apply(this, arguments), ms);
};
}
let f1000 = delay(console.log, 1000);
f1000('test'); // test
Debounce decorator
// Debounce decorator
function debounce(fn, ms) {
let timeout = null;
return function () {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, arguments), ms);
}
}
let eventHandlers = {
wait(x) {
console.log(x);
}
}
debounce(eventHandlers.wait, 1000)('wait for one second'); // wait for one second
debounce(eventHandlers.wait, 3000)('wait for two seconds'); // wait for two seconds
Throttle decorator
// Throttle decorator
function throttle(fn, ms) {
let isThrottled = false
let savedArgs = null;
let savedThis = null;
function wrapper() {
if (isThrottled) {
savedArgs = arguments;
savedThis = this;
return;
}
isThrottled = true;
fn.apply(this, arguments);
setTimeout(function () {
isThrottled = false;
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = null;
savedThis = null;
}
}, ms);
}
return wrapper;
}
Function binding
Losing “this”
なぜthis
を失うでしょうか。これはsetTimeout()
などのWebAPI
だけに起こる問題ではありませんが。少し別の例を見ていきたいと思います。
let user = {
firstName: 'John',
sayHi() {
console.log(`Hello, ${this.firstName}!`);
}
}
setTimeout(user.sayHi, 1000); // Hello, undefined!
function test1(fn) {
console.log('do something for one second');
// fn is function parameter which belongs to function "test1"'s scope
return fn();
}
test1(user.sayHi);
// TypeError: Cannot read properties of undefined (reading 'firstName')
// note: when we put argument into function, actually it means an action on "reassign", so the scope of argument will be changed
this
を失ったのは、引数のスコープが変更されたのが原因です。引数が関数に入れるというのは関数のパラメータに代入するということで、引数は関数スコープに所属していてthis
が現在の関数に参照しています。
なので引数にせず、そのまま呼び出したら関数に所属しないプロパティ(user
)が外部への参照を探し、最後は外部のuser
にたどり着いてsayHi()
を正しく実行した。(もし関数内変数user
が同じく存在したらsayHi()
が見つからずにエラーになる。)
function test2() {
console.log('do something for one second');
// not as argument, its scope won't be changed
return function () {
user.sayHi();
}
}
test2()();
// do something for one second
// Hello, John!
// or
function test3() {
console.log('do something for one second');
// not as argument, its scope won't be changed
return () => user.sayHi();
}
test3()();
// do something for one second
// Hello, John!
setTimeout(() => user.sayHi(), 1000); // Hello, John!
これがSolution 1: a wrapper
が成立したわけです。
setTimeout()
にはもう一つの特性があります。setTimeout()
のようなタイマーが非同期を実現するために、Event Loop
ではタイマーが別のタスクMacrotask
に移し処理を行い、下のコードを継続していくのです。
let user = {
firstName: 'John',
sayHi() {
console.log(`Hello, ${this.firstName}!`);
}
}
setTimeout(() => user.sayHi(), 1000);
user = {
sayHi() {
console.log(`Reference has been changed`)
}
}
// Reference has been changed
なのでMacrotask
でタイマーが終わり、コールバックを実行する時に変数user
はすでに入れ替えられ、別のsayHi()
を実行してしまいました。
これを解決するにはbind()
で外側から強制的にコンテキストの参照先を結合させます。
// bind()
// only property
let user = {
firstName: 'John'
};
// method
function sayBye() {
console.log(`Bye, ${this.firstName}`);
}
let bye = sayBye.bind(user);
bye(); // Bye, John
function greet(phrase) {
console.log(`${phrase}, ${this.firstName}`);
}
let greeting = greet.bind(user);
greeting('Hello'); // Hello, John
// note: we don't have to change scope, we can use "bind()" to combine with "user" which "this" always refers to
(bind()
を利用することでプロパティとメソッド、機能を分離する(decoupling
)こともできます。)
let user = {
firstName: 'John',
sayHi() {
console.log(`Hello, ${this.firstName}`);
}
}
let sayHi = user.sayHi.bind(user);
setTimeout(sayHi, 1000);
user = {
sayHi() {
console.log(`Reference has been changed`);
}
}
// Hello, John
多くのメソッドが存在し、それらをバインドする必要があるなら、
let user = {
firstName: 'John',
methods: [
function sayHi() {
console.log(`Hello, ${this.firstName}`);
},
function sayBye() {
console.log(`bye, ${this.firstName}`);
}
]
}
// bindAll
for (let method of user.methods) {
if (typeof method === 'function') {
// bind() in for...of
method = method.bind(user);
setTimeout(method, 1000);
}
}
// Hello, John
// bye, John
console.log(user.methods[0]);
// [Function: sayHi]
// or
user.methods[0] = user.methods[0].bind(user);
setTimeout(user.methods[0], 1000);
// Hello, John
console.log(user.methods[0]);
// [Function: bound sayHi]
for...of
ではuser.methods
配列内のメソッドたちが、浅いコピーでmethod
に代入されfor...of
のスコープ内だけバインドを使用するので、外側のuser.methods
には影響を及ぼしません。
逆に、user.methods[0]
に再代入するならバインド済みのメソッドで上書きします。
Partial functions
部分関数は、カリー化と似ていますが少し違う概念です。
カリー化は一部の固定引数を関数内スコープで保存し、別の内部関数に利用させるという、結合度が高い方法です。部分関数は結合度を下げるために、外側からbind()
で固定引数を与えます。
// Partial functions
function multiple(a, b) {
return a * b;
}
let double = multiple.bind(null, 2);
console.log(double(3)); // 6
console.log(double(4)); // 8
function getBindMultiple(base, multiplier) {
return multiple.bind(null, base)(multiplier);
}
console.log(getBindMultiple(2, 3)); // 6
console.log(getBindMultiple(3, 4)); // 12
利用したいプロパティとメソッドが同じオブジェクトに存在するという前提で、bind()
を使ったら前の例のような、いちいち固定したコンテキスト(bindAll
)か、コンテキストをnull
することで固定引数を与え、ほかのプロパティを取り入れる部分関数にしかならない。
あるメソッドをベースにして、各々のコンテキストから引数を自由に組み合わせる部分関数を創るためにはcall()
を使うほうがもっと適切です。
(call
は強制的に結合するではなく参照先や引数を提供するのです。)
// Going partial without context
let user = {
firstName: 'John',
say(time, phrase) {
console.log(`[${time}] ${this.firstName}:${phrase}`);
}
};
function partial(fn, ...argsBound) {
return function (...args) {
return fn.call(this, ...argsBound, ...args);
}
}
// fn, argsBound(time)
user.sayNow = partial(user.say, `${new Date().getHours()}:${new Date().getMinutes()}`)
// args(phrase)
user.sayNow('Hello'); // [11:15] John: Hello
call()
、apply()
、bind()
、使い方としてはとても似ているかもしれないが本質が違うと思います。
call()
とapply()
は関数の正しい参照先を見つけるためのメソッド、
bind()
は指定するコンテキストを強制的に結合するメソッドです。
何か月前もcall()
とapply()
、そしてbind()
についての勉強メモを書いてみたけど、今回の振り返りではやはりその時完全に理解していなかったと分かった。this
のことを語りたかったのでthis
関連メソッドからコンテキストがどう結合するのを理解しようとしたけれど、今思い返せば無理やり解釈していたかもしれません。
でも今回の振り返りを通して今の自分が前よりコードや書き手の意図を読めるようになって、そして解釈もできるようになりました。何か月も勉強メモを書き続けてよかったと思います。とてもいい勉強になりました。