(この記事は、2020年にQiitaに上げた記事の再投稿です。内容が古くなっている可能性がありますのでご了承ください。)

TL;DR

アロー関数をメソッドとコンストラクタに使えるようにした!

  • アロー関数の this は、ラッパーの function オブジェクト内で eval を使い再定義することで束縛
  • Proxy でコンストラクタの prototype をラップすることで、 prototype に定義するメソッドもアロー関数で書ける

はじめに

アロー関数って、見た目がシンプルで良いですよね!

いっそ全部の関数をアロー関数で書きたい!

でも、メソッドやコンストラクタなど、 this を使う関数には使えません…

アロー関数内の this は、呼び出し時ではなく定義時に決まってしまうからです。

const taro = {name: 'Taro'};
taro.sayHi = function() {
  console.log(`Hi! I am ${this.name}`);
};
taro.sayHiArrow = () => {
  console.log(`Hi! I am ${this.name}`);
};

taro.sayHi(); // Hi! I am Taro
taro.sayHiArrow(); // Hi! I am undefined

this を動的に変えられる3種の神器bind , call , apply もアロー関数には歯が立ちません。

const arrowPlus = (other) => {
  console.log(this.val + other);
};
const normalPlus = function(other) {
  console.log(this.val + other);
};

const myObj = {val: 10};
//              this   arg1
normalPlus.call(myObj, 10); // 20
arrowPlus.call(myObj, 10); // NaN (undefined + 10)

//              this
normalPlus.bind(myObj)(10); // 20
arrowPlus.bind(myObj)(10); // NaN

//               this   args
normalPlus.apply(myObj, [10]); // 20
arrowPlus.apply(myObj, [10]); // NaN

それもそのはず、アロー関数はややこしい this の仕様を気にせず使えるようにするために作られました。

アロー関数 - JavaScript | MDN

ES6 In Depth: Arrow functions - Mozilla Hacks - the Web developer blog

しかし、できないと言われるとやりたくなるのが人の性。アロー関数で、メソッドやコンストラクタ、作ってやろうじゃないですか!

動作環境

  • Node.js v12.16.2
  • Chrome バージョン81

アロー関数でコンストラクタを作ろう!

コード

アロー関数だけでコンストラクタもメソッドも作れちゃう!thisにレシーバ入れ放題!もう function の打ちすぎで腱鞘炎になったりしないぜ🤪

// アロー関数でthis使えるようにするラッパーthisable(下記)をかける
const Person = thisable((name, age) => {
  this.name = name;
  this.age = age;
});

Person.prototype.sayHi = () => {
  console.log(`Hi! I am ${this.name}`);
};

Person.prototype.canDrink = () =>
  this.age >= 20;


// インスタンスがちゃんとできている!
const taro = new Person("Taro", 23);
console.log(taro); // {name: 'Taro', age: 23}
taro.sayHi(); // Hi! I am Taro
console.log(taro.canDrink()); // true

const jiro = new Person("Jiro", 18);
console.log(jiro); // {name: 'Jiro', age: 18}
jiro.sayHi(); // Hi! I am Jiro
console.log(jiro.canDrink()); // false

アロー関数で this を使えるようにするラッパー関数!

const thisable = (constructor_) => 
  funcHandler(arrow2func(constructor_));

const arrow2func = f => function() {
  const args = [].slice.call(arguments);
  return eval(f.toString())(...args);
};

const funcHandler = f => {
  const handler = {
    get: (target, name) => {
      if (name in target && typeof target[name] === 'function') {
        return arrow2func(target[name]);
      }
      return target[name];
    }
  };

  f.prototype = new Proxy(f.prototype, handler);
  return f;
};

コード詳細

アロー関数を普通のfunctionに変換

アロー関数は、定義時の this を参照します。ならば、 this がレシーバになる場所で再定義すれば良いのです。

普通の function の内部でアロー関数を定義すれば、 this は外側の関数のものを参照できます。

const taro = {name: "Taro"};
taro.sayHi = function(){
  return (() => {
    console.log(`I am ${this.name}`);
  })();
};

taro.sayHi(); // I am Taro

上記と同じことをするために、アロー関数のコード文字列を取り出し、ラッパー関数の内部でevalします。

const sayHi = () => {
  console.log(`I am ${this.name}`);
};

const taro = {name: "Taro"};

taro.sayHi = function(){
  return eval(sayHi.toString())();
};

taro.sayHi(); // I am Taro

これで、アロー関数を普通の function に変換できました。

後は、上記を一般化すれば完成です。 ちなみに arguments もアロー関数で扱えないので、残余引数に変換しておきます。

const arrow2func = f => function() {
  const args = [].slice.call(arguments);
  return eval(f.toString())(...args);
};

メソッド(prototype)をProxyでトラップ

これで、アロー関数をメソッドやコンストラクタに使えるようになりました。ですが、それぞれのメソッドに対しいちいちラッパーを呼び出すのは面倒です。

そこで、コンストラクタをラッパーにくるんだら、その prototype に定義したメソッド(アロー関数で定義)が勝手に function に変換されるようにします。 つまり、定義するときはアロー関数で、呼び出し時には function となって呼び出されるようにします。

Person.prototype.sayHi = () => {
  console.log(`Hi! I am ${this.name}`);
};

const taro = new Person("Taro", 23);
taro.sayHi(); // Hi! I am Taro

これには、 Proxy オブジェクトを使用します。

Proxy オブジェクトはRubyの method_missing のような動作を追加するラッパーオブジェクトで、存在しないプロパティを呼び出された場合にデフォルト処理をするといったことができます。

メタプログラミング - JavaScript | MDN

Javascriptでrubyのmethod_missing的なことをして関数の引数を受け取る - Qiita

コンストラクタの prototypeProxy オブジェクトに置き換えます。

prototype のプロパティの呼び出しをトラップして、それが関数オブジェクト(=メソッド呼び出し)だった場合、 function オブジェクトに変換してから返します。

こうすることで、アロー関数で定義したメソッドが呼び出されるときに function に自動で変換されます。

const funcHandler = f => {
  const handler = {
    // プロパティが参照されたときに、代わりにこのハンドラを呼び出す
    get: (target, name) => {
      // 関数オブジェクトが呼ばれた場合、function形式の関数に変換して返す
      if (name in target && typeof target[name] === 'function') {
        return arrow2func(target[name]);
      }
      // そのまま元のプロパティを返す
      return target[name];
    }
  };

  // コンストラクタのプロトタイプをProxyでラップ
  f.prototype = new Proxy(f.prototype, handler);
  return f;
};

「1つもfunctionは使いたくない!」という原理主義者の方へ

「おい、 arrow2func の中で function 使ってるじゃん」

と思った貴方。こちらのコードを代わりに使えば、完ぺきな function フリーを実現できます。

const arrow2func = f => {
  const [argsSrc, bodySrc] = f.toString().split("=>");
  // 前後に()がある場合消す
  const args = argsSrc.replace(/^ *\(/, "").replace(/\) *$/, "").split(",");
  // 前後に{}がある場合消す
  const body = bodySrc.replace(/^ *\{/, "").replace(/\} *$/, "");
  return new Function(...args, body);
};
const foo = (bar, hoge) => {
  console.log(this.val + bar + hoge);
  return this.val;
};

console.log(arrow2func(foo).toString());

/*
function anonymous(bar, hoge
) {

  console.log(this.val + bar + hoge);
  return this.val;

}
*/

console.log(arrow2func(foo).bind({val: "val"})("bar", "hoge")); // valbarhoge

おわりに

こんなことしてないで、Node.jsの勉強すすめないと…