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,4 +1,4 @@
# Шесть типов данных
# Шесть типов данных, typeof
В JavaScript существует несколько основных типов данных.
@ -104,12 +104,65 @@ alert( x ); // "undefined"
Первые 5 типов называют *"примитивными"*.
Особняком стоит шестой тип: *"объекты"*. К нему относятся, например, даты, функции, он используется для коллекций данных и для объявления более сложных сущностей.
Особняком стоит шестой тип: *"объекты"*.
Позже, в главе [про объекты](/object) мы вернёмся к этому типу и рассмотрим его принципиальные отличия от примитивов.
Он используется для коллекций данных и для объявления более сложных сущностей.
Объявляются объекты при помощи фигурных скобок `{...}`, например:
```js
var user = { name: "Вася" };
```
Мы подробно разберём способы объявления объектов и, вообще, работу с объектами, позже, в главе [](/object).
## Оператор 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" (1)
*/!*
*!*
typeof function(){} // "function" (2)
*/!*
```
Последние две строки помечены, потому что `typeof` ведет себя в них по-особому.
<ol>
<li>Результат `typeof null == "object"` -- это официально признанная ошибка в языке, которая сохраняется для совместимости. На самом деле `null` -- это не объект, а отдельный тип данных.</li>
<li>Функции мы пройдём чуть позже. Пока лишь заметим, что функции не являются отдельным базовым типом в JavaScript, а подвидом объектов. Но `typeof` выделяет функции отдельно, возвращая для них `"function"`. На практике это весьма удобно, так как позволяет легко определить функцию.</li>
</ol>
К работе с типами мы также вернёмся более подробно в будущем, после изучения основных структур данных.
## Итого
Есть 5 "примитивных" типов: `number`, `string`, `boolean`, `null`, `undefined` и 6-й тип -- объекты `object`.
Очень скоро мы изучим их во всех деталях.
Оператор `typeof x` позволяет выяснить, какой тип находится в `x`, возвращая его в виде строки.

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>
Там, где нужно различать объекты, обычно используется утиная типизация, то есть мы смотрим, есть ли в объекте нужный метод, желательно -- тот, который мы собираемся использовать.

View file

@ -198,7 +198,7 @@ function work() {
Здесь нам не важно, какие, нас интересует именно как описана эта библиотека, как в ней применяется приём "модуль".
Вот выдержка из исходного файла:
Вот примерная выдержка из исходного файла:
```js
//+ run no-beautify
@ -221,8 +221,7 @@ function work() {
// код функции size, пока что доступен только внутри
*/!*
function size(collection) {
var length = collection ? collection.length : 0;
return typeof length == 'number' ? length : Object.keys(collection).length;
return Object.keys(collection).length;
}
*!*

View file

@ -243,9 +243,7 @@ alert( pete.age ); // 25
С обычными свойствами в коде меньше букв, они удобны, причины использовать функции пока нет.
...Но рано или поздно может произойти что-то, что потребует более сложной логики.
Например, формат данных изменился и теперь вместо возраста `age` хранится дата рождения `birthday`:
...Но рано или поздно могут произойти изменения. Например, в `User` может стать более целесообразно вместо возраста `age` хранить дату рождения `birthday`:
```js
function User(name, birthday) {
@ -263,12 +261,13 @@ var pete = new User("Петя", new Date(1987, 6, 1));
Добавление `get`-функции `age` позволяет обойти проблему легко и непринуждённо:
```js
//+ run
//+ run no-beautify
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
*!*
// age будет высчитывать возраст по birthday
Object.defineProperty(this, "age", {
get: function() {
var todayYear = new Date().getFullYear();
@ -280,10 +279,11 @@ function User(name, birthday) {
var pete = new User("Петя", new Date(1987, 6, 1));
alert( pete.age ); // получает возраст из даты рождения
alert( pete.birthday ); // и дата рождения доступна
alert( pete.age ); // и возраст
```
Таким образом, `defineProperty` позволяет нам использовать обычные свойства и, при необходимости, в любой момент заменить их на функции, сохраняя полную совместимость.
Таким образом, `defineProperty` позволяет нам начать с обычных свойств, а в будущем, при необходимости, можно в любой момент заменить их на функции, реализующие более сложную логику.
## Другие методы работы со свойствами

View file

@ -415,15 +415,9 @@ function mul(a, b) {
return a * b;
};
function ask(question, correctAnswer, ok, fail) {
var result;
if (typeof correctAnswer == 'boolean') {
result = confirm(question);
} else {
result = prompt(question, '');
}
if (result == correctAnswer) ok()
function ask(question, answer, ok, fail) {
var result = prompt(question, '');
if (result.toLowerCase() == answer.toLowerCase()) ok();
else fail();
}

View file

@ -1,104 +0,0 @@
# Секретное свойство [[Class]]
Для встроенных объектов есть одна "секретная" возможность узнать их тип, которая связана с методом `toString`.
Во всех встроенных объектах есть специальное свойство `[[Class]]`, в котором хранится информация о его типе или конструкторе.
Оно взято в квадратные скобки, так как это свойство -- внутреннее. Явно получить его нельзя, но можно прочитать его "в обход", воспользовавшись методом `toString` из `Object`.
[cut]
## Получение [[Class]]
Вернёмся к примеру, который видели раньше:
```js
//+ run
var obj = {};
alert( obj ); // [object Object]
```
**В выводе стандартного `toString` для объектов внутри `[object ...]` указано как раз значение `[[Class]]`.**
Для обычного объекта это как раз и есть `"Object"`, но если бы такой `toString` запустить для даты, то будет `[object Date]`, для массивов -- `[object Array]` и т.п.
К сожалению или к счастью, но большинство встроенных объектов в JavaScript имеют свой собственный метод `toString`: для массивов он выводит элементы через запятую, для дат -- строчное представление и так далее.
То есть, просто вызов `[1,2,3].toString()` вернёт нам `1,2,3` и никакой информации про `[[Class]]`.
Поэтому для получения `[[Class]]` мы одолжим функцию `toString` у стандартного объекта и запустим её в контексте тех значений, для которых нужно получить тип. В этом нам поможет метод `call`:
```js
//+ run
var toClass = {}.toString; // (1)
var arr = [1, 2];
alert( toClass.call(arr) ); // (2) [object Array]
var date = new Date;
alert( toClass.call(date) ); // [object Date]
var type = toClass.call(date).slice(8, -1); // (3)
alert( type ); // Date
```
Разберем происходящее более подробно.
<ol>
<li>Можно переписать эту строку в две:
```js
var obj = {};
var toClass = obj.toString;
```
Иначе говоря, мы создаём пустой объект `{}` и копируем ссылку на его метод `toString` в переменную `toClass`.
**Для получения `[[Class]]` нужна именно внутренняя реализация `toString` стандартного объекта `Object`, другая не подойдёт.**</li>
<li>Вызываем скопированный метод в контексте нужного объекта `obj`.
Мы могли бы поступить проще -- одолжить метод под другим названием:
```js
//+ run
var arr = [1, 2];
arr.toClass = {}.toString;
alert( arr.toClass() ); // [object Array]
```
...Но зачем копировать лишнее свойство в объект? Синтаксис `toClass.call(arr)` делает то же самое, поэтому используем его.
</li>
<li>Всё, класс получен. При желании можно убрать обёртку `[object ...]`, взяв подстроку вызовом `slice(8,-1)`.</li>
</ol>
Метод также можно использовать с примитивами:
```js
//+ run
alert( {}.toString.call(123) ); // [object Number]
alert( {}.toString.call("строка") ); // [object String]
```
[warn header="Вызов `{}.toString` в консоли может выдать ошибку"]
При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку `{}.toString.call(...)` -- будет ошибка. С другой стороны, вызов `alert( {}.toString... )` -- работает.
Эта ошибка возникает потому, что фигурные скобки `{ }` в основном потоке кода интерпретируются как блок. Интерпретатор читает `{}.toString.call(...)` так:
```js
//+ no-beautify
{ } // пустой блок кода
.toString.call(...) // а что это за точка в начале? не понимаю, ошибка!
```
Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки `( {}.toString... )` тоже сработает нормально.
[/warn]
## Итого
<ul>
<li>Свойство `[[Class]]` позволяет получить тип для встроенных объектов. Далее мы будем рассматривать создание своих объектов через функцию-конструктор, с ними `[[Class]]` не работает.</li>
<li>Для доступа к `[[Class]]` используется `{}.toString.call(obj).slice(8, -1)`.</li>
</ul>
Обычно в JavaScript используется "утиная" типизация. Свойство `[[Class]]` -- самое надёжное средство проверки типа встроенных объектов, но обычно утиной типизации вполне хватает.