JavaScript спецификация: темные стороны

JavaScript спецификация называется ECMAScript. Язык популярен и обладает низким порогом входа. Но есть моменты, которые требуют отдельных объяснений.

JavaScript спецификация

Мы не будем заострять внимание на востребованности JS. Чтобы понять, о чем пойдет речь, предлагаем ознакомиться со статьями «JavaScript и то, что вы о нем не знали», а также «Перлы языка JavaScript».

Добавление RegExps

Знаете ли вы, что можете добавлять такие цифры?

// устанавливаем метод toString

RegExp.prototype.toString = function () {

   Return this.source

}

/ 7 / - / 5 / // -> 2

Объяснение:

Функции вызова

Давайте объявим функцию, которая выводит все параметры в консоль:

function f(...args) {
  return args
}

Несомненно, вы знаете, что эту функцию можно вызвать следующим образом:

f(1, 2, 3) // -> [ 1, 2, 3 ]

Но знаете ли вы, что можете вызвать любую функцию вот так?

f`true is ${true}, false is ${false}, array is ${[1,2,3]}`
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// ->   true,
// ->   false,
// ->   [ 1, 2, 3 ] ]

Объяснение:

Это вовсе не волшебство. В приведенном выше примере функция f является тегом для шаблонного литерала. Первый аргумент содержит массив строковых значений. Остальные аргументы связаны с выражениями. Пример:

function template(strings, ...keys) {
  // делаем что-то со строками и ключами…
}

Это волшебство знаменитой библиотеки под названием styled-components, столь популярной в сообществе React.

Ссылка на спецификацию:

Вызов вызов вызов

Найдено у @cramforce.

console.log.call.call.call.call.call.apply(a => a, [1, 2])

Объяснение:

Внимание, это может сломать вам голову! Попробуйте мысленно воспроизвести этот код: мы применяем метод call с помощью метода apply. Узнайте больше:

JavaScript спецификация: свойство конструктора

const c = 'constructor'
c[c][c]('console.log("WTF?")')() // > WTF?

Объяснение:

Давайте рассмотрим этот пример шаг за шагом:

// Объявляем новую константу, которая является строкой 'constructor'
const c = 'constructor'

// c – строка
c // -> 'constructor'

// Получение конструктора строки
c[c] // -> [Function: String]

// Получение конструктора конструктора
c[c][c] // -> [Function: Function]

// Вызов конструктора Function и передача тела новой функции в качестве аргумента
c[c][c]('console.log("WTF?")') // -> [Function: anonymous]

// И затем вызываем эту анонимную функцию
// Результатом является консольная запись строки WTF?
c[c][c]('console.log("WTF?")')() // > WTF?

Object.prototype.constructor возвращает ссылку на функцию-конструктор объекта, которая создала объект экземпляра. В случае со строками  – это строка, в случае с числами – это число и т. д.

Объект как ключ к свойствам объекта

{ [{}]: {} } // -> { '[object Object]': {} }

Объяснение:

Почему это так работает? Здесь мы используем вычисленное имя объекта. Когда вы передаете объект между этими скобками, получаете ключ свойства '[object Object]' и значение {}.

Мы можем сделать следующим образом:

({[{}]:{[{}]:{}}})[{}][{}] // -> {}

// structure:
// {
//   '[object Object]': {
//     '[object Object]': {}
//   }
// }

Подробнее об объектных читайте здесь:

Доступ к прототипам с помощью __proto__

Порой JavaScript элементы поражают. Как мы знаем, примитивы не имеют прототипов. Но если мы попытаемся получить значение примитивов __proto__, мы получим следующее:

(1).__proto__.__proto__.__proto__ // -> null

Объяснение:

Это происходит потому, что, когда что-то не имеет прототипа, оно будет завернуто в оболочку с использованием метода ToObject. Итак, шаг за шагом:

(1).__proto__ // -> [Number: 0]
(1).__proto__.__proto__ // -> {}
(1).__proto__.__proto__.__proto__ // -> null

Вот более подробная информация о __proto__:

`${{Object}}`

Каков результат приведенного ниже выражения?

`${{Object}}`

Ответ:

// -> '[object Object]'

Объяснение:

Мы определили объект со свойством Object, используя Shorthand property notation:

{ Object: Object }

Затем мы передали этот объект в шаблонный литерал, так что метод toString вызывает данный объект. Вот почему мы получаем строку '[object Object]'.

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

Рассмотрим этот пример:

let x, {x: y = 1} = {x}; у;

Отличная задача для собеседования. Какое значение y? Ответ:

// -> 1

Объяснение:

let x, {x: y = 1} = {x}; у;

// ↑ ↑ ↑ ↑

// 1 3 2 4

В приведенном выше примере:

  1. Мы объявляем x без значения, поэтому он не определен.
  2. Затем мы упаковываем значение x в свойство объекта x.
  3. Затем мы извлекаем значение x, используя деструктурирование, и хотим назначить его y. Если значение не определено, мы будем использовать 1 в качестве значения по умолчанию.
  4. Возвращаем значение y.

Такие JavaScript элементы, как точки и их расширение

Интересные примеры могут быть составлены с расширением массивов. Учтем это:

[...[...'...']].length // -> 3

Объяснение:

Почему 3? Когда мы используем оператор расширения, вызывается метод @@iterator, и возвращаемый итератор используется для получения значений, которые нужно повторить. По умолчанию итератор для строки расширяет строку символами. После расширения мы собираем эти символы в массив. Затем мы снова расширяем данный массив и снова упаковываем его в массив.

Строка '...' состоит из трех точек (символов), поэтому длина результирующего массива равна трем.

Теперь, шаг за шагом:

[...'...']             // -> [ '.', '.', '.' ]

[...[...'...']]        // -> [ '.', '.', '.' ]

[...[...'...']].length // -> 3

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

[...'...']                 // -> [ '.', '.', '.' ]

[...[...'...']]            // -> [ '.', '.', '.' ]

[...[...[...'...']]]       // -> [ '.', '.', '.' ]

[...[...[...[...'...']]]]  // -> [ '.', '.', '.' ]

// и так далее …

Labels

Не так много программистов знают о метках в JavaScript. Они интересны:

foo: {

  console.log('first');

  break foo;

  console.log('second');

}



// > first

// -> undefined

Объяснение:

Обозначенная инструкция используется с операторами break или continue. Вы можете применить метку для идентификации цикла, а затем использовать break или continue, чтобы указать, должна ли программа прерывать цикл или продолжать его выполнение.

В приведенном выше примере мы идентифицируем метку foo. После этого выполняется console.log ('first'); , а затем мы прерываем выполнение.

JavaScript элементы «метки»:

Вложенные ярлыки

a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5

Объяснение:

Как и в предыдущих примерах, следуйте по этим ссылкам:

Коварный try..catch

Что именно должно вернуться, исходя из приведенного ниже примера? 2 или 3?

(() => {

  try {

    return 2;

  } finally {

    return 3;

  }

})()

Ответ 3. Удивлены?

Объяснение:

Относится ли это к множественному наследованию?

Некоторые JavaScript элементы заставляют серьезно поразмыслить. Взгляните на представленный код:

new (class F extends (String, Array) { }) // -> F []

Является ли это множественным наследованием? Нет.

Объяснение:

Значение extends ((String, Array)) представляет собой любопытную часть кода. Оператор группировки всегда возвращает свой последний аргумент, поэтому (String, Array) – это на самом деле просто Array. Значит, мы создали класс, который просто наследует массив.

Генератор

Рассмотрим пример генератора:

(function* f() { yield f })().next()

// -> { value: [GeneratorFunction: f], done: false }

Как вы можете видеть, возвращаемое значение – это объект со значением, равным f. В данном случае мы можем сделать что-то вроде этого:

(function* f() { yield f })().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }

// и снова
(function* f() { yield f })().next().value().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }

// и еще раз
(function* f() { yield f })().next().value().next().value().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }

// и так далее
// ...

Объяснение:

Предлагаем к прочтению такие разделы спецификации:

Класс класса

И снова JavaScript элементы ведут себя вне законов логики. Рассмотрим запутанный синтаксис:

(typeof (new (class { class () {} }))) // -> 'object'

Кажется, мы объявляем класс внутри класса. Должны быть и ошибки, однако, мы получаем строку 'object'.

Объяснение:

Начиная с эпохи ECMAScript 5, ключевые слова допускаются как имена свойств. Поэтому подумайте об этом простом примере:

const foo = {

  class: function() {}

};

И ES6 стандартизовал сокращенные определения методов. Кроме того, классы могут быть анонимными. Поэтому, если мы отбросим : function, то получим:

class {

  class() {}

}

Результат класса по умолчанию всегда является простым объектом. И его typeof должно возвращать 'object'.

Подробнее здесь:

Non-coercible objects

С известными символами есть способ избавиться от принуждения типа JS. Взгляните:

function nonCoercible(val) {
  if (val == null) {
    throw TypeError('nonCoercible should not be called with null or undefined')
  }

  const res = Object(val)

  res[Symbol.toPrimitive] = () => {
    throw TypeError('Trying to coerce non-coercible object')
  }

  return res
}

Теперь мы можем использовать это следующим образом:

// JavaScript элементы
// объекты
const foo = nonCoercible({foo: 'foo'})

foo * 10      // -> TypeError: Trying to coerce non-coercible object
foo + 'evil'  // -> TypeError: Trying to coerce non-coercible object

// строки
const bar = nonCoercible('bar')

bar + '1'                 // -> TypeError: Trying to coerce non-coercible object
bar.toString() + 1        // -> bar1
bar === 'bar'             // -> false
bar.toString() === 'bar'  // -> true
bar == 'bar'              // -> TypeError: Trying to coerce non-coercible object

// числа
const baz = nonCoercible(1)

baz == 1             // -> TypeError: Trying to coerce non-coercible object
baz === 1            // -> false
baz.valueOf() === 1  // -> true

Запутанные стрелочные функции

Рассмотрим приведенный ниже пример:

let f = () => 10

f() // -> 10

Хорошо, хорошо, но что насчет этого?

let f = () => {}

f() // -> undefined

Объяснение:

Вы можете ожидать {} вместо undefined. Это связано с тем, что фигурные скобки являются частью синтаксиса стрелочных функций, поэтому f вернется undefined.

Трудное возвращение

С return тоже не все так чисто. Учтите данный нюанс:

(function () {

  return

  {
    b : 10
  }

})() // -> undefined

Объяснение:

Дело в том, что return и возвращаемое выражение должны находиться в одной строке:

(function () {

  return {
    b : 10
  }

})() // -> { b: 10 }

Доступ к свойствам объекта с использованием массивов

Такие JavaScript элементы как массивы заслуживают отдельного раздела «необъяснимо, но факт». Тем не менее, затронем мы их здесь:

var obj = { property: 1 }

var array = ['property']


obj[array] // -> 1

Как насчет псевдо-многомерных массивов?

var map = {}

var x = 1

var y = 2

var z = 3



map[[x, y, z]] = true

map[[x + 10, y, z]] = true



map["1,2,3"]  // -> true

map["11,2,3"] // -> true

Объяснение:

Оператор скобок [] преобразует выражение, переданное toString. Преобразование одноэлементного массива в строку – это такое же преобразование элемента в строку:

['property'].toString() // -> 'property'

Прочие источники

  1. wtfjs.com – коллекция тех самых специфических нарушений, несоответствий и просто мучительно нелогичных моментов.
  2. Wat – молниеносная речь Гэри Бернхардта, CodeMash 2012
  3. What the... JavaScript? – Кайл Симпсонс рассказывает о вытаскивании всего самого странного из JavaScript. Он помогает производить более чистый, элегантный и читаемый код, а затем вдохновляет ЦА присоединиться к Open Source сообществу.

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ