JavaScript: As diferenças entre os métodos Call e Apply

Introdução

Neste artigo irei demonstrar duas formas de invocação de métodos. As implementações call e apply possibilitam informar o contexto do método e executá-lo imediatamente, seguindo uma abordagem diferente do método bind que prepara a execução informando o contexto e os parâmetros.

Como funciona?

Os métodos call e apply facilitam que você escreva um método e componha outros objetos, uma boa definição extraída do Livro JavaScript Patterns é que esta feature específica da linguagem JavaScript proporciona emprestar um método, onde você pode definir uma função atribuída no contexto global sem mesmo ter um objeto ou classe como owner, claro que atribuir ao contexto global funções e objetos não é uma boa prática, porém você pode tomar emprestado métodos de outros objetos ou classes.

Diferenças entre o Call x Apply

Ambos resolvem o mesmo problema na linguagem JavaScript, como foi definido anteriormente emprestando métodos.
A real diferença está nos parâmetros, Apply invoca uma função com o this (contexto) e um array com os parâmetros da função, já o Call utiliza como primeiro parâmetro o this (contexto) e os próximos são os parâmetros da função, veja abaixo as definições dos métodos.

fun.call(thisArg[, arg1[, arg2[, ...]]])
fun.apply(thisArg, [argsArray])

Ok, agora na prática.

Serão definidos métodos que serão emprestados, utilizando os métodos apresentados call e apply. A classe ShopCart toma emprestado os métodos notifyClient , sendMail e getDeals, neste cenário foram definidos métodos em um contexto global, porém estes métodos poderiam ser de um objeto ou classe.

Foram criados dois exemplos seguindo os padrões EcmaScript (ES5 e ES6).

ES5

function notifyClient(message) {
  alert(message);
}

function sendMail(customer, total) {  
  notifyClient.call(this, 'TODO: Send mail, (' + String(customer.id) + ', ' + String(total) + ')');
}

function getDeals() {  
  var product = arguments[0];
  var minPrice = arguments[1];
  
  product.price = Math.round(product.price * 0.9, 2);
  
  if(product.price < minPrice) {
  	product.price = minPrice;
  }
}

function ShopCart(){
  
}

ShopCart.prototype.items = [];

ShopCart.prototype.customer = { id: 1 };

ShopCart.prototype.total = function total() {
  var total = 0;
  var item;
  
  for(var i=0, c=this.items.length; i<c; i++){
    item = this.items[i];
    total += item.price * item.quantity; 
  }
  
  return total;
};

ShopCart.prototype.addItem =  function addItem(product) {
  this.getDealsByProduct(product);
  this.items.push(product);
  notifyClient.call(this, 'Item added ' + product.name);
};

ShopCart.prototype.getDealsByProduct =  function getDealsByProduct(product) {
  getDeals.apply(this, [product, product.price * 0.6]);
};

ShopCart.prototype.placeOrder =  function placeOrder() {
  sendMail.apply(this, [this.customer, this.total()]);
};

var shopCart = new ShopCart();

shopCart.addItem({
  id: 1,
  name: 'Xbox Game',
  price: 179,
  quantity: 1
});

shopCart.placeOrder();

ES6

function notifyClient(message) {
  alert(message);
}

function sendMail(customer, total) {  
  notifyClient.call(this, `TODO: Send mail, (${String(customer.id)}, ${String(total)})`);
}

function getDeals() {  
	var product = arguments[0];
  var minPrice = arguments[1];
  
  product.price = Math.round(product.price * 0.9, 2);
  
  if(product.price < minPrice) {
  	product.price = minPrice;
  }
}

class ShopCart{
  constructor() {
  	this.items = [];
    this.customer = { id: 1 };
  }

  total() {
    var total = 0;
    var item;

    for(var i=0, c=this.items.length; i<c; i++){
      item = this.items[i];
      total += item.price * item.quantity; 
    }

    return total;
  }

  addItem(product) {
    this.getDealsByProduct(product);
    this.items.push(product);
    notifyClient.call(this, 'Item added ' + product.name);
  }

  getDealsByProduct(product) {
    getDeals.apply(this, [product, product.price * 0.6]);
  }

  placeOrder() {
    sendMail.apply(this, [this.customer, this.total()]);
  }
};

var shopCart = new ShopCart();

shopCart.addItem({
  id: 1,
  name: 'Xbox Game',
  price: 179.00,
  quantity: 1
});

shopCart.placeOrder();

A variável Arguments

Outro recurso interessante é a variável arguments que se trata de um array dos argumentos, definido em toda a função JavaScript, esta é outra forma de obter os parâmetros, esta implementação foi realizada no método getDeals do exemplo acima.

Conclusão

Estes são recursos existentes no JavaScript desde a versão EcmaScript 3 (ES3). A linguagem JavaScript possui conceitos bem diferentes em relação às outras linguagens que flexibilizam o desenvolvimento de software, você pode optar por herança ou composição de objetos. Sendo assim escolha qual abordagem se enquadra em sua solução de software.