this関数・メソッド

【JavaScript】 そろそろcall()とapply()を理解してみようと思う

更新日:2023/06/22

JavaScriptにはcallとapplyというメソッドがある。
あまり使わないなと思って理解することを放棄していたのだが、いい機会なので記事にしながら理解していこうと思う。


2023/6/22 どちらの関数が配列を受け取るのか確認しやすいように最初に構文を記述しました。

 

call()とapply()の定義

ECMAScript仕様では、call()とapply()は次のように構文が定義されています。

call()の構文

Function.prototype.call ( thisArg, ...args )

apply()の構文

Function.prototype.apply ( thisArg, argArray )
  • thisArg:this値として渡す値
  • ...args:カンマ(,)で区切られた引数のリスト
  • argArray:引数がセットされた配列

 

call()とapply()は関数オブジェクトのメソッドである

call()とapply()は関数オブジェクトのメソッドです。
関数として定義したものは、全てこの三つのメソッドを呼び出せます。

試しに呼び出してみましょう。

call()とapply()呼び出し例


function a(){
    console.log("test!");
}
a.call();     // 結果: test!
a.apply();  //  結果: test!

同じ結果がコンソール画面に表示されました。
何がおこったでしょうか?

この二つのメソッドは、自分自身のコード(今回は関数aの中のコード)を実行しています。

a()で実行すればいいんだから、必要ないよね?

この使い方では意味がありませんね。

では、どんな使い方をするのでしょうか。

 

call()とapply()は引数でthisを変更できる。

call()とapply()は引数でthis値を変更できます。

関数のthisはWindowオブジェクト

次の例は、関数内でthisを参照しています。

call()とapply()呼び出し例


function a(){
    console.log(this); // 結果: Window(グローバルオブジェクト)
    console.log(this.message); // 結果: undefined
}
a.call();
a.apply();

関数のthisはグローバルオブジェクトで、ブラウザの場合はWindowオブジェクトです。
Windowオブジェクトにはmessageプロパティがないので、this.messageはundefinedと表示されます。

ちなみにstrictモードの場合、thisにはundefinedがセットされます。
undefineはプロパティが存在しないので、messageを参照するとエラーでストップします。

call()とapply()呼び出し例 strictモード


"use strict";
function a(){
    console.log(this); // 結果: undefined
    console.log(this.message); // 結果: TypeError: this is undefined
}
a.call();
a.apply();

関数のthisを自分の都合で変えてみる

call()とapply()は、第一引数で指定されたオブジェクトを関数のthisと置き換えます。

call()とapply()呼び出し例


function a(){
    console.log(this);         // ①
    console.log(this.message); // ②
}
a.call( { message: "test!!" } );
      //   ↑thisとして渡すオブジェクト
      // 実行結果 : ①  this : object { message: "test!!" }
      //            ②  this.message :  "test!!"

a.apply( { message: "test!! test!!" } )
      //   ↑thisとして渡すオブジェクト
      // 実行結果 : ①  this :  object { message: "test!! test!!" }
      //            ②  this.message : "test!! test!!"

messageプロパティを持つオブジェクトが、関数のthisに置き換わっているのがわかりますね。

第一引数にnullやundefinedを指定したときの挙動

第一引数にnullやundefinedを指定すると、thisはグローバルオブジェクト(ブラウザはwindowオブジェクト)となります。

nullやundefined指定時の挙動

function a(){
    console.log(this);
}
a.call( null );            // 結果: window
a.call( undefined );       // 結果: window
a.apply( null );           // 結果: window
a.apply( undefined );      // 結果: window

nullやundefinedが、グローバルオブジェクトに自動変換されるのは少し奇妙に感じるかもしれません。
この動作は、次のJavaScriptの内部アルゴリズムで定義されています。
9.2.1.2 OrdinaryCallBindThis ( F, calleeContext, thisArgument ) | ECMAScript2020言語仕様

異なるプラットフォームではグローバルオブジェクトの値が異なります。
例えば、ブラウザはWindowオブジェクトで、Node.jsはglobalオブジェクトです。

このことを念頭に置くと、関数内でグローバルオブジェクトを使用する必要があるとき、nullやundefinedが自動でプラットフォームに適したグローバルオブジェクトに変換されるのは便利な仕様だと言えます。

ただしstrictモードでは、指定したものがそのまま適用されます。

strictモードでnullやundefined指定時の挙動


"use strict";
function a(){
    console.log(this);
}
a.call( null );            // 結果: null
a.call( undefined );       // 結果: undefined
a.apply( null );           // 結果: null
a.apply( undefined );      // 結果: undefined

こちらの動作についても、先ほどのリンク先のアルゴリズムで定義されています。
class構文内では強制的にstrictモードが適用されるなど、JavaScriptは非strictモードからの脱却をはかろうという意図が見えます。

つまりnullやundefinedの自動変換は、JavaScriptとしてはあまり好ましくない動作といえます。

そのため、この挙動に頼ったプログラミングは避けるべきです。
その代わり、globalThisというグローバルオブジェクトを指し示すキーワードが追加されているので、こちらを使用します。

globalThisを指定

function a(){
    console.log(this);
}
a.call( globalThis );               // 結果: window(ブラウザの場合)
a.apply( globalThis );            // 結果: window(ブラウザの場合)

アロー関数はthisを渡さない

アロー関数は、関数そのものがthisを所持していません。
そのため、call()やapply()の第一引数は無効となります。


const a = () => {
    console.log( this );
};
a();                           // 結果: window(ブラウザの場合)
a.call( { m : "a" } );  // 結果: window(ブラウザの場合)

ただし、call()とapply()の違いで説明する引数は渡すことができます。

アロー関数については、次のページを読んでみてください。
■参考:【JavaScript】 アロー関数はかっこいいだけじゃない!

bind()で作成したオブジェクトには使用できない

bind()は、関数オブジェクトにthisをセット(バインド)した、特殊な関数オブジェクトを返すメソッドです。

このオブジェクトに対してcall()やapply()を呼び出した場合、bind()でセットしたthisが使用されます。


function a(){
    console.log( this.message);
}

const bindA = a.bind( { message: "test!!" } );
        
bindA.call( { message: "テスト!!" } ); // 結果: test!! ← callの第一引数が無視された

 

call()とapply()の違い

call()とapply()は第二引数以降に、関数へ渡す引数を指定できます。

ただしcall()とapply()では、その記述方法が異なります。

call()は引数の羅列で指定

callは第一引数でthisを指定して、第二引数以降には関数への引数を「 , (カンマ)」で区切って指定します。
普通に関数を呼び出すイメージですね。

構文

関数.call( this値 , 引数1 , 引数2 … );

■引数

this値 : 関数に渡すthis値。省略可能

引数1 , 引数2 … : : 関数に渡す引数。省略可能

■戻り値

関数の実行結果

call()での引数指定例


function a( p1 , p2 , p3 ){
    console.log( this.message + p1 + p2 + p3 ); // 結果: test:ok!ok!!ok!!!
}
a.call( { message:"test:" } , "ok!" , "ok!!" , "ok!!!");
    //                                            ↑引数を羅列

apply()は配列で指定

apply()は、引数を配列で指定します。

構文

関数.apply( this値 , [ 引数1 , 引数2 …] );

■引数

this値 : 関数に渡すthis値。省略可能

[引数1 , 引数2 …] : : 関数に渡す引数を要素とした配列。省略可能

■戻り値

関数の実行結果

call()での引数指定例


function a( p1 , p2 , p3 ){
    console.log( this.message + p1 + p2 + p3 ); // 結果: test:ok!ok!!ok!!!
}
a.apply( {message:"test:"} , [ "ok!" , "ok!!" , "ok!!!" ] );
    //                                            ↑配列で指定

 

call()とapply()の使い道

call()とapply()の使い道を考えてみます。

可変長の引数リストを受け取る関数に配列で渡す

applyを使うと、配列のデータをそのまま関数に渡すことができ、状況によってはコードの記述が簡潔になります。


const data = [ 1 , 2 , 3 ];

a( data[0] , data[1] , data[2] ); // 個別に記述するのは面倒

a.apply( null , data ); // 配列のまま渡せるので楽

また、次の例のような可変長の引数を受け取る関数も、applyを使うと楽になるケースがあります。

例:Math.max(値1,値2,値3,・・・・) 与えられた引数の中で最大値を返す

配列の中で一番大きい値を取得したいケースはよくあると思います。
そこで普通にMath.max()を使用するなら、次のようなコードが考えられます。

Math.maxで配列の最大値を求める


const data = [1,20,3,40,5];
const maxValue = data.reduce( (a,b)=> Math.max( a , b )  );

console.log( maxValue ); // 結果: 40

ループめんどくさい。
forループの方が速くないかな?とか考え始めるといやんなっちゃう。

そんなとき、applyを使うと悩まなくて済みます。

Math.max.applyで配列の最大値を求める


const data = [1,20,3,40,5];
const maxValue = Math.max.apply( null , data );

console.log( maxValue ); // 結果: 40

Math.maxはthisを使っていないので、第一引数はnullで大丈夫です。

ただ、今回のケースはapplyを使用しなくても、変数の前に「...」をつけると自動で展開してくれるスプレッド構文を使用するという方法があります。
さらに、スプレッド構文を使用すると、複数の配列を一度に指定することもできるという利点があります。

Math.maxで配列の最大値を求める その2


const data1 = [ 1 , 20 , 3 , 40 , 5 ];
const data2 = [ 10 , 200 , 30 , 400 , 50 ];
const maxValue = Math.max( null , ...data1 , ...data2 );

console.log(maxValue); // 結果: 400

参考:【JavaScript】 コード中の「...」は意味があった

なりすまし

call()やapply()を使うと、自分が持っていない機能を使用できます。

自作関数で解説すると

なんのこっちゃ?

になるので、とりあえず実践的な例でいきます。

例えばpタグ内のテキストを配列に格納したいとします。
そこで次のようなコードを作成しました。

pタグのテキストを取得


const pText = ( document.querySelectorAll( "p" ) ).map( function (e) {
    return e.textContent;
});
console.log( pText ); // TypeError: document.querySelectorAll(...).map is not a function

残念ながらエラーが出てしまい、動作しませんでした。

map()は、配列の要素を一つずつ処理して新しい配列を作成するArrayオブジェクトの関数です。
Arrayオブジェクトは、数字でアクセスできるメンバーとlengthプロパティを持っています。

こんな感じ↓
0 : 値 , 1 : 値 ,  2 : 値 , length : 3

document.querySelectorAll()は、html内の要素を検索してArrayっぽいオブジェクトを返します。

こんな感じ↓
 0 : 要素 , 1 :  要素 , 2 : 要素 ,   length : 3 

似てますね。
でも、ここ重要!

Arrayっぽいオブジェクトなだけで、map()メソッドを持っていません。
つまりmap()を実行できないのです。

そこでthisとしてquerySelectorAll()の結果を渡してあげると、map()メソッドが実行できてしまうのです。

つまりArrayっぽいオブジェクトが、Arrayオブジェクトになりすましたわけです。

なりすまし例


const pText = Array.prototype.map.call( document.querySelectorAll( "p" ), function (e) {
    return e.textContent;
});

自分でもよくわからないので、map()メソッドのコードを再現してみます。

map()メソッドの想像コード


const myArray = function (引数1,引数2,引数3,・・・) {
    // thisのプロパティとして添え字0から順番に引数をセットする処理
};
myArray.prototype.map=function(callBack){
    const result = [];
    for(let i = 0 ; i < this.length ; i ++){
        result.push( callBack(this[i]) )
    }
    return result;
};

const pText = myArray.prototype.map.call(document.querySelectorAll( "p" ), function (e) {
    return e.textContent;
});
console.log( pText );

実際にこんなコードになっているわけではありません。
あくまで自分で作ってみたら、ということです。
(実際のアルゴリズム:22.1.3.18 Array.prototype.map ( callbackfn [ , thisArg ] ))

このコードからArrayオブジェクトのthisになりすます様子を想像できるのではないでしょうか。

なりすましは正常に動作するとは限らない

次のコードは、なりすましに失敗しています。

なりすまし失敗例


const myFunc = function ( message ) {
    this.message = message;
};
myFunc.prototype = {
    proc : function(){
        console.log( this.proc2( this.message ) );
    },
    proc2 : function ( message ) {
        return message + "可愛いい";
    }
};

(new myFunc( "ネコが" ) ).proc();     // 結果: ネコが可愛いい
(new myFunc( "ネコが" ) ).proc.call( { message : "自分が" } ); 
        // TypeError: this.proc2 is not a function

メソッドprocに、call()内でthisを与えたことで、this.proc2が未定義となります。
そのため、メソッドproc内でthis.proc2を呼び出すことができません。

正常に動作させるには、次のようにproc2プロパティを定義したオブジェクトを渡す必要があります。

(new myFunc( "ネコが" ) )
    .proc.call( { message : "自分が" , proc2: myFunc.prototype.proc2} );

このようにcall()とapply()は、関数コードで使用しているプロパティを把握しているという前提で使用すべき機能です。

コールバックにthisを与える

あまり利用する場面がないと思いますが、コールバック関数にthisを与えることができます。

関数式で渡したコールバック関数のthisは、windowです。
(strictモードではundefined)


function myCalltest( callBack ) {
    callBack();
}

function myCall(){
    myCalltest( function () {
        console.log( this ); // window または undefined
    });
}
myCall();

ですがaddEventListenerなどに与えたコールバック関数は、thisに要素がセットされ呼び出されます。


document.querySelector( "p" ).addEventListener( "click",
                function () {
                    console.log(this);  // 結果: <p> ← thisがセットされている!!
                });

これはコールバック関数を呼び出すときに、call()を使用してthis値をセットしているからです。


addEventListener( callBack ) {
   ・・・処理
    callBack().call( <p>要素 );
}

 

call()とapply()はnewができない

newでオブジェクトのインスタンスを作成するとき、call()とapply()は使用できません。

インスタンスを作成するには、コンストラクタを呼び出して新しくオブジェクトを生成する必要があります。


function a(x , y){
    this.x = x;
    this.y = y;
}
const b = new a(1,2);

上の例では、new 演算子で新しいオブジェクトを作成してxとyのプロパティを設定しています。

では、あらかじめ用意しておいたオブジェクトに、xとyのプロパティを追加する目的で、コンストラクタを実行してみます。


const obj = { z:100 };

function a(x , y){
    this.x = x;
    this.y = y;
}
const b = new a.call( { z : 100 } ,1,2 ); // TypeError: a.call is not a constructor

a.callは、コンストラクターではないので、new演算子が使用できないのです。

今回のような目的なら、次のようにObject.assignを使用してオブジェクトをコピーするなどの方法は考えられます。


const obj = { z : 100 };

function a(x , y){
    this.x = x;
    this.y = y;
}

const b = new a( 1,2 );

Object.assign( obj , b );

目的により効果的な方法が異なるので、いろいろ考えてみてください。

 

まとめ

call()やapply()を使っているコードを見ると、なんかかっこいいなと思っていました。
JavaScriptの標準組み込み関数で使うと便利なケースもありそうですね。

ただ自作コードで使うのはちょっと控えた方がよさそう。
普通に引数で渡した方がわかりやすい。

特にドキュメントを書いてない、にわかプログラマーな僕は、時間がたってコード見たら絶対悩みます。

更新日:2023/06/22

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。