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 dearray
-
last
: elemento inserido no final dearray
.
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 comundefined
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 objetooptions
. O novo objeto recebe todas as propriedades deunsafeOptions
, mas as que faltam são obtidas dedefaults
.
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 retornaundefined
.
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, enquantonull
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, uselet
- 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?