$mol_strict: Как же меня [object Object] этот ваш undefined NaN‼

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Здравствуйте, меня зовут Дмитрий Карловский и я… не прощаю ошибок. Как только вижу оную — тут же бросаю что-нибудь исключительно тяжёлое. И как же тяжела работа программиста на JS...


class Foo extends Object {}
const foo = new Foo

`Здравствуйте, ${ foo }!`
// "Здравствуйте [object Object]!"

`В этом месяце вы заработали ${ foo / 1000 } тысяч рублей.`
// "В этом месяце вы заработали NaN тысяч рублей."

`Ваша цель "${ 'foo'[4] }" наконец-то достигнута.`
// "Ваша цель "undefined" наконец-то достигнута."

`Осталось ещё ${ foo.length - 1 } целей и вы достигните успеха.`
// "Осталось ещё NaN целей и вы достигните успеха."

Облегчить его страдания можно разными путями..


  1. Прикрыться тайпскриптом. Но в рантайме ноги всё равно остаются босыми, и на них кто-нибудь вечно наступает.
  2. Обложиться проверками. Но чуть замешкаешься и рантайм грабли тут же бьют по голове.
  3. Исправить JS. Даже не надейтесь.
  4. Исправить JS рантайм. Ну, давайте подумаем..

Проблемы с динамической типизацией JS возникают по 2 основным причинам:


  • Автоматическое (и порой неуместное) приведение типов, когда значение одного типа используется в контексте, предназначенном для другого.
  • Возврат undefined в качестве значения не объявленных полей.

Сначала разберёмся с первой проблемой. JS устроен так, что приведение примитивных типов мы никак исправить не сможем. А вот за приведением объектов у нас есть полный контроль. Поэтому давайте пропатчим глобальный прототип всех объектов, чтобы никакие объекты по умолчанию не допускали приведения типов:


Object.prototype[ Symbol.toPrimitive ] = function() {
    throw new TypeError( `Field Symbol(Symbol.toPrimitive) is not defined` )
}

Теперь, чтобы разрешить приведение типа, нужно переопределить метод Symbol.toPrimitive у нужного объекта.


Ладно, с первой проблемой разобрались. Но как-то она далась нам подозрительно легко… Что-то тут не так! Не похоже это на Веб… Ну да ладно, пошли к следующей.


Тут нам нужно как-то перехватывать обращения к произвольным полям объекта, даже если они ещё никак не были объявлены. В JS для этого есть только один механизм — прокси. Что ж, напишем такой прокси который при обращении к любому полю громко ругается квазицензурными словами:


export let $mol_strict_object = new Proxy( {}, {

    get( obj: object, field: PropertyKey, proxy: object ) {
        const name = JSON.stringify( String( field ) )
        throw new TypeError( `Field ${ name } is not defined` )
    },

})

К сожалению, поменять prototype у Object, как мы это сделали ранее, браузер нам уже не даст. Как и поменять прототип у Object.prototype — он всегда будет null. Зато мы можем менять прототипы у почти всех остальных стандартных классов унаследованных от Object:



Поэтому пройдёмся по всем глобальным переменным:


for( const name of Reflect.ownKeys( $ ) ) {
    // ...
}

Отсеим те из них, кто не является классами:


const func = Reflect.getOwnPropertyDescriptor( globalThis, name )!.value
if( typeof func !== 'function' ) continue
if(!( 'prototype' in func )) continue

Обратите внимание, что мы не используем globalThis[name] для получения значения, чтобы не триггерить ненужные варнинги.


Теперь оставляем лишь те классы, что непосредственно унаследованы от Object:


const proto = func.prototype
if( Reflect.getPrototypeOf( proto ) !== Object.prototype ) continue

И, наконец, подменяем прототип прототипа с Object.prototype на наш строгий вариант:


Reflect.setPrototypeOf( proto, $mol_strict_object )

Теперь почти все стандартные объекты будут смотреть на вас с укоризной, при обращении к свойству, которому не задано значение. Ведь если значение задано, то браузер не пойдёт по цепочке прототипов и не дойдёт до нашего прокси.


К сожалению, есть и исключения, такие как сам Object, все объектные литералы и всё, что унаследовано от EventTarget, который тоже не дают менять.


И тут CSSStyleDeclaration делает нам подножку: если подменить его прототип на прокси (даже прозрачный, не кидающий исключений), то, внезапно, в Хроме 89 он перестаёт синхронизироваться с атрибутом style dom-элемента:


( <div style={{ color: 'red' }} /> ).outerHTML // <div></div>

Поэтому его пока что приходится вносить в исключения.


Есть и ещё одна беда… Если объявлять прикладные классы так:


class Foo {}

То объекты этих классов не будут строгими. Но если объявить их так:


class Foo extends Object {}

То… ничего особо не изменится. Но вот если подменить глобальный объект Object на свой строгий подкласс:


globalThis.Object = function $mol_strict_object( this: object, arg: any ) {
    let res = Object_orig.call( this, arg )
    return this instanceof $mol_strict_object ? this : res
}

Reflect.setPrototypeOf( Object, Reflect.getPrototypeOf({}) )
Reflect.setPrototypeOf( Object.prototype, $mol_strict_object )

То прикладные классы, явно унаследованные от Object, станут строгими.


Итак, что у нас получилось...


class Foo extends Object {}
const foo = new Foo

`Здравствуйте, ${ foo }!`
// TypeError: Field "Symbol(Symbol.toPrimitive)" is not defined

`В этом месяце вы заработали ${ foo / 1000 } тысяч рублей.`
// TypeError: Field "Symbol(Symbol.toPrimitive)" is not defined

`Ваша цель "${ 'foo'[4] }" наконец-то достигнута.`
// TypeError: Field "4" is not defined

`Осталось ещё ${ foo.length - 1 } целей и вы достигните успеха.`
// TypeError: Field "length" is not defined

На случай, если это будут читать дети, подчеркну:


Не меняйте поведение рантайма в библиотеках, ибо таким образом вы можете сломать кому-нибудь приложение. А вот для запуска тестов лучше использовать как раз максимально строгий рантайм — мало ли использовать вашу библиотеку будут именно в нём.


Если хотите лучше понять как всё это работает, то гляньте эту статью: Насколько JavaScript сильный?.


Полные исходники можно найти тут: $mol_strict.


Для подключения к своему NPM проекту достаточно прописать где-нибудь в начале точки входа:


import "mol_strict"

Или:


require("mol_strict")

Другие независимые сборки микробиблиотек из $mol можно найти тут: $mol: Usage from NPM ecosystem.


А если хотите обсудить подноготную JS рантайма, то присоединяйтесь к этим чатам:


  • У браузера под юбкой (Обсуждаем разработку браузерных движков. Парсинг, рендеринг, архитектура, вот это всё.)
  • UfoStation Chat (ФП — Фронтенд и Программирование.)

Наконец, в твиттере _jinnin можно обнаружить много свежих мыслей на тему фронтенда, JS, UX и прочей дичи.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Вы уже перешли на TypeScript?

  • 0,0%Нет, у меня строгий рантайм0
  • 33,3%Нет, я люблю риск2
  • 0,0%Нет, я адепт защитного программирования0
  • 0,0%Нет, я использую другой тайпчекер0
  • 0,0%Да, но он мало чего чекает0
  • 33,3%Да, использую его в строгом режиме2
  • 33,3%Да, но нет2
Источник: https://habr.com/ru/post/550982/


Интересные статьи

Интересные статьи

Decision Table (таблица решений) — техника, помогающая наглядно изобразить комбинатору условий из ТЗ. Чем проще и понятнее требования, тем меньше будет разночтений. И тем...
Здравствуйте, Хабровчане! Сегодня я постараюсь поведать вам как школьник может наговнокодить написать бота для хранения домашки для VK. ОсторожноСразу обращу внимание, что для серьёзных проек...
Всем привет! Сегодня вашему вниманию предлагается перевод вдумчиво написанной статьи об одной из базовых проблем Java — изменяемости, и о том, как она сказывается на устройстве структур данны...
Многие творческие люди знают об этом феномене: они с волнением начинают новый проект по многообещающей теме, но если не могут сразу придумать оригинальную идею, то теряют интерес. Они приходя...
Июнь 2016-го. Остров Чеджу, Южная Корея. Третий день международной конференции по тепловым трубам. Во время перерыва подходят два китайца: – Здравствуйте! А вы из Теркона? – Из Теркона. – А ...