Тем не менее, не всегда это удобно. В частности, это не удобно, когда пишешь библиотеку для кого-то, кто потом может вызвать твои методы не так. По-этому я иногда предпочитаю вносить дополнительный код в свои скрипты, что бы чётко определять, какими типами должны являться значения тех или иных переменных.
Это решение направлено не только на проверку, но ещё и на преобразование типа к тому, которого я жду во избежание разных фокусов, которые могут подстерегать при автоматических контекстных преобразованиях.
В интегрированной среде разработки Eclipse есть такое средство, как Snippets, прекрасно подходящее для решения задачи контроля типов в JavaScript. Snippet - это кусочек кода в общем виде, настраиваемый под конкретную ситуацию путём редактирования некоторых имён и значений, называемых переменными snippet`ов. Их можно создавать и группировать, что бы потом удобно было находить и использовать. Я у себя создал группу "Params check" и в ней расположил snippet`ы для всех примитивов.
Перед тем, как начать, следует ввести константы-обозначения типов, с которыми будет проще осуществлять эти проверки. Эти константы будут иметь строковые типы и будут служить для сравнения результатов операции
typeof
, применяемой к переменным:var undefined,
Types = {
// Primitive types
/** @type {string} */ STRING_TYPE: 'string',
/** @type {string} */ OBJECT_TYPE: 'object',
/** @type {string} */ NUMBER_TYPE: 'number',
/** @type {string} */ BOOLEAN_TYPE: 'boolean',
/** @type {string} */ FUNCTION_TYPE: 'function',
/** @type {string} */ UNDEFINED_TYPE: 'undefined'
};
Итак, начнём с самого простого типа - boolean. На данный момент я произвожу проверку на него следующим блоком кода://Param x as boolean check
if (typeof x !== Types.BOOLEAN_TYPE)
if (x instanceof Boolean)
x = x.valueOf();
else {
if (x instanceof String)
x = x.valueOf();
if (typeof x === Types.STRING_TYPE)
x = x !== '' && x !== 'false';
else
x = x ? true : false;
}
, изначальный же Snippet для этогой проверки должен содержать переменную snippet`а для имяни переменной, что бы внести его один раз и больше не маяться://Param ${varName} as boolean check
if (typeof ${varName} !== Types.BOOLEAN_TYPE)
if (${varName} instanceof Boolean)
${varName} = ${varName}.valueOf();
else {
if (${varName} instanceof String)
${varName} = ${varName}.valueOf();
if (typeof ${varName} === Types.STRING_TYPE)
${varName} = ${varName} !== '' && ${varName} !== 'false';
else
${varName} = ${varName} ? true : false;
}
Следующий пример - для наиболее частого в использовании типа - строк://Param s1 as string check
if (s1 === null || typeof s1 === Types.UNDEFINED_TYPE)
throw new TypeError(
'Required param - s1 - is not specified by function call!'
);
if (typeof s1 !== Types.STRING_TYPE)
s1 = s1 instanceof String ?
s1.valueOf()
: s1.toString();
if (!/^[1-0a-f]*$/i.test())
throw SyntaxError('Required param - s1 - has invalid format.');
Обратите внимание, что здесь у нас уже две переменных для строки я счёл важным не только проверку её типа, но ещё и правильную проверку её формата с использованием регулярного выражения. В данном примере по шаблону проверяется, является ли строка представлением числа в 16-ричном формате.Теперь решение в общем виде - string Snippet:
//Param ${varName} as string check
if (${varName} === null || typeof ${varName} === Types.UNDEFINED_TYPE)
throw new TypeError(
'Required param - ${varName} - is not specified by function call!'
);
if (typeof ${varName} !== Types.STRING_TYPE)
${varName} = ${varName} instanceof String ?
${varName}.valueOf()
: ${varName}.toString();
if (!/^${regexp}$$/.test())
throw SyntaxError('Required param - ${varName} - has invalid format.');
Что же касается числового типа - number, то здесь имеется небольшой подводный камешек. Дело в том, что целые и вещественные числа спецификация ECMAScript не различает. Однако, как правило, в сценарии по логике легко понять, какого именно числа мы ждём - целого или дробного, так что я сделал два разных блока проверки - для целых (integer) и вещественных (float) чисел://Param x as integer check
if (x === null || typeof x === Types.UNDEFINED_TYPE)
throw new TypeError(
'Required param - x - is not specified by function call.'
);
if (typeof x !== Types.NUMBER_TYPE)
if (x instanceof Number)
x = x.valueOf();
else {
if (x instanceof String)
x = x.valueOf();
if (typeof x === Types.STRING_TYPE)
x = parseInt(x);
else
throw new TypeError(
'Required param - x - has invalid type!'
);
}
if (x < 1 || x > 100)
throw new RangeError(
'Required param - x = '
+ x
+ ' - is out of correct range (1 - 100).'
);
//Param y as float check
if (y === null || typeof y === Types.UNDEFINED_TYPE)
throw new TypeError(
'Required param - y - is not specified by function call.'
);
if (typeof y !== Types.NUMBER_TYPE)
if (y instanceof Number)
y = y.valueOf();
else {
if (y instanceof String)
y = y.valueOf();
if (typeof y === Types.STRING_TYPE)
y = parseFloat(y);
else
throw new TypeError(
'Required param - y - has invalid type.'
);
}
if (y < 0.55 || y > 0.99)
throw new RangeError(
'Required param - y = '
+ y
+ ' - is out of correct range (0.55 - 0.99).'
);
Видим, что для чисел кроме типа проверяется тот интервал значений, в который они должны попадать. Если они не входят в него, возбуждается исключение. Snippet`ы://Param ${varName} as integer check
if (${varName} === null || typeof ${varName} === Types.UNDEFINED_TYPE)
throw new TypeError(
'Required param - ${varName} - is not specified by function call.'
);
if (typeof ${varName} !== Types.NUMBER_TYPE)
if (${varName} instanceof Number)
${varName} = ${varName}.valueOf();
else {
if (${varName} instanceof String)
${varName} = ${varName}.valueOf();
if (typeof ${varName} === Types.STRING_TYPE)
${varName} = parseInt(${varName});
else
throw new TypeError(
'Required param - ${varName} - has invalid type!'
);
}
if (${varName} < ${min_value} || ${varName} > ${max_value})
throw new RangeError(
'Required param - ${varName} = '
+ ${varName}
+ ' - is out of correct range (${min_value} - ${max_value}).'
);
//Param ${varName} as float check
if (${varName} === null || typeof ${varName} === Types.UNDEFINED_TYPE)
throw new TypeError(
'Required param - ${varName} - is not specified by function call.'
);
if (typeof ${varName} !== Types.NUMBER_TYPE)
if (${varName} instanceof Number)
${varName} = ${varName}.valueOf();
else {
if (${varName} instanceof String)
${varName} = ${varName}.valueOf();
if (typeof ${varName} === Types.STRING_TYPE)
${varName} = parseFloat(${varName});
else
throw new TypeError(
'Required param - ${varName} - has invalid type.'
);
}
if (${varName} < ${min_value} || ${varName} > ${max_value})
throw new RangeError(
'Required param - ${varName} = '
+ ${varName}
+ ' - is out of correct range (${min_value} - ${max_value}).'
);
Вот и всё. Остаётся лишь добавить, что чаще всего я использую такие проверки для написания внешних библиотек, что бы контролировать те значения, которые приходят в мою функцию. Эти проверки излишни, если тот код, который вы пишете, будет вызываться так же вами и, таким образом, легче установить контроль с вызывающей стороны. Однако бывают ситуации, когда параметры вызова так или иначе зависит не от вас и тогда такие проверки бывают оправданны.
5 комментариев:
Интересный метод. Не проще ли обойтись простым сравнением конструктора поступившего параметра с необходимым типом? К примеру, ожидается строка:
if (srt && str.constructor == String) {;}
либо короче:
str = str.constructor == String && str;
Дело не столько в простоте, сколько в правильности. Просто нужно понимать, какие операции языка стоят за теми или иными его выражениями. За тем, что предлагаете вы, стоит следующее - JS-машина преобразует примитивное значение строки в объект String и затем вызывает у него метод constructor, сравнивая его (при чём, отметим, нечётко сравнивая, т.е. производя ещё кучу дополнительных условий!) с функцией String. Тогда как typeof не требует преобразования примитива в объект и по-этому работает, используя минимум ресурсов.
К сожалению, JavaScript даёт большие возможности по написанию не очень хороших сценариев с точки зрения оптимальности расходования ресурсов - и подавляющее большинство JS-программистов это используют, не задумываясь об этом. Мне в этом блоге хотелось бы обратить внимание на вопросы правильного написания JS-кода.
Окей, товарищ Java-программист, понял твою страсть всё имплементировать в объекты, даже, на мой неискушённый взгляд, такую простую вещь, как проверка ожидаемого типа )
С точки зрения производительности я как раз-таки и рассуждал, что простое обращение к конструктору переменной "легче" (пусть и с преобразованием примитива в объект), чем несколько вызовов функции typeof и свойств
объекта Types. Впрочем, это надо ещё проверять, что будет быстрее - это же JavaScript ("как хочу, так и реализовываю интерпретатор в своём браузере"), тесты производительности дают порой противоположные логике результаты даже в современных браузерах.
Согласен с замечанием, неявное сравнение == здесь плод поспешности, конечно сравнивать с приведением типов.
С существованием такой методики не спорю вообще, у меня он лишь вызвал некоторые вопросы.
Вообще, в целях убыстрения, можно при компрессии (например, YUICompressor`ом) менять константы объекта Types на их строковые значения - мне кажется, это будет быстрее, чем вызывать поле объекта. Я сейчас как раз работаю над такого рода add-on`ами к YUICompressor`у, которые позволяли бы такое проделывать, ориентируясь на JSDoc-комментарии. Но это - довольно сложная задача...
Отправить комментарий