refactor types

This commit is contained in:
Ilya Kantor 2015-03-21 16:48:12 +03:00
parent 0712ddc698
commit c108f03596
10 changed files with 69 additions and 438 deletions

View file

@ -1,28 +0,0 @@
function formatDate(date) {
if (typeof date == 'number') {
// перевести секунды в миллисекунды и преобразовать к Date
date = new Date(date * 1000);
} else if (typeof date == 'string') {
// разобрать строку и преобразовать к Date
date = date.split('-');
date = new Date(date[0], date[1] - 1, date[2]);
} else if (date.length) { // есть длина, но не строка - значит массив
date = new Date(date[0], date[1], date[2]);
}
// преобразования для поддержки полиморфизма завершены,
// теперь мы работаем с датой (форматируем её)
var day = date.getDate();
if (day < 10) day = '0' + day;
var month = date.getMonth() + 1;
if (month < 10) month = '0' + month;
// взять 2 последние цифры года
var year = date.getFullYear() % 100;
if (year < 10) year = '0' + year;
var formattedDate = day + '.' + month + '.' + year;
return formattedDate;
}

View file

@ -1,18 +0,0 @@
describe("formatDate", function() {
it("читает дату вида гггг-мм-дд из строки", function() {
assert.equal(formatDate('2011-10-02'), "02.10.11");
});
it("читает дату из числа 1234567890 (миллисекунды)", function() {
assert.equal(formatDate(1234567890), "14.02.09");
});
it("читает дату из массива вида [гггг, м, д]", function() {
assert.equal(formatDate([2014, 0, 1]), "01.01.14");
});
it("читает дату из объекта Date", function() {
assert.equal(formatDate(new Date(2014, 0, 1)), "01.01.14");
});
});

View file

@ -1,55 +0,0 @@
Для определения примитивного типа строка/число подойдет оператор [typeof](#type-typeof).
Примеры его работы:
```js
//+ run
alert( typeof 123 ); // "number"
alert( typeof "строка" ); // "string"
alert( typeof new Date() ); // "object"
alert( typeof [] ); // "object"
```
Оператор `typeof` не умеет различать разные типы объектов, они для него все на одно лицо: `"object"`. Поэтому он не сможет отличить `Date` от `Array`.
Используем для них утиную типизацию:
Функция:
```js
//+ run
function formatDate(date) {
if (typeof date == 'number') {
// перевести секунды в миллисекунды и преобразовать к Date
date = new Date(date * 1000);
} else if (typeof date == 'string') {
// разобрать строку и преобразовать к Date
date = date.split('-');
date = new Date(date[0], date[1] - 1, date[2]);
} else if (date.length) { // есть длина, но не строка - значит массив
date = new Date(date[0], date[1], date[2]);
}
// преобразования для поддержки полиморфизма завершены,
// теперь мы работаем с датой (форматируем её)
var day = date.getDate();
if (day < 10) day = '0' + day;
var month = date.getMonth() + 1;
if (month < 10) month = '0' + month;
// взять 2 последние цифры года
var year = date.getFullYear() % 100;
if (year < 10) year = '0' + year;
var formattedDate = day + '.' + month + '.' + year;
return formattedDate;
}
alert( formatDate('2011-10-02') ); // 02.10.11
alert( formatDate(1234567890) ); // 14.02.09
alert( formatDate([2014, 0, 1]) ); // 01.01.14
alert( formatDate(new Date(2014, 0, 1)) ); // 01.01.14
```

View file

@ -1,26 +0,0 @@
# Полиморфная функция formatDate
[importance 5]
Напишите функцию `formatDate(date)`, которая возвращает дату в формате `dd.mm.yy`.
Ее первый аргумент должен содержать дату в одном из видов:
<ol>
<li>Как объект `Date`.</li>
<li>Как строку в формате `yyyy-mm-dd`.</li>
<li>Как число *секунд* с `01.01.1970`.</li>
<li>Как массив `[гггг, мм, дд]`, месяц начинается с нуля</li>
</ol>
Для этого вам понадобится определить тип данных аргумента и, при необходимости, преобразовать входные данные в нужный формат.
Пример работы:
```js
function formatDate(date) { /* ваш код */ }
alert( formatDate('2011-10-02') ); // 02.10.11
alert( formatDate(1234567890) ); // 14.02.09
alert( formatDate([2014, 0, 1]) ); // 01.01.14
alert( formatDate(new Date(2014, 0, 1)) ); // 01.01.14
```

View file

@ -1,184 +0,0 @@
# Полиморфизм, typeof и утиная типизация
В этой главе мы рассмотрим, как создавать *полиморфные* функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты.
Для реализации такой возможности нужен способ определить тип переменной.
[cut]
Как мы знаем, существует несколько *примитивных типов*:
<dl>
<dt>`null`</dt>
<dd>Специальный тип, содержит только значение `null`.</dd>
<dt>`undefined`</dt>
<dd>Специальный тип, содержит только значение `undefined`.</dd>
<dt>`number`</dt>
<dd>Числа: `0`, `3.14`, а также значения `NaN` и `Infinity`</dd>
<dt>`boolean`</dt>
<dd>`true`, `false`.</dd>
<dt>`string`</dt>
<dd>Строки, такие как `"Мяу"` или пустая строка `""`.</dd>
</dl>
Все остальные значения, включая даты и массивы, являются объектами.
## Оператор typeof [#type-typeof]
Оператор `typeof` возвращает тип аргумента. У него есть два синтаксиса: со скобками и без:
<ol>
<li>Синтаксис оператора: `typeof x`.</li>
<li>Синтаксис функции: `typeof(x)`.</li>
</ol>
Работают они одинаково, но первый синтаксис короче.
**Результатом `typeof` является строка, содержащая тип:**
```js
typeof undefined // "undefined"
typeof 0 // "number"
typeof true // "boolean"
typeof "foo" // "string"
typeof {} // "object"
*!*
typeof null // "object"
*/!*
function f() { /* ... */ }
typeof f // "function"
*/!*
```
Последние две строки помечены, потому что `typeof` ведет себя в них по-особому.
<ol>
<li>Результат `typeof null == "object"` -- это официально признанная ошибка в языке, которая сохраняется для совместимости.
На самом деле `null` -- это не объект, а примитив. Это сразу видно, если попытаться присвоить ему свойство:
```js
//+ run
var x = null;
x.prop = 1; // ошибка, т.к. нельзя присвоить свойство примитиву
```
</li>
<li>Для функции `f` значением `typeof f` является `"function"`. На самом деле функция не является отдельным базовым типом в JavaScript, все функции являются объектами, но такое выделение функций на практике удобно, так как позволяет легко определить функцию.</li>
</ol>
**Оператор `typeof` надежно работает с примитивными типами, кроме `null`, а также с функциями. Но обычные объекты, массивы и даты для `typeof` все на одно лицо, они имеют тип `'object'`:**
```js
//+ run
alert( typeof {} ); // 'object'
alert( typeof [] ); // 'object'
alert( typeof new Date ); // 'object'
```
Поэтому различить их при помощи `typeof` нельзя.
## Утиная типизация
Основная проблема `typeof` -- неумение различать объекты, кроме функций. Но есть и другой способ проверки типа.
Так называемая "утиная типизация" основана на одной известной пословице: *"If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck (who cares what it really is)"*.
В переводе: *"Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка (какая разница, что это на самом деле)"*.
Смысл утиной типизации -- в проверке необходимых методов и свойств.
Например, у нас функция работает с массивами. Мы можем проверить, что объект -- массив, уточнив наличие метода `splice`, который, как известно, есть у всех массивов:
```js
//+ run
var something = [1, 2, 3];
if (something.splice) {
alert( 'Это утка! То есть, массив!' );
}
```
Обратите внимание -- в `if` мы не вызываем метод `something.splice()`, а пробуем получить само свойство `something.splice`. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте `true`.
Проверить на дату можно, определив наличие метода `getTime`:
```js
//+ run
var x = new Date();
if (x.getTime) {
alert( 'Дата!' );
}
```
С виду такая проверка хрупка, ее можно "сломать", передав похожий объект с тем же методом.
Но как раз в этом и есть смысл утиной типизации: если объект похож на массив, у него есть методы массива, то будем работать с ним как с массивом (какая разница, что это на самом деле).
[smart header="Метод `Array.isArray()`"]
Для массивов есть специальный метод проверки: `Array.isArray(arr)`, который возвращает `true` только если `arr` -- массив:
```js
//+ run
alert( Array.isArray([1,2,3]) ); // true
alert( Array.isArray("not array")); // false
```
Этот метод уникален в своём роде, других аналогичных (типа `Object.isObject`, `Date.isDate`) -- нет.
Если нужно удостовериться, что мы получили именно массив, а не нечто похожее на него -- можно использовать `Array.isArray`. Но при этом нужно отдавать себе отчёт, что этим мы одновременно ограничиваем применимость кода: "похожие на массив" данные теперь обрабатываться не будут. Решение зависит от конкретной ситуации.
[/smart]
## Полиморфизм
Пример полиморфной функции -- `sayHi(who)`, которая будет говорить "Привет" своему аргументу, причём если передан массив -- то "Привет" каждому:
```js
//+ run
function sayHi(who) {
if (Array.isArray(who)) {
who.forEach(sayHi);
} else {
alert( 'Привет, ' + who );
}
}
// Вызов с примитивным аргументом
sayHi("Вася"); // Привет, Вася
// Вызов с массивом
sayHi(["Саша", "Петя"]); // Привет, Саша... Петя
// Вызов с вложенными массивами - тоже работает!
sayHi(["Саша", "Петя", ["Маша", "Юля"]]); // Привет Саша..Петя..Маша..Юля
```
Здесь используется не "duck typing", а "жёсткая" проверка на массив. Можно было бы и поступить мягче -- проверить только наличие метода `forEach`:
```js
if (who.forEach) {
...
}
```
## Итого
Для написания полиморфных (это удобно!) функций нам нужна проверка типов.
Для примитивов с ней отлично справляется оператор `typeof`.
У него две особенности:
<ol>
<li>Он считает `null` объектом, это внутренняя ошибка в языке.</li>
<li>Для функций он возвращает `function`, по стандарту функция не считается базовым типом, но на практике это удобно и полезно.</li>
</ol>
Там, где нужно различать объекты, обычно используется утиная типизация, то есть мы смотрим, есть ли в объекте нужный метод, желательно -- тот, который мы собираемся использовать.