7 dicas para lidar com indefinido em JavaScript

A maioria das linguagens modernas como Ruby, Python ou Java tem um único valor nulo (nil ou null), que parece uma abordagem razoável.

Mas JavaScript é diferente.

null, mas também undefined, representam valores vazios em JavaScript. Então, qual é a diferença exata entre eles?

A resposta curta é que o interpretador JavaScript retorna undefined ao acessar uma variável ou propriedade de objeto que ainda não foi inicializada. Por exemplo:

Por outro lado, null representa um objeto ausente referência. JavaScript não inicializa variáveis ou propriedades de objeto com null.

Alguns métodos nativos como String.prototype.match() podem retornar null para denotar um objeto ausente. Dê uma olhada no exemplo:

Como o JavaScript é permissivo, os desenvolvedores têm a tentação de acessar valores não inicializados. Eu também sou culpado de tais práticas ruins.

Freqüentemente, essas ações arriscadas geram undefined erros relacionados:

  • TypeError: "undefined" is not a function
  • TypeError: Cannot read property "<prop-name>" of undefined
  • e erros de tipo semelhantes.

O desenvolvedor de JavaScript pode entender a ironia dessa piada :

Para reduzir esses erros, você deve entender os casos em que undefined é gerado. Vamos explorar undefined e seu efeito na segurança do código.

1. O que é indefinido

JavaScript tem 6 tipos primitivos:

E um tipo de objeto separado: {name: "Dmitri"} , .

Dos 6 tipos primitivos, undefined é um valor especial com seu próprio tipo Indefinido. De acordo com a especificação ECMAScript:

O valor primitivo de valor indefinido é usado quando uma variável não recebeu um valor.

O padrão define claramente que você receberá undefined ao acessar variáveis não inicializadas, propriedades de objetos não existentes, elementos de matriz não existentes e semelhantes.

Alguns exemplos:

O exemplo acima demonstra que acessar:

  • uma variável não inicializada number
  • uma propriedade de objeto não existente movie.year
  • ou um elemento de matriz não existente movies

são avaliados para undefined.

A especificação ECMAScript define o tipo de undefined valor:

O tipo indefinido é um tipo cujo único valor é o valor undefined.

Nesse sentido, retorna "undefined" string para um valor undefined:

Claro que typeof funciona bem para verificar se uma variável contém um valor undefined:

2. Cenários que criam indefinido

2.1 Variável não inicializada

Uma variável declarada, mas ainda não atribuída com um valor (não inicializada) é, por padrão, undefined.

Puro e simples:

myVariable é declarado e ainda não foi atribuído a um valor. O acesso à variável avalia para undefined.

Uma abordagem eficiente para resolver os problemas de variáveis não inicializadas é sempre que possível atribuir um valor inicial. Quanto menos a variável existir em um estado não inicializado, melhor.

Idealmente, você atribuiria um valor imediatamente após a declaração const myVariable = "Initial value". Mas isso nem sempre é possível.

Dica 1: dê preferência a const, caso contrário, use let, mas diga adeus a var

Na minha opinião, um dos melhores recursos do ECMAScript 2015 é a nova maneira de declarar variáveis usando const e let. É um grande passo em frente.

const e let têm escopo de bloco (ao contrário do escopo de função mais antigo var) e existem em uma zona morta temporal até a linha de declaração.

Eu recomendo a variável const quando seu valor não vai mudar. Ele cria uma ligação imutável.

Um dos recursos interessantes de const é que você deve atribuir um valor inicial à variável const myVariable = "initial". A variável não é exposta ao estado não inicializado e o acesso a undefined é impossível.

Vamos verificar a função que verifica se uma palavra é um palíndromo:

length e half são atribuídas com um valor uma vez. Parece razoável declará-los como const, uma vez que essas variáveis não serão alteradas.

Use a declaração let para variáveis cujo valor pode mudar. Sempre que possível, atribua um valor inicial imediatamente, por exemplo, let index = 0.

E a velha escola var? Minha sugestão é parar de usá-lo.

var problema de declaração é o levantamento de variável dentro do escopo da função. Você pode declarar uma var variável em algum lugar no final do escopo da função, mas ainda assim, você pode acessá-la antes da declaração: e você obterá um undefined.

myVariable é acessível e contém undefined mesmo antes da linha de declaração: var myVariable = "Initial value".

Ao contrário, uma variável const ou let não pode ser acessada antes da linha de declaração – a variável está em uma zona morta temporal antes da declaração. E isso é bom porque você tem menos chance de acessar um undefined.

O exemplo acima atualizado com let (ao invés de var) lança um ReferenceError porque a variável na zona morta temporal não está acessível.

Incentivar o uso de const para ligações imutáveis ou let de outra forma garante uma prática que reduz a aparência de pessoas não inicializadas variável.

Dica 2: Aumente a coesão

A coesão caracteriza o grau em que os elementos de um módulo (namespace, classe, método, bloco de código) pertencem um ao outro. A coesão pode ser alta ou baixa.

Um módulo de alta coesão é preferível porque os elementos desse módulo se concentram apenas em uma única tarefa. Isso torna o módulo:

  • Focado e compreensível: mais fácil de entender o que o módulo faz
  • Mantível e mais fácil de refatorar: a mudança no módulo afeta menos módulos
  • Reutilizável: estar focado em uma única tarefa, torna o módulo mais fácil de reutilizar
  • Testável: você testaria mais facilmente um módulo que está focado em uma única tarefa

Alta coesão acompanhada de acoplamento fraco é a característica de um sistema bem projetado.

Um bloco de código pode ser considerado um pequeno módulo. Para lucrar com os benefícios da alta coesão, mantenha as variáveis o mais próximo possível do bloco de código que as utiliza.

Por exemplo, se uma variável existe apenas para formar a lógica do escopo do bloco, então declare e torne a variável viva apenas dentro desse bloco (usando const ou let declarações). Não exponha essa variável ao escopo do bloco externo, pois o bloco externo não deve se preocupar com essa variável.

Um exemplo clássico da vida desnecessariamente estendida de variáveis é o uso de for ciclo dentro de uma função:

index, item e length as variáveis são declaradas no início do corpo da função. No entanto, eles são usados apenas perto do fim. Qual é o problema com esta abordagem?

Entre a declaração no topo e o uso na for instrução, as variáveis index, item não foram inicializados e foram expostos a undefined. Eles têm um ciclo de vida excessivamente longo em todo o escopo da função.

Uma abordagem melhor é mover essas variáveis o mais próximo possível de seu local de uso:

index e item as variáveis existem apenas no escopo do bloco da instrução for. Eles não têm nenhum significado fora de for.
length a variável também é declarada perto da fonte de seu uso.

Por que a versão modificada é melhor do que a inicial? Vejamos:

  • As variáveis não são expostas ao estado não inicializado, portanto, você não tem risco de acessar undefined
  • Movendo o variáveis tão próximas quanto possível de seu local de uso aumenta a legibilidade do código
  • Pedaços de código altamente coesos são mais fáceis de refatorar e extrair em funções separadas, se necessário

2.2 Acessando uma propriedade não existente

Ao acessar uma propriedade de objeto não existente, JavaScript retorna undefined.

Vamos demonstrar isso em um exemplo:

favoriteMovie é um objeto com uma única propriedade title. Acessar uma propriedade não existente actors usando um acessador de propriedade favoriteMovie.actors avalia como undefined.

Acessar uma propriedade não existente não gera um erro. O problema aparece ao tentar obter dados de uma propriedade não existente, que é a armadilha undefined mais comum, refletida na conhecida mensagem de erro TypeError: Cannot read property <prop> of undefined.

Vamos modificar ligeiramente o fragmento de código anterior para ilustrar um TypeError lançamento:

favoriteMovie não tem a propriedade actors, então favoriteMovie.actors avalia para undefined.

Como resultado, acessar o primeiro item de um valor undefined usando a expressão favoriteMovie.actors lança um TypeError.

A natureza permissiva do JavaScript que permite acessar propriedades não existentes é uma fonte de não determinismo: a propriedade pode ser definida ou não. A boa maneira de contornar esse problema é restringir o objeto para que sempre tenha definido as propriedades que contém.

Infelizmente, muitas vezes você não tem controle sobre os objetos. Esses objetos podem ter um conjunto diferente de propriedades em diversos cenários. Portanto, você deve lidar com todos esses cenários manualmente.

Vamos implementar uma função append(array, toAppend) que adiciona no início e / ou no final de uma matriz de novos elementos. O parâmetro toAppend aceita um objeto com propriedades:

  • first: elemento inserido no início de array
  • last: elemento inserido no final de array.

A função retorna uma nova instância de array, sem alterar o array original.

A primeira versão de append(), um pouco ingênua, pode ter a seguinte aparência:

Porque toAppend o objeto pode omitir as propriedades first ou last, é obrigatório verificar se essas propriedades existem em toAppend.

Um acessador de propriedade avalia como undefined se a propriedade não existe. A primeira tentação de verificar se as propriedades first ou last estão presentes é verificá-las em undefined. Isso é executado em condicionais if(toAppend.first){} e if(toAppend.last){}

Não tão rápido. Essa abordagem tem uma desvantagem. undefined, bem como false, null, 0, NaN e "" são valores falsos.

Na implementação atual de append(), a função não permite inserir elementos falsos:

As dicas a seguir explicam como verificar corretamente a existência da propriedade.

Dica 3: verifique a existência da propriedade

Felizmente, o JavaScript oferece várias maneiras de determinar se o objeto tem uma propriedade específica:

  • obj.prop !== undefined: compare com undefined diretamente
  • typeof obj.prop !== "undefined": verifique o tipo de valor da propriedade
  • obj.hasOwnProperty("prop"): verifique se o o objeto tem uma propriedade própria
  • "prop" in obj: verifique se o objeto tem uma propriedade própria ou herdada

Minha recomendação é para use o operador in. Tem uma sintaxe curta e agradável. A presença do operador in sugere uma intenção clara de verificar se um objeto tem uma propriedade específica, sem acessar o valor real da propriedade.

obj.hasOwnProperty("prop") também é uma boa solução. É um pouco mais longo que o operador in e verifica apenas nas próprias propriedades do objeto.

Vamos melhorar a função append(array, toAppend) usando o operador in:

"first" in toAppend (e "last" in toAppend) é true se a propriedade correspondente existe, false caso contrário.

in O operador corrige o problema com a inserção de elementos falsos 0 e false. Agora, adicionar esses elementos no início e no final de produz o resultado esperado .

Dica 4: Destruição para acessar as propriedades do objeto

Ao acessar uma propriedade do objeto, às vezes é necessário definir um valor padrão se a propriedade não existir.

Você pode usar in acompanhado do operador ternário para fazer isso:

A sintaxe do operador ternário torna-se assustadora quando o número de propriedades a verificar aumenta. Para cada propriedade, você deve criar uma nova linha de código para lidar com os padrões, aumentando uma parede feia de operadores ternários de aparência semelhante.

Para usar uma abordagem mais elegante, vamos nos familiarizar com um ótimo recurso do ES2015 chamado desestruturação de objetos.

A desestruturação do objeto permite a extração embutida dos valores das propriedades do objeto diretamente nas variáveis e definir um valor padrão se a propriedade não existir. Uma sintaxe conveniente para evitar lidar diretamente com undefined.

Na verdade, a extração de propriedade agora é precisa:

Para ver as coisas em ação, vamos definir uma função útil que envolve uma string entre aspas.

quote(subject, config) aceita o primeiro argumento como a string a ser quebrada. O segundo argumento config é um objeto com as propriedades:

Aplicando os benefícios da desestruturação do objeto, vamos implementar quote():

const { char = """, skipIfQuoted = true } = config atribuição de desestruturação em uma linha extrai as propriedades char e skipIfQuoted do objeto config.
Se algumas propriedades estiverem faltando no objeto config, a atribuição de desestruturação define os valores padrão : """ para char e false para skipIfQuoted.

Felizmente, a função ainda pode ser aprimorada.

Vamos mover a atribuição de desestruturação para a seção de parâmetros. E defina um valor padrão (um objeto vazio { }) para o parâmetro config, para pular o segundo argumento quando as configurações padrão forem suficientes.

A atribuição de desestruturação substitui o parâmetro config na assinatura da função. Eu gosto disso: quote() fica uma linha a menos.

= {} no lado direito da atribuição de desestruturação garante que um objeto vazio seja usado se o segundo argumento não for especificado de todo quote("Sunny day").

A desestruturação de objetos é um recurso poderoso que lida com eficiência na extração de propriedades de objetos. Gosto da possibilidade de especificar um valor padrão a ser retornado quando a propriedade acessada não existe. Como resultado, você evita undefined e as complicações em torno disso.

Dica 5: Preencha o objeto com as propriedades padrão

Se não houver necessidade de criar variáveis para cada propriedade, como faz a atribuição de desestruturação, o objeto que faltar algumas propriedades pode ser preenchido com valores padrão.

O ES2015 Object.assign(target, source1, source2, ...) copia os valores de todas as propriedades próprias enumeráveis de um ou mais objetos de origem para o objeto de destino. A função retorna o objeto de destino.

Por exemplo, você precisa acessar as propriedades do objeto unsafeOptions que nem sempre contém seu conjunto completo de propriedades.

Para evitar undefined ao acessar uma propriedade não existente de unsafeOptions, vamos fazer alguns ajustes:

  • Defina um objeto defaults que contém os valores de propriedade padrão
  • Chame Object.assign({ }, defaults, unsafeOptions) para construir um novo objeto options. O novo objeto recebe todas as propriedades de unsafeOptions, mas as que faltam são obtidas de defaults.

unsafeOptions contém apenas a propriedade fontSize. O objeto defaults define os valores padrão para as propriedades fontSize e color.

Object.assign() recebe o primeiro argumento como um objeto de destino {}. O objeto de destino recebe o valor da propriedade fontSize do objeto de origem unsafeOptions. E o valor da propriedade color do objeto de origem defaults, porque unsafeOptions não contém color.

A ordem em que os objetos de origem são enumerados importa: as propriedades posteriores do objeto de origem substituem as anteriores.

Agora você está seguro para acessar qualquer propriedade do objeto options, incluindo options.color que não estava disponível em unsafeOptions inicialmente.

Felizmente, existe uma alternativa mais fácil para preencher o objeto com as propriedades padrão.Eu recomendo usar as propriedades de propagação em inicializadores de objeto.

Em vez da invocação Object.assign(), use a sintaxe de propagação do objeto para copiar para o objeto de destino todas as propriedades próprias e enumeráveis dos objetos de origem:

O o inicializador de objetos espalha propriedades de objetos de origem defaults e unsafeOptions. A ordem em que os objetos de origem são especificados é importante: as propriedades posteriores do objeto de origem sobrescrevem as anteriores.

Preencher um objeto incompleto com valores de propriedade padrão é uma estratégia eficiente para tornar seu código seguro e durável. Não importa a situação, o objeto sempre contém o conjunto completo de propriedades: e undefined não pode ser gerado.

Dica bônus: coalescência nula

O operador coalescência nula avalia para um valor padrão quando seu operando é undefined ou null:

O operador de coalescência nula é conveniente para acessar uma propriedade de objeto, embora tenha um valor padrão quando esta propriedade é undefined ou null:

styles o objeto não tem a propriedade color, portanto styles.color o acessador de propriedade é undefined. styles.color ?? "black" avalia o valor padrão "black".

styles.fontSize é 18, portanto, o operador de coalescência nula avalia o valor da propriedade 18.

2.3 Parâmetros da função

Os parâmetros da função implicitamente assumem o padrão undefined.

Normalmente, uma função definida com um número específico de parâmetros deve ser chamada com o mesmo número de argumentos. É quando os parâmetros obtêm os valores que você espera:

Quando multiply(5, 3), os parâmetros a e b recebem 5 e respectivamente 3 valores. A multiplicação é calculada conforme o esperado: 5 * 3 = 15.

O que acontece quando você omite um argumento na chamada? O parâmetro correspondente dentro da função torna-se undefined.

Vamos modificar levemente o exemplo anterior chamando a função com apenas um argumento:

A invocação multiply(5) é realizado com um único argumento: como resultado a o parâmetro é 5, mas o b o parâmetro é undefined.

Dica 6: use o valor do parâmetro padrão

Às vezes, uma função não requer o conjunto completo de argumentos na chamada. Você pode definir padrões para parâmetros que não têm um valor.

Recordando o exemplo anterior, vamos fazer uma melhoria. Se o parâmetro b for undefined, deixe como padrão 2:

A função é chamada com um único argumento multiply(5). Inicialmente, o parâmetro a é 2 e b é undefined.
A declaração condicional verifica se b é undefined. Se isso acontecer, a atribuição b = 2 define um valor padrão.

Embora a forma fornecida para atribuir valores padrão funcione, eu não recomendo comparar diretamente com undefined. É prolixo e parece um hack.

Uma abordagem melhor é usar o recurso de parâmetros padrão do ES2015. É curto, expressivo e sem comparações diretas com undefined.

Adicionar um valor padrão ao parâmetro b = 2 parece melhor:

b = 2 na assinatura da função garante que se b for undefined, o parâmetro padrão é 2.

O recurso de parâmetros padrão do ES2015 é intuitivo e expressivo. Sempre use-o para definir valores padrão para parâmetros opcionais.

2.4 Valor de retorno da função

Implicitamente, sem a instrução return, uma função JavaScript retorna undefined.

Uma função que não tem return instrução retorna implicitamente undefined:

square() função não retorna nenhum resultado de cálculo. O resultado da chamada da função é undefined.

A mesma situação acontece quando a instrução return está presente, mas sem uma expressão próxima:

return; instrução é executada, mas não retorna nenhuma expressão. O resultado da invocação também é undefined.

Claro, indicar próximo a return a expressão a ser retornada funciona conforme o esperado:

Agora a invocação da função é avaliada como 4, que é 2 ao quadrado.

Dica 7: não confie na inserção automática de ponto-e-vírgula

A seguinte lista de instruções em JavaScript deve terminar com ponto-e-vírgula (;) :

  • declaração vazia
  • let, const, var, import, export declarações
  • declaração de expressão
  • debugger declaração
  • continue declaração, break declaração
  • throw declaração
  • return declaração

Se você usar uma das afirmações acima, certifique-se de indicar um ponto e vírgula no final:

No final de ambos let declaração e return declaração um ponto e vírgula obrigatório é escrito.

O que acontece quando você não quer indicá-los ponto e vírgula? Em tal situação, o ECMAScript fornece um mecanismo de inserção automática de ponto e vírgula (ASI), que insere para você os pontos e vírgulas ausentes.

Com a ajuda do ASI, você pode remover os pontos e vírgulas do exemplo anterior:

O texto acima é um código JavaScript válido. Os pontos-e-vírgulas ausentes são inseridos automaticamente para você.

À primeira vista, parece bastante promissor. O mecanismo ASI permite que você ignore os pontos-e-vírgulas desnecessários. Você pode tornar o código JavaScript menor e mais fácil de ler.

Existe uma pequena, mas irritante armadilha criada pelo ASI. Quando uma nova linha fica entre return e a expressão retornada return \n expression, o ASI insere automaticamente um ponto e vírgula antes da nova linha return; \n expression.

O que significa dentro de uma função ter uma instrução return;? A função retorna undefined. Se você não conhece em detalhes o mecanismo do ASI, o undefined retornado inesperadamente é enganoso.

Por exemplo, vamos estudar o valor retornado de getPrimeNumbers() invocação:

Entre a instrução return e a expressão literal da matriz existe uma nova linha. JavaScript insere automaticamente um ponto e vírgula após return, interpretando o código da seguinte maneira:

A instrução return; faz com que a função getPrimeNumbers() retorne undefined em vez da matriz esperada.

O problema é resolvido removendo a nova linha entre return e literal de array:

Minha recomendação é estudar como exatamente a inserção automática de ponto-e-vírgula funciona para evitar tais situações.

Claro, nunca coloque uma nova linha entre return e a expressão retornada.

2,5 operador vazio

void <expression> avalia a expressão e retorna undefined independentemente do resultado da avaliação.

Um caso de uso do operador void é suprimir a avaliação da expressão para undefined, contando com algum efeito colateral da avaliação.

3. undefined in arrays

Você obtém undefined ao acessar um elemento de array com um índice fora dos limites.

colors array tem 3 elementos, portanto, os índices válidos são 0, 1 e 2.

Como não há elementos da matriz nos índices 5 e -1, os acessadores colors e colors são undefined.

Em JavaScript, você pode encontrar os chamados sparse arrays. Esses são arrays que têm lacunas, ou seja, em alguns índices, nenhum elemento é definido.

Quando um gap (também conhecido como slot vazio) é acessado dentro de um array esparso, você também obtém um undefined.

O exemplo a seguir gera matrizes esparsas e tenta acessar seus slots vazios:

sparse1 é criado invocando um Array construtor com um primeiro argumento numérico.Possui 3 slots vazios.

sparse2 é criado com uma matriz literal com o segundo elemento ausente.

Em qualquer um desses arrays esparsos, o acesso a um slot vazio é avaliado como undefined.

Ao trabalhar com arrays, para evitar undefined, certifique-se de usar índices de array válidos e evitar a criação de arrays esparsos.

4. Diferença entre undefined e null

Qual é a principal diferença entre undefined e null? Ambos os valores especiais implicam em um estado vazio.

undefined representa o valor de uma variável que ainda não foi inicializada, enquanto null representa uma ausência intencional de um objeto.

Vamos explorar a diferença em alguns exemplos.

A variável number está definida , no entanto, não é atribuído com um valor inicial:

number variável é undefined, que indica uma variável não inicializada.

O mesmo conceito não inicializado acontece quando uma propriedade de objeto não existente é acessada:

Porque lastName propriedade não existe em obj, JavaScript avalia obj.lastName para undefined.

Por outro lado, você sabe que uma variável espera um objeto. Mas por algum motivo, você não pode instanciar o objeto. Nesse caso, null é um indicador significativo de um objeto ausente.

Por exemplo, clone() é uma função que clona um objeto JavaScript simples. Espera-se que a função retorne um objeto:

No entanto, clone() pode ser chamado com um argumento não objeto: 15 ou null. Nesse caso, a função não pode criar um clone, então ela retorna null – o indicador de um objeto ausente.

typeof operador faz a distinção entre undefined e null:

Além disso, o operador de qualidade estrita === diferencia de null:

5. Conclusão

undefined a existência é uma consequência da natureza permissiva do JavaScript que permite o uso de:

  • variáveis não inicializadas
  • propriedades ou métodos de objeto não existentes
  • índices fora dos limites para acessar elementos de matriz
  • o resultado da invocação de uma função que não retorna nada

Comparar diretamente com undefined não é seguro porque você confia em uma prática permitida, mas desencorajada, mencionada acima.

Uma estratégia eficiente é reduzir pelo menos o aparecimento de undefined palavra-chave em seu código, aplicando bons hábitos, como:

  • reduzir o uso de variáveis não inicializadas
  • tornar o ciclo de vida das variáveis curto e próximo à fonte de seu uso
  • sempre que possível atribuir valores iniciais às variáveis
  • favorecer const, caso contrário, use let
  • use valores padrão para parâmetros de função insignificantes
  • verifique as propriedades existência ou preencher os objetos não seguros com propriedades padrão
  • evitar o uso de matrizes esparsas

É bom que o JavaScript tenha ambos undefined e null para representar valores vazios?

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *