JavaScriptで名前付き関数を動的に生成する
結論
名前付き関数を返す関数を返す関数を作れば実現できる
↑の手法は関数の中身自体を実質evalするという危険極まりない方法なので激しくオススメできない
thisに依存しないPure Functionならいいけど
コード
code:ts
function namedFunction (name, func, thisarg?) {
const str = `return function (func, thisarg) {
return function ${name} (...args) {
return func.call(thisarg || this, ...args);
}
};`;
return new Function(str)()(func, thisarg);
}
解説
とある理由からランタイムに名前付き関数を生成したくなった
Function.prototype.nameが、関数の名前を返すことは意外と知られていない。
code:ts
const isKeroxp = a => a === "keroxp";
function doKeroxp (a) {
return a + "keroxp";
}
console.log(isKeroxp.name); // => "isKeroxp"
console.log(doKeroxp.name); // => "doKeroxp"
コード再掲
code:ts
function namedFunction (name, func, thisarg?) {
const str = `return function (func, thisarg) {
return function ${name} (...args) {
return func.call(thisarg || this, ...args);
}
};`;
return new Function(str)()(func, thisarg);
}
↑の関数が何をやっているかというと
1. namedFunctionは、nameという名前のfuncという関数の、thisargで束縛された関数を返す
2. strは、nameという名前のfuncという関数をthisargでcallする関数を返す無名関数の文字列である
3. return new Function(str)().call(this, func, thisarg);
Functionは、new引数に文字列を与えるとそれをevalする関数を生成する関数になる
new Function(str)()
で、strの最初の関数が返ってくる
これはfunc,thisargを受け取って、funcをthisargでcallするnameという名前の関数を返す関数である
.(func, thisarg)
で、nameという関数を取得している
三回クロージャを挟んでいる(二回でない)理由は、funcとthisargを文字列の中に組み込めないからだ
Function.prototype.bindを使うと二回で実装できるが...
code:ts
function boundNamedFunc (name, func, thisarg?) {
const str = `return function ${name} (...args) {
return func.call(this.thisarg||this, ...args);
}`
return new Function(str)().bind({func, thisarg});
}
code:ts
const func = bundNamedFunc ("myFunc", a => a);
console.log(func.name); // => bound myFunc
bindを使うと関数名にboundが付与されてしまうのである
まぁ別にそれでも良いんだけどねkeroxp.icon
テスト
code:ts
describe("namedFunction", () => {
test("名前付き関数を返す", () => {
let o = { i : 1};
const func = namedFunction("myFunc", a => o.i += a);
expect(func.name).toBe("myFunc");
expect(func(10)).toBe(11);
});
test("this束縛", () => {
const self = new function() {
this.title = "title:"
};
const func = namedFunction("returnTitle", function (arg) {
return this.title + arg;
},self);
expect(func.name).toBe("returnTitle");
expect(func("keroxp")).toBe("title:keroxp");
});
});
なかなかパズルみたいで面白かったkeroxp.icon*9
オチ
Function コンストラクタで生成された Function オブジェクトは、関数が作成されたときにパースされます。これは、関数をfunction 文で宣言し、それをコード内で呼び出した場合に比べ、コードの他の部分と一緒にパースされるため、効率が落ちます。
メタプログラミングの宿命なんだなぁ