質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.96%

apply()やcall()を使う機会を知りたい

解決済

回答 6

投稿

  • 評価
  • クリップ 4
  • VIEW 2,509

souta-haruran

score 82

apply()、call()メソッドについて

この2つのメソッドについては、使用している実際のコードを目にすることもあり、これまでにもネットで調べて自分でカスタマイズしつつ検証してみたことはあり、何となく掴めかけてる気はするのですが、実際の実務では使いどころがイメージできておらず使用したことがありません。

どんな時にこの2つのメソッドを使うと便利なのでしょうか?
便利でなくとも、使う意味がある。というシーンはどんな時なのかご教示頂ければと思います。

該当のソースコード

// 昔調べたときに目にしたサイトのコードを一部引用
// 昔なので引用元url見つかりませんでした。。

// apply()を使った例
function Teki() {
  this.hp = 100;
}

function Dragon() {
  Teki.apply(this,arguments);
}
Dragon.prototype = new Teki;

Dragon.prototype.attack = function() {
  console.log("1...ドラゴンの攻撃! ドラゴンのHPは"+this.hp+"だ!");
};

var boss = new Dragon();
boss.attack(); // 1...ドラゴンの攻撃! ドラゴンのHPは100だ!

// call()を使った例
function Teki2() {
  this.hp = 200;
}

function Dragon2() {
  Teki2.call(this);
}
Dragon2.prototype = new Teki2;

Dragon2.prototype.attack = function() {
  console.log("2...ドラゴンの攻撃! ドラゴンのHPは"+this.hp+"だ!");
};

var boss2 = new Dragon2();
boss2.attack(); // 2...ドラゴンの攻撃! ドラゴンのHPは200だ!


上記どちらもそれぞれapply()、call()を使用していますが、例えばcall()を下記みたいに書き換えてみます。

function Teki2() {
  this.hp = 200;
}

function Dragon2() {
  Teki2();
}
Dragon2.prototype = new Teki2;

Dragon2.prototype.attack = function() {
  console.log("2...ドラゴンの攻撃! ドラゴンのHPは"+this.hp+"だ!");
};

var boss2 = new Dragon2();
boss2.attack(); // 2...ドラゴンの攻撃! ドラゴンのHPは200だ!

このように同じ結果が得られています。
こういう感じの理解しかできておらず、使いどころがイメージ出来ていません。

すみませんが、ご教示よろしくお願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 6

+5

 prototype 上の関数を静的関数として使う

Array.prototype 上の関数を疑似配列に適用するのは比較的よく使われますが、この手法は他の関数にも応用できます。

Array.prototype.map.call(document.querySelector('p'), function (element) {
  return element.textContent;
});

 this 値が意図せぬ値になってしまう場合に正規の this 値に戻す

addEventListenerArray.prototype.forEach 等のコールバック関数で prototype 上の関数を使いたい場合、this 値が維持されない為に callapplybind を使う場合があります。

// bad
Hoge.prototype.something = function something () {
  document.addEventListener('click', function handleClick (event) {
    // this === document なので this.foo(); で Hoge.prototype.foo を呼び出せない
    this.foo();
  }, false);
}

// good 1 (Function.prototype.bind)
Hoge.prototype.something = function something () {
  document.addEventListener('click', function handleClick (event) {
    // bind で this 束縛したので this.foo(); で Hoge.prototype.foo を呼び出せる
    this.foo();
  }.bind(this), false);
}

// good 2 (アロー関数)
Hoge.prototype.something = function something () {
  document.addEventListener('click', (event) {
    // アロー関数は上位スコープの this を引き継ぐので this.foo(); で Hoge.prototype.foo を呼び出せる
    this.foo();
  }, false);
}

// good 3 (addEventListener の listener オブジェクト)
Hoge.prototype.handleEvent = function handleEvent (event) {
  this.foo();
}
Hoge.prototype.something = function something () {
  document.addEventListener('click', this, false);  // addEventListener の機能によって第二引数のオブジェクトに this 束縛される
}

 可変長引数に apply()

可変長引数と Function.prototype.apply は相性が良く、静的関数であっても良く使います。

Math.max.apply(null, [1, 2, 3, 4, 5]);  // 5

ただし、this 値を変更せずに可変長引数を配列で渡したい場合は rest parameters しかないので、ES5 で実装する時には苦労します。

 「該当のソースコード」について

質問文中にある「該当のソースコード」ですが、この場合は Function.prototype.apply を利用する必要がないと思います。

function Battle () {
  this.count = 0;
}

Battle.prototype.enemyAttack = function enemyAttack (enemy) {
  console.log(++this.count + ': ' + enemy.name + 'の攻撃!' + enemy.name + 'のHPは' + enemy.hp + 'だ!');
}

function Enemy (name, hp) {
  this.name = name;
  this.hp = hp;
}

var battle = new Battle,
    slime = new Enemy('スライム', 8),
    dragon1 = new Enemy('ドラゴン1', 100),
    dragon2 = new Enemy('ドラゴン2', 200);

battle.enemyAttack(slime);   // 1: スライムの攻撃!スライムのHPは8だ!
battle.enemyAttack(dragon1); // 2: ドラゴン1の攻撃!ドラゴン1のHPは100だ!
battle.enemyAttack(dragon2); // 3: ドラゴン2の攻撃!ドラゴン2のHPは200だ!

Re: souta-haruran さん

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/01/31 22:55

    think49さん
    ありがとうございます。
    何例も回答をご提示いただきありがとうございます。
    prototype、thisなど具体的な回答も示していただいて勉強になりました。

    キャンセル

+3

そういうケースよりは、例えばjQueryオブジェクトに純粋なJSのmap関数を使ったりとか、
Math.max関数の引数に配列を適用したりだとか、
配列用メソッドを持たない配列のようなオブジェクトに対して、配列用のメソッドを使用するケースが考えられます。

 jQueryオブジェクトにJavaScriptのmapメソッドを使用する

$el.map()という風に記述すると、jQueryで定義されたmapメソッドを実行してしまいます。

var $el = $('.sample');

var result = [].map.call($el, function(item, index){
    // 処理
    var $item = $(item);
});

 配列の最小値・最大値を求める

Math.max Math.minの引数に指定できるのは通常だと数値ですが、applyを併用すると配列ごと渡せます。 

Math.max.apply(null, array);
Math.min.apply(null, array);

 配列のようなオブジェクトに対して、配列用のメソッドを使用する

例えば、document.querySelectorAll()で取得できるオブジェクトはNodeListと呼ばれる配列のようなオブジェクトです。
NodeListは配列用のメソッドをそのままでは使用できませんが、callやapplyを使用することで配列用メソッドを使えるようになります。

var el = document.querySelectorAll('.class');

el.slice() // 不可
el.map() // 不可

call+sliceを使って配列に変換するパターン

var el = document.querySelectorAll('.class');

// 変換1
var list = [].slice.call(el);

// 変換2
var list = Array.prototype.slice.call(el)

list.map() // 使える

applyを使って変換するパターン

var el = document.querySelectorAll('.class');

// 変換3
var list = Array.apply(null, el);

list.map() // 使える

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/01/31 23:07

    yamato_hikawaさん
    ありがとうございます。
    具体的な使用シーンを頂きました。
    こういう場面でこういう使い方をすることでこういう結果が得られる。
    とイメージできました。

    キャンセル

+2

有用な使いみちとして、「thisを入れ替えて実行できる」ということがあります。

今はArray.fromというものがありますが、なかった頃にargumentsやノードリストなどの「配列のようなオブジェクト」を配列に変換するために、Array.prototype.slice.call(arguments)のように実行することがあります。

あと、.applyは引数リストを配列で渡せます(今は...配列で展開できますが)。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/01/31 23:05

    maisumakunさん
    ありがとうございます。
    > thisを入れ替えて実行できる
    分かりやすい言葉でした。
    Array.fromというものについてもまだ勉強不足で知らなかったものなので、
    頂いた回答をもとに勉強していきます。

    キャンセル

+2

下記は現代的JS原理主義者による文章です。彼はIE排他主義者でもあります。それを理解の上、お読みください。


もはやapply()call()は必要とされていません。もし、使うことがあるとすれば、パフォーマンス改善のためのリファクタリングを行うときですが、その機会は極僅かです。

質問のソースコードの手法が古い

まず、質問のソースコードは、プロトタイプベースで継承を行う為の古い手法です。この手法にはコンストラクタが余計に呼ばれるという根本的な問題があり、現代的なJavaScriptではもはや使われていません。現代的なJavaScriptではクラスベースで継承を行います。

class Teki {
  constructor() {
    this.hp = 100;
  }
}

class Dragon extends Teki {
  attack() {
    console.log("1...ドラゴンの攻撃! ドラゴンのHPは"+this.hp+"だ!");
  };
}

const boss = new Dragon();
boss.attack(); // 1...ドラゴンの攻撃! ドラゴンのHPは100だ!

class Teki2 {
  constructor() {
    this.hp = 200;
  }
}

class Dragon2 extends Teki2 {
  attack() {
    console.log("2...ドラゴンの攻撃! ドラゴンのHPは"+this.hp+"だ!");
  }
}

const boss2 = new Dragon2();
boss2.attack(); // 2...ドラゴンの攻撃! ドラゴンのHPは200だ!

ここにapply()call()が入り込む余地はありません。

複数の引数を配列としてまとめて渡すのにapply()は不要

apply()は複数の引数を配列として渡すことができます。しかし、...argを使えば、普通の呼び出しでも配列を分割して複数の引数にしてくれます。apply()を使う必要はありません。

const f = (a, b, c) => a + b + c;
const a = [2, 3, 5];
console.log(f.apply(this, a)); // ×
console.log(f(...a)); // ○

ジェネリックメソッドは混乱の元になるため、使用すべきではない

他にapply()call()の使い方として、ジェネリックメソッド(generic method)があります。ArrayとStringのメソッドは意図的にジェネリック(intentionally generic)として定義されており、これがジェネリックメソッドです。しかし、ほとんどのメソッドにおいては、単純にArrayやStringに変換してからそのメソッドを適用しているに過ぎず、あえて使うメリットはありません。

const list = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
console.log(Array.prototype.join.call(list)); // ×
console.log(Array.from(list).join()); // ○
console.log(String.prototype.search.call(list, 'b')); // ×
console.log(String(list).search('b')); // ○

Array.prototype.push()など副作用を伴うメソッドはapply()call()を使うしかありませんが、意図通り動くかどうかはthisとして渡すオブジェクト次第になります。

const list = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
Array.prototype.push.call(list, 'd');
console.log(list);

これがどのような結果になるかをすぐに予測できなければ、使わない方がいいでしょう。もっとも、全てのオブジェクトはimmutableに扱うべきであり、副作用を伴うメソッド自体を使用すべきではありません、と関数型原理主義者が言っていました。


以上ですが、わりと本気で、このようにあるべきだと私個人は思っています。。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/01/31 22:45 編集

    raccyさん
    ありがとうございます。
    現代的な書き方という点を含めいい勉強させてもらうきっかけになりました。
    更には色々なパターンで例をいただき大変感謝しております。

    キャンセル

checkベストアンサー

+1

結論から言えば上の例(callを使った方)が良いですね。
上の例と下の例は応用した時に違いが生まれます。

// 上の例
var hoge;
hoge = boss2.attack;
hoge() // 2...ドラゴンの攻撃! ドラゴンのHPはundefinedだ!

// 下の例
var hoge;
hoge = boss2.attack;
hoge() // 2...ドラゴンの攻撃! ドラゴンのHPは200だ!

HPはundefinedって超絶強そうですね(小並感


callを使った上の例の場合、そのものにスコープが行きます。
これはbossのHPを変更した場合に顕著になります。

var hoge;
boss2.hp = 3000;
boss2.attack(); // 2...ドラゴンの攻撃! ドラゴンのHPは3000だ!
hoge = boss2.attack;
hoge(); // 2...ドラゴンの攻撃! ドラゴンのHPはundefinedだ!

callを使わない下の例の場合、こうなります。

var hoge;
boss2.hp = 3000;
boss2.attack(); // 2...ドラゴンの攻撃! ドラゴンのHPは3000だ!
hoge = boss2.attack;
hoge(); // 2...ドラゴンの攻撃! ドラゴンのHPは200だ!

これはTeki2の下位概念にDragon2インスタンスが作られ、boss2という名前が与えられますが、
boss2自体のプロパティにはhpは存在しません。
なのでスコープチェーンに従い、boss2の上位にあるTeki2.hpの値が利用されます。

しかし、今回boss2.hp = 3000と外部からboss2インスタンス自体のプロパティをいじったので、
boss2.hpは3000だけど、Teki2.hpは200というチグハグな状況が生まれます。
これを「種族HPだからMaxHPは親を辿って参照しよう!」という使い方も考えられますが、ちょっとイレギュラーなのでやめておいたほうがいいかと思います。


お次はapplyの解説です。
ぶっちゃけQiitaの記事がまとまり過ぎてて説明する余地がほぼ無いのですが…

関数型プログラミングとJavaScriptは相性が非常に良く、私は大抵のケースをリスト操作で片付けます。
リスト操作を覚えれば、殆どのケースでfor文を使う必要がなくなります。

applyは配列から関数を叩ける、thisを誘導出来るという2点が魅力です。
その結果、配列を持ってて加工したいケースが多いのでapplyは重宝します。

例として生徒の国語の最高得点、最低得点、平均点をJavaScriptで求めてみましょう。
今回は前者の「配列から関数を叩ける」点がメリットとして働いている事が分かるかと思います。
多少イディオムを使っているとはいえ、たったこれだけの行数で済むのは力強いですね。

var user1 = {
  kokugo: 80,
  sugaku: 75
};
var user2 = {
  kokugo: 85,
  sugaku: 60
};
var user3 = {
  kokugo: 40,
  sugaku: 90
};
var users = [user1, user2, user3]; // こういうケースはNode.jsやSPAでJSのカバー範囲が広くなると頻出します
var output_nums = function (nums) {
  var mean = function () {
    var plus = function (a, b) {return a + b;};
    var sum = Array.prototype.slice.call(arguments).reduce(plus);
    return Math.round(sum / arguments.length);
  }
  console.log(Math.max.apply(null, nums));
  console.log(Math.min.apply(null, nums));
  console.log(mean.apply(null, nums));
}

output_nums(users.map(function(it){return it.kokugo}));
// 85
// 40
// 68

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/01/31 22:48 編集

    miyabi-sunさん
    ありがとうございます。
    私の例を踏襲したわかりやすい形でのご回答に大変感謝しております。
    call()、apply()と質問に対する回答を細かくいただけて勉強になります。
    今回は質問の例に沿ってご回答いただいたこちらをベストアンサーに選ばせていただきました。

    キャンセル

+1

たとえば、

function Dragon2() {
  Teki2();
}


のところでTeki2()を実行していますが、
Teki2のコードは

function Teki2() {
  this.hp = 200;
}


となっていますが、この時のthisはwindowになります。
ですのでこのコードを実行してみると、windowにhpというプロパティが生成されており値が200になっていると思います。
こういったときに

function Dragon2() {
  Teki2.call(this);
}


とすることで、呼ぶ関数内でのthisをapply()やcall()で渡すことで変更することができます。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/01/31 23:03

    turbgraphics200さん
    ありがとうございます。
    端的に示していただきわかりやすい回答でした。
    > この時のthisはwindowになります。
    言われたらその通りですよね。
    全般通して分かりやすかったです。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.96%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる