DEV Community

Filipe Roberto Beck
Filipe Roberto Beck

Posted on • Edited on

5

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

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay