1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptの関数の高度な機能について

Last updated at Posted at 2022-10-17

初めに

今回は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...ofif条件式を通して自動ハンドラーの実装ができます。

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関連メソッドからコンテキストがどう結合するのを理解しようとしたけれど、今思い返せば無理やり解釈していたかもしれません。

でも今回の振り返りを通して今の自分が前よりコードや書き手の意図を読めるようになって、そして解釈もできるようになりました。何か月も勉強メモを書き続けてよかったと思います。とてもいい勉強になりました。

1
0
2

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?