DEV Community

Filipe Roberto Beck
Filipe Roberto Beck

Posted on • Edited on

Javascript e as diferenças entre `this` léxico e dinâmico

Diferente de linguagens como C++, Java e PHP, em Javascript o identificador this pode ser usado em qualquer função e não apenas em métodos de classes. O comportamento exato depende da forma como a função é declarada e invocada. Considere o seguinte código:

var point = { x: 0, y: 0 }

function move(x, y) {
  this.x = x
  this.y = y
}

point.move = move

point.move(10, 10)
move(20, 20)

A função move está sendo declarada na forma padrão até a versão ES5, utilizando a palavra reservada function. Nesta forma de declaração, o escopo de this é dinâmico e será o objeto ao qual a função estiver atribuída no momento em que for invocada. Neste caso, this será o objeto point na instrução point.move(10, 10). Já na instrução move(20, 20), a função está sendo invocada diretamente sem ter sido atribuída a algum objeto. Como todo identificador top-level não atribuído a qualquer objeto pertence à window, então this é uma referência à window (e poderia ser invocada com window.move(20, 20) tendo o mesmo efeito).

Agora, suponha que haja uma função chamada log que imprime a data e hora atual e receba como argumento um callback que deve retornar uma string com a mensagem que será concatenada no final:

function log(callback) {
  var date = new Date().toUTCString()
  var message = callback()

  console.log(date, message)
}

Então, definimos uma função para serializar o objeto point da seguinte forma:

point.serialize = function() {
  return '(' + this.x + ',' + this.y + ')'
}

E invocamos log fornecendo uma referência à serialize:

log(point.serialize)

Isso não funciona, pois é na chamada ao callback, dentro da função serialize, em var message = callback() que será determinado quem será o this. Nesse caso, como visto antes, this será window. Para entender melhor é necessário saber que, tecnicamente, não existem métodos de classes em um nível mais baixo de execução, e o this não passa de um argumento a mais na função. O seguinte trecho:

point.move(10, 10)
move(20, 20)

Será convertido para:

move(point, 10, 10) // Point é o `this`
move(window, 10, 10) // Quando invocado diretamente

E a função move será convertida para:

function move(point, x, y) {
  point.x = x
  point.y = y
}

Como visto no exemplo anterior, estamos fornecendo apenas uma referência à função serialize (que não possui nenhuma referência à point) e dentro de log o callback é invocado diretamente. Logo, o objeto window será fornecido como primeiro argumento na forma convertida da chamada.

Para resolver o problema, podemos criar uma função que empacota a chamada à serialize:

function serializeWrapper() {
  return point.serialize()
}

E fornecer a versão empacotada:

log(serializeWrapper)

Ou usar o método bind pertencente ao protótipo de Function, o qual recebe como argumento um objeto e retorna uma cópia da função com o this vinculado ao objeto fornecido:

log(point.serialize(point))

Devemos tomar cuidado também quando formos definir funções inerentes:

point.serialize = function() {
  function innerSerialize() {
    return '(' + this.x + ',' + this.y + ')'
  }

  return innerSerialize()
}

Aqui, temos o mesmo problema visto anteriormente. A função innerSerialize está sendo invocada diretamente e this será window.

A partir da versão ES6, podemos usar as arrow functions, que mantém o escopo léxico dethis na definição da função, independentemente de como é invocada, e classes para definirmos as estruturas dos objetos. O problema da função inerente acima é resolvido se a declararmos como uma arrow function e usarmos classes:

class Point {
  serialize() {
    const innerSerialize = () => {
      return '(' + this.x + ',' + this.y + ')'
    }

    return innerSerialize()
  }
}

const p1 = new Point()
const p2 = new Point()

p1.serialize()
p2.serialize()

A diferença entre as sintaxes das declarações está no uso da flecha (=>), além de nunca poderem ser nomeadas. Quando houver apenas um argumento, os parênteses podem ser omitidos e quando o corpo da função se limitar a apenas uma expressão, as chaves e o valor de retorno também podem ser omitidos.

Dentro da definição de innerSerialize, this mantém o escopo léxico da função serialize. Nesse caso, a instância da classe Point que estiver invocando o método. Mesmo que innerSerialize seja atribuida à algum outro objeto, this permanecerá sendo o objeto point

Top comments (0)