Что, если мы перестанем использовать null?

В настоящее время API-интерфейсы DOM и браузера, похоже, используют null в некоторых случаях и undefined в некоторых других. Хотя у них были свои исторические причины, результатом этого несоответствия является то, что нам приходится полагаться на приведение типов чаще, чем следовало бы, что создает целый ряд проблем.

Однако я хотел бы обсудить, что мы получим, если перестанем использовать null и будем придерживаться undefined в пользовательском коде.

1. Семантика - меньше о чем думать

Неужели такая большая разница в семантике? Для меня и null, и undefined означают примерно одно и то же - отсутствие значения. Рассмотрим, например, этот код:

const map = new Map();
map.get('abc'); // undefined
localStorage.get('abc'); // null

В одном случае мы получаем undefined, в другом случае мы получаем null, но какая разница на самом деле для потребительского кода?

2. Global 'undefined' is safe now

Ранее в ECMA с 1 по 5.1 в спецификации не было глобального свойства undefined, были только Неопределенный тип и значение. Итак, чтобы безопасно использовать undefined, вам нужно было использовать переменную со значением undefined в локальной области, иначе последствия могли быть непредсказуемыми:

window.undefined = '¯\_(ツ)_/¯';
var a;
a === undefined; // false

Начиная с ECMA 6.0 / 2015 глобальный undefined доступен только для чтения, поэтому его можно безопасно использовать.

3. Явное намерение

Если вам действительно нужно проверить, определено ли свойство объекта, существует оператор in, который по какой-то причине используется редко, но на самом деле существует именно для этой цели, чтобы вы могли отличить отсутствующее свойство от свойства с неопределенным значением. .

const obj = {a: undefined};
'a' in obj; // true
'b' in obj; // false

Обратите внимание, что оператор in будет искать в цепочке прототипов, если собственное свойство не определено. Если вы хотите проверить только собственное свойство, используйте метод hasOwnProperty.

4. Оператор typeof работает интуитивно понятно.

Не секрет, что typeof null в JavaScript возвращает «объект» в лучшую или худшую сторону. Какие бы доводы ни стояли за этим в первые дни существования языка, с точки зрения потребителя, это не имеет особого смысла, поскольку null - это «пустое» значение, и обычно вы хотите отличить его от других типов значений. На самом деле, чтобы правильно проверить тип, мы проверяем и typeof, и !== null.

В случае undefined, typeof undefined возвращает «undefined», что дает нам хороший последовательный и интуитивно понятный тип, когда мы проверяем пустоту.

5. А как насчет JSON.stringify ()?

Когда мы сериализуем данные, JSON.stringify() удаляет все свойства с неопределенными значениями, что хорошо, потому что нам не нужно отправлять их по сети и другим местам.

JSON.stringify({b: undefined, c: null}); // {"c": null}

Однако, если вы хотите сохранить неопределенные свойства такими, какие они есть, это очень просто сделать:

JSON.stringify(
  {b: undefined, c: null}, 
  (key, value) => value === undefined ? 'undefined' : value 
); // {b: "undefined", "c": null}

То же самое и с JSON.parse().

6. Параметры функции по умолчанию

Начиная с ECMAScript 2015, в язык были добавлены параметры функций по умолчанию. Если вы не передадите параметр или передадите undefined - будет использовано значение по умолчанию.

const f = (a = 1) => a;
f(); // 1
f(undefined); // 1
f(null); // null

Здесь мы должны заметить, что язык действительно хочет, чтобы мы использовали undefined для пустых значений, а не null.

7. Деструктуризация со значениями по умолчанию.

Значения по умолчанию в синтаксисе деструктурирования присваивания, также представленном в ES2015, работают таким же образом.

const {a = 1} = {a: null};
a; // null
const {a = 1} = {};
a; // 1
const {a = 1} = {a: undefined};
a; // 1
const [a = 1] = [];
a; // 1
const [a = 1] = [undefined];
a; // 1
const {a: {b = 1}} = {a: {b: undefined}};
b; // 1

Значения по умолчанию теперь могут быть еще более полезными, поскольку их можно использовать не только в параметрах функции, но также в массивах, объектах и ​​даже вложенных объектах и ​​массивах.

8. Печатные языки и аннотации.

С появлением типизированных аннотаций для JavaScript нам также необходимо сделать так, чтобы система типов знала о типах null и undefined.

Например, в Flow есть специальная нотация для уже существующих значений, которая называется Maybe Types.

// @flow
const acceptsMaybeNumber = (value: ?number) => {
  // ...
}

acceptsMaybeNumber(42);        // Works!
acceptsMaybeNumber();          // Works!
acceptsMaybeNumber(undefined); // Works!
acceptsMaybeNumber(null);      // Works!
acceptsMaybeNumber("42");      // Error!

Теперь вам нужно написать код, который проверяет оба: значение не равно undefined, а значение не равно null.

В то же время существует необязательный синтаксис значения, который работает, когда значение undefined или вообще не было передано значения, поэтому в вашем коде вам нужно только проверить undefined.

// @flow
const acceptsMaybeNumber = (value?: number) => {
  if (value !== undefined) {
    return value * 2;
  }
}

acceptsMaybeNumber(42);        // Works!
acceptsMaybeNumber();          // Works!
acceptsMaybeNumber(undefined); // Works!
acceptsMaybeNumber(null);      // Error!
acceptsMaybeNumber("42");      // Error!

Это становится довольно сложным, если мы хотим разрешить свойство, которое может иметь значение null или undefined или вообще не определяться.

type A = {p: ?number};
const a: A = {}; // Error!

Итак, чтобы поддерживать все 3 состояния, нам нужно описать необязательное свойство с помощью типа Maybe:

type A = {p?: ?number};
const a: A = {}; // Works!

Как видите, ваша кодовая база была бы намного чище, если бы вы просто придерживались undefined, поскольку некоторые языковые функции не работают с null, а их смешивание повсюду приводит к дополнительным проверкам.

Такое ощущение, что язык JavaScript хочет, чтобы мы использовали undefined полностью.