[Node.js] apply関数の挙動を懇切丁寧に解説する
目次
Introduction
javascriptやNode.jsを使用しているコードでたまーにみるapplyという関数。読者の皆さんはこの関数がどのように挙動をするか正しく把握してますか?
私が関わっているプロジェクトではアロー関数を多く使用しておりClassやインスタンスを使用した実装ではないため、あまりapplyやcallを使用した実装を見かけません(thisの概念を入れると複雑になるので)。そのためapplyなどの関数をあまり触れる機会がなく挙動がよくわかっていなかったのですが、ちょっとapply関数に触れる機会があったのでこの際正しく理解しようかと思い記事を書きました。
applyやcall,bindに関する記事は多くあるので今回は自分用のメモとして書いていきたいと思います。
環境
$ node -v
v16.17.0
$ npx tsc -v
Version 4.8.3
applyの基礎
最初にapplyの構文について解説していきます。
私はバックエンドエンジニアのためNode.jsで解説していきます。window関数とかよくわからないので
例1-1) 引数を指定した基本的な構文
function hoge (arg1, arg2) {
console.info(this.props);
console.info(arg1);
console.info(arg2);
};
hoge.apply({ props: 'fuga' }, ['piyo', 'foo']);
// fuga
// piyo
// foo
一番わかりやすい例になります。 applyの第1引数はthisになります。今回は{ props: 'fuga' }
がthisに相当します。そのためthis.props
でfugaを取り出せます。
次に第2引数は配列になり配列の値は可変長引数になります。そのため上記の例でさらに追加で['piyo', 'foo', 'bar']
としてもhoge関数でbarを受け取れません。
このようにapply関数はthisを意図的に変更して指定の関数を実行することが可能です。
例1-2) 引数のない場合
function hoge() {
console.info('hogehoge');
};
hoge();
hoge.apply();
// hogehoge
// hogehoge
applyの説明記事でよくみるやつですね。特に引数を必要としない関数は普通に呼び出してもapplyを使用しても結果に変わりはありません。そりゃそうだ
例1-3) 配列関数の場合
次にMDNで記載がある例です
const array = ['a', 'b'];
const elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array);
// ["a", "b", 0, 1, 2]
MDNの説明では、配列の関数pushでapplyを使用することでelements
の引数が可変長になり配列の中に配列ができることがなく、ちゃんと配列全体を1つの要素にできるよ!って言っています。
私的には変数の値を変更するぐらいならconcatで新しい値にした方がいいような気がしますが、実装によっては値をそのまま変更したい場合もあるのでしょう。
const array = ['a', 'b'];
const elements = [0, 1, 2];
array.push(array, ...elements);
console.info(array);
// <ref *1> [ 'a', 'b', [Circular *1], 0, 1, 2 ]
でも同じような結果が得られると思いましたが、循環参照になりダメでした
また余談になるのですが、array.push.apply(array, elements);
はarrayのpush関数を使用してarrayをthisにしてelementsを引数にしています。そのため
const array = ['a', 'b'];
const array1 = ['foo', 'bar'];
const elements = [0, 1, 2];
array.push.apply(array1, elements);
console.info(array);
console.log(array1);
// [ 'a', 'b' ]
// [ 'foo', 'bar', 0, 1, 2 ]
というようにarray.push.apply(array1, elements);
の第1引数に別の配列を指定したらarray自体にはpush関数が適用されません。
通常のarray.push(xxx)
はpush関数の中のthisがarrayになりますが、applyを使用することによりthisをarray1に変換して処理します。そのためarrayにはpush関数が適用されずにarray1をthisとしてarray1の値にpush関数が適用されます。
例1-4) use strictの使用
MDNでは第一引数をnullにするとthisはグローバルオブジェクトに置き換えられると記載されています。
global.val = 'fuga'
function hoge () {
console.info(this.val);
};
hoge.apply(null);
// fuga
確かにglobalオブジェクトがthisになっていますね。Node.jsだけglobalオブジェクトでブラウザだとwindowオブジェクトになるみたいです。
しかしuse strictにするとnullやundefinedを指定するとthisはグローバルオブジェクトにならずnullやundefinedがそのままthisオブジェクトになってしまいます。
'use strict'
global.val = 'fuga'
function hoge () {
console.info(this);
};
hoge.apply(null);
// null
そのため、第一引数は厳密にはnullではなくglobalThis
を指定した方が安全です
'use strict'
global.val = 'fuga'
function hoge () {
console.info(this.val);
};
hoge.apply(globalThis);
// fuga
例1-5) アロー関数
const hoge = (arg) => {
console.log(this);
console.log(arg);
};
hoge.apply({props: 'fuga'}, ['piyo']);
// {}
// piyo
アロー関数にはthisの概念がないので第1引数は無理されます
実際に実装でどのように使用するかは以下の記事が参考になります。
Javascriptのcall/apply関数のプロっぽい使い方 〜 JSおくのほそ道 #014
typescriptでのapply
typescriptをそのまま使用したいのでts-node-devを使用します
npmのプロジェクトの作成方法は省きます。以下のようにしてファイルを実行して検証します
npx ts-node-dev apply.ts
typescriptでは、javascriptでのapplyと挙動が少し挙動が変わります
例2-1) 第1引数null
function hoge (arg: string) {
console.info(global);
console.info(arg);
};
hoge.apply(null, ['fuga']);
// globalオブジェクト
// fuga
typescriptは、直接globalのthisを呼び出せないため、globalでglobalオブジェクトを呼び出します。
例2-2) 第1引数globalオブジェクト
どうしてもglobalのthisを使用したい場合は引数に指定します。
function hoge (this: any, arg: string) {
console.info(this);
console.info(arg);
};
hoge.apply(globalThis, ['fuga']);
// globalオブジェクト
// fuga
hoge.apply(111, ['fuga'])
// 111
// fuga
第1引数をnullにした場合は、console.log(this);
の結果はnullになります。 型と引数の数さえあっていれば第1引数はなんでも渡されます。んーここの挙動難しいですね
tsconfig.jsonのapplyに関する設定
strictBindCallApply
tsconfig.jsonの設定でstrictBindCallApplyを使用するとapplyやcall,bindを型安全に使用できます
function hoge (arg: string) {
console.info(arg);
};
hoge.apply(globalThis, 'fuga');
function hoge (arg: number) {
console.info(arg);
};
hoge.apply(globalThis, ['fuga']);
上記の例は、型が間違ってますが、strictBindCallApplyがfalesだとチェックしてくれません。
一つ目の例は、applyの第2引数が配列でないといけないですが、stringになっています。
二つ目の例は型がnumberですが、引数はstringの配列になっています。
call,bindの場合分けは、以下の記事が参考になります。
→ TypeScriptの関数を振り返る
また、strictBindCallApplyの詳しい記事は以下になります
→ サバイバルTypeScript
まあ、皆さんはtsconfig.jsonの設定でstrict: true
にしてType Checkingを全て適用にしていると思うので特に問題はないと思いますが
apply・call・bindの書き分け
call
callはapplyとほとんど同じ挙動をしますが、引数の指定方法が違います。
function hoge (arg1, arg2) {
console.info(this.props);
console.info(arg1);
console.info(arg2);
};
hoge.call({ props: 'fuga' }, 'piyo', 'foo');
// fuga
// piyo
// foo
callはapplyと違って第2引数以降を個々に指定します。
そのため配列の値をcallの引数にしたい場合は可変長引数で指定できます。
function hoge (arg1, arg2) {
console.info(this.props);
console.info(arg1);
console.info(arg2);
};
const list = ['piyo', 'foo'];
hoge.call({ props: 'fuga' }, ...list);
// fuga
// piyo
// foo
bind
bindもapplyやcallと同じような挙動をしますが、applyやcallと違う点はbindを呼び出した際にbindされた関数をすぐに実行するのではなく、bind関数を作成します
function hoge (arg1, arg2) {
console.info(this.props);
console.info(arg1);
console.info(arg2);
};
const hogeBind = hoge.bind({ props: 'fuga' }, 'piyo', 'foo');
hogeBind();
// fuga
// piyo
// foo
bind関数を事前に生成しておくことができます
終わりに
以上でapply,call,bindの基本的な説明を終わりにしたいと思います。コード例も多く記載があり少しはわかりやすく書けたかなと思います。
しかしながらapplyはやっぱり直感的にわかりにくい関数のため使用しない方が可読性のあるコードになるかなと個人的には思います。もしapplyを使用する場合、また既存のコードで見つけた場合は利用方法に注意して扱いましょう