7
Author: Michael Thiessen
Translator: Front-end Xiaozhi
Source: dev

If you have dreams and dry goods, search [Moving the World] attention to this Shawanzhi who is still washing dishes in the early morning.

This article has been included in GitHub https://github.com/qq449245884/xiaozhi . There are complete test sites, materials and my series of articles for interviews with first-tier manufacturers.

Many programming languages have a non-value called null . It indicates that a variable does not currently point to an object, for example, when it has not been initialized.

In contrast, JavaScript has two such non-values: undefined and null . In this blog post, we'll examine how they differ and how best to use or avoid them.

1. undefined vs. null

The two values are very similar and are often used interchangeably. Therefore, their difference is subtle.

1.1 ECMAScript Language Specification: undefined vs. null

The ECMAScript language specification describes it as follows.

  • undefined is "used when a variable has not been assigned a value"
  • null "Indicates that no object value exists intentionally"

1.2 Two non-values - an irreparable error

Having two non-values in JavaScript is now considered a design error (even by the creator of JavaScript, Brendan Eich).

So why not just remove one of those values from JavaScript? A core principle of JavaScript is to never break backward compatibility. This principle has many benefits. Its biggest disadvantage is that design errors cannot be removed.

1.3 History of undefined and null

In Java (which inspired many aspects of JavaScript), the initialization value depends on the static type of the variable.

  • A variable with object type is initialized to null .
  • Each primitive type has its own initialization value. For example, the int variable is initialized to 0 .

In JavaScript, every variable can hold object values and primitive values. So if null means "not an object", then JavaScript also needs an initializer value, meaning "neither an object nor a primitive value". This initialization value is undefined .

occurrence of undefined

If a variable myVar has not been initialized, its value is undefined .

let myVar;
assert.equal(myVar, undefined);

If an attribute .unknownProp is missing, accessing the attribute yields the value undefined .

const obj = {};
assert.equal(obj.unknownProp, undefined);

If a function has a return statement with no arguments, the function implicitly returns undefined .

function myFunc() {
  return;
}
assert.equal(myFunc(), undefined);

If a parameter x is omitted, the language initializes the parameter to undefined .

function myFunc() {
  return;
}
assert.equal(myFunc(), undefined);
myFunc();

Optional link via obj?.someProp , returns undefined if obj is undefined or empty.

> undefined?.someProp
undefined
> null?.someProp
undefined

3. The occurrence of null

An object's prototype is either an object or null at the end of the prototype chain. Object.prototype has no prototype.

> Object.getPrototypeOf(Object.prototype)
null

If we match a regular expression (like /a/ ) with a string (like 'x' ), we either get an object with the matched data (if the match succeeds) or empty (if the match fails).

> /a/.exec('x')
null

The JSON data format does not support undefined , only null .

> JSON.stringify({a: undefined, b: null})
'{"b":null}'

4. Specially handle undefined or null operators

4.1 undefined and parameter defaults

In the following cases, the parameter default value will be used.

  • A parameter is missing.
  • The value of one parameter is undefined .

E.g:

function myFunc(arg='abc') {
  return arg;
}
assert.equal(myFunc('hello'), 'hello');
assert.equal(myFunc(), 'abc');
assert.equal(myFunc(undefined), 'abc');

undefined also triggers the default value of the parameter, which means it's a meta value.

The following example illustrates the usefulness of this.

function concat(str1='', str2='') {
  return str1 + str2;
}
function twice(str) { // (A)
  return concat(str, str);
}

In line A , we did not specify a parameter default value for str . When this parameter is missing, we forward the state to concat() to have it choose a default value.

4.2 Default values for undefined and destructuring

Defaults in destructuring work similarly to parameter defaults -- if a variable does not match in the data, or if it matches a variable of undefined , they are used.

const [a='a'] = [];
assert.equal(a, 'a');

const [b='b'] = [undefined];
assert.equal(b, 'b');

const {prop: c='c'} = {};
assert.equal(c, 'c');

const {prop: d='d'} = {prop: undefined};
assert.equal(d, 'd');

4.3 undefined and null and optional chaining

When optional linking via value?.prop

  • Returns undefined if value is undefined or null . That is, this happens whenever value.prop would throw an exception.
  • Otherwise, value.prop is returned.
function getProp(value) {
  // optional static property access
  return value?.prop;
}
assert.equal(
  getProp({prop: 123}), 123);
assert.equal(
  getProp(undefined), undefined);
assert.equal(
  getProp(null), undefined);

The following two operations have similar effects.

obj?.[«expr»] // optional dynamic property access
func?.(«arg0», «arg1») // optional function or method call

4.4 Coalescing undefined and null and nullish

The nullish merge operator ?? allows us to use the default value if a value is undefined or null :

> undefined ?? 'default value'
'default value'
> null ?? 'default value'
'default value'

> 0 ?? 'default value'
0
> 123 ?? 'default value'
123
> '' ?? 'default value'
''
> 'abc' ?? 'default value'
'abc'

The null coalescing assignment operator ??= :

function setName(obj) {
  obj.name ??= '(Unnamed)';
  return obj;
}
assert.deepEqual(
  setName({}),
  {name: '(Unnamed)'}
);
assert.deepEqual(
  setName({name: undefined}),
  {name: '(Unnamed)'}
);
assert.deepEqual(
  setName({name: null}),
  {name: '(Unnamed)'}
);
assert.deepEqual(
  setName({name: 'Jane'}),
  {name: 'Jane'}
);

5. Handling undefined and null

Let's share the most common ways to handle undefined and null in our own code.

5.1 Neither undefined nor null can be used as actual values

For example, we might want an attribute file.title to exist forever and always be a string. There are two common ways to achieve this.

Here, only undefined and null are checked, not whether a value is a string. You have to decide for yourself whether you want to implement it as an extra security measure.

5.1 Both undefined and null are forbidden

As follows

function createFile(title) {
  if (title === undefined || title === null) {
    throw new Error('`title` must not be nullish');
  }
  // ···
}

Why choose this method?

We want to undefined and null as the same thing, because JavaScript code often does this -- say.

// 检测一个属性是否存在
if (!obj.requiredProp) {
  obj.requiredProp = 123;
}

const myValue = myParameter ?? 'some default';

If something goes wrong in our code, undefined or null , we want it to fail as quickly as possible.

5.1.2 Both undefined and null trigger default values

As follows

function createFile(title) {
  title ??= '(Untitled)';
  // ···
}

We can't use parameter defaults here because it's only triggered by undefined . Instead, we rely on the nullish coalescing assignment operator ??= .

Why choose this method?

  • We want to undefined and null same (see previous section).
  • We want our code to handle undefined and null robustly and silently.

5.2 undefined or null are "off" values

For example, we might want an attribute file.title to be a string or "close" (the file has no title). There are several ways to achieve this.

5.2.1 null is a "closed" value

function createFile(title) {
  if (title === undefined) {
    throw new Error('`title` must not be undefined');
  }
  return {title};
}

Alternatively, undefined can trigger a default value.

function createFile(title = '(Untitled)') {
  return {title};
}

Why choose this method?

  • We need a non-value representing "off".
  • We don't want our non-values triggering parameter defaults and destructuring defaults.
  • We want to stringify non-values to JSON (which we can't do with undefined ).

5.2.2 undefined is the "closed" value.

function createFile(title) {
  if (title === null) {
    throw new Error('`title` must not be null');
  }
  return {title};
}

Why choose this method?

  • We need a non-value representing "off".
  • We do want our non-values to trigger parameter defaults and destruct defaults.

5.2.3 Why not use both undefined and null as "close" values?

When receiving a value, it makes sense to treat both undefined and null as "not a value". However, when we create values, we want to be unambiguous in order to keep things simple when dealing with those values.

This points to a different approach. What if we need an "off" value, but don't want to use undefined or null for such a value? Please look down.

5.3 Other ways to handle "close"

5.3.1 Special values

We can create a special value that we use whenever the property .title is turned off.

const UNTITLED = Symbol('UNTITLED');
const file = {
  title: UNTITLED,
};

5.3.2 Empty Object Pattern

The empty object pattern comes from object-oriented programming.

  • All subclasses of a common superclass have the same interface.
  • Each subclass implements a different mode of operation for an instance.
  • One of the patterns is "null".

In the example below, UntitledFile implements the "null" mode.

// 抽象的超类
class File {
  constructor(content) {
    if (new.target === File) {
      throw new Error('Can’t instantiate this class');
    }
    this.content = content;
  }
}

class TitledFile extends File {
  constructor(content, title) {
    super(content);
    this.title = title;
  }
  getTitle() {
    return this.title;
  }
}

class UntitledFile extends File {
  constructor(content) {
    super(content);
  }
  getTitle() {
    return '(Untitled)';
  }
}

const files = [
  new TitledFile('Dear diary!', 'My Diary'),
  new UntitledFile('Reminder: pick a title!'),
];

assert.deepEqual(
  files.map(f => f.getTitle()),
  [
    'My Diary',
    '(Untitled)',
  ]);

We could also use empty object mode for just the header (instead of the whole file object).

5.3.3 The Maybe type

The Maybe type is a functional programming technique.

function getTitle(file) {
  switch (file.title.kind) {
    case 'just':
      return file.title.value;
    case 'nothing':
      return '(Untitled)';
    default:
      throw new Error();
  }
}

const files = [
  {
    title: {kind: 'just', value: 'My Diary'},
    content: 'Dear diary!',
  },
  {
    title: {kind: 'nothing'},
    content: 'Reminder: pick a title!',
  },
];

assert.deepEqual(
  files.map(f => getTitle(f)),
  [
    'My Diary',
    '(Untitled)',
  ]);

We can encode "just" and "nothing" via Arrays. The benefit of our approach is that it is well supported by TypeScript (via discriminative union).

6. My method

I don't like using undefined as the value for "off" for three reasons.

  • undefined often appears unexpectedly in JavaScript.
  • ndefined triggers parameters and structured defaults (some people like undefined for the same reason).

So if I need a special value, I use one of the following two methods.

  • I use null as a "off" value. (As a bystander, this approach is relatively well supported in TypeScript). )
  • I avoided undefined and null by one of the techniques above. This has the advantage of being cleaner, the disadvantage is that it involves more work.

~ Finished, I'm Xiao Zhi, I've gone to wash the dishes, and now I can experience a little fun in life only by washing the dishes, escape~


The bugs that may exist after the code is deployed cannot be known in real time. In order to solve these bugs afterwards, a lot of time is spent on log debugging. By the way, I recommend a useful bug monitoring tool Fundebug .

Original: https://2ality.com/2021/01/undefined-null-revisited.html

communicate with

If you have dreams and dry goods, search [Moving the World] Follow this brush bowl wisdom who is still washing dishes in the early morning.

This article has been included in GitHub https://github.com/qq449245884/xiaozhi , and there are complete test sites, materials and my series of articles for interviews with first-tier manufacturers.


王大冶
68k 声望104.9k 粉丝