Заметки про JavaScript и не только

Обещания, работа с асинхронным кодом

2 января 2016, 21:35

Один из наиболее пожилых способ работы с асинхронным кодом — это использование функцией обратного вызова. Он заключается в передаче в асинхронный метод ссылки на функцию, которая будет вызвана (точнее, которая должна быть вызвана) после завершения работы асинхронной части данного метода. Такой подход имеет право на существование, но имеет ряд недостатков: он не консистентен (реализация решения может варьироваться в зависимости от авторов кода), не предполагается наличие гарантированного ответа (выполнился ли асинхронный код, выполнился ли он корректно), в целом, при работе с потоками данных, код получается плохо читаемым.

callService(data, function(result) {

});

Еще одной серьезной проблемой является снижение читаемости кода с ростом числа зависимых асинхронных вызовов.

$button.on('click', function (event) {
	setSomeAnimation(function () {
		result && service.getData(function (data) {
			$button.text(data);
		})
	});
});

Длинных влажностей можно избежать различными способами, например добавив свою агрегирующую функцию, которая будет передавать результат от асинхронного вызова к вызову.

//getUserData = ...;

getUserData([
	function (callback) {
		service.getUserAddress(id, callback);
	},
	function (callback) {
		service.getUserName(id, callback);
	},
	function (callback) {
		service.getUserSettings(id, callback);
	}
], function(result) {
	console.log(result);
});

Избежать этих проблем позволяет добавленный в ECMAScript 6 объект Promise. Если коротко, то он представляет из себя абстракцию, имеющую постоянный интерфейс, позволяющий решать проблемы, представленные выше, а так же позволяющий работать с асинхронными и синхронными потоками данных работать в единообразном стиле.

getUserAddress.then(getUserName).then(getUserSettings).then(getUserData);

По сути — это контейнер, который содержит значение или будет содержать значение.

Основные и вспомогательные методы

Promise.all( ) — метод возвращает обещание, которое будет разрешено после того, как будут разрешены все переданные в метод обещания, или отклонён, если будет отклонено хотя бы одно обещание.
Promise.race( ) — метод возвращает обещание, которое будет разрешено, как только будет разрешено хотя бы одно обещание
Promise.resolve( ) — возращает обещание, которое будет разрешено переданным в конструктор аргументом
Promise.prototype.then( ) — функция, переданная в данный метод, будет вызвана после разрешения данного общения
Promise.reject( ) — ...
Promise.prototype.catch( ) — ...


Создание обещания и паттерн раскрытия конструктора

При разработке класса Promise авторы использовали так называемый паттерн раскрытия конструктора (revealing constructor pattern). Обещание и связный с его разрешением код (код, манипулирующий внутренним состоянием класса) находятся сразу в одном явном месте. Таким образом работа с конструктором сразу раскрывает суть конкретного обещания. Таким образом, для создания экземпляра общения надо передать функцию (которая будет тут же вызвана) прямо в конструктор:

new Promise(function(resolve, reject) { 
});

Иначе мы получим ошибку:

new Promise();
// Uncaught TypeError: Promise resolver undefined is not a function

Лично для меня это решение кажется спорным и субъективно делает код менее понятным. Сравните два данных решения. Так делать следует:

function getData() {
	return new Promise(function(resolve) {

		// Resolve promise
		setTimeout(resolve, 500);
	});
}

А так делать хотелось бы:

// Данный код, как вы уже поняли, работать не будет
function getData() {
	var promise = new Promise();

	// Resolve promise
	setTimeout(promise.resolve, 500);

	return promise;	
}

В первом варианте не так очевидно, что функция, передаваемая в конструктор, будет вызвана сразу.

Promise.prototype.then(someFunction)

Следует запомнить одну простую вещь — аргументом всегда является функция. Таким образом можно сразу избежать неявных ошибок.

getUserData().then(getUserSetting).then(getUserName).then(updatePage);


Смешивание синхронного и асинхронного кода

Как я упоминал ранее, одно из примечательных преимуществ работы с постоянным интерфейсом, является возможность абстрагироваться от типа выполняемого кода, становится не важно, асинхронный был вызов или нет. В данном примере часть данных мы берем из локального кэша, а часть получается запросом:

getUserData().then(getUserSetting).then(getUserName).then(updatePage);

function getUserSettings() {
	return session.get('userId') || getFromServer();
}

function getUserName() {
	return new Promise(function(resolve) {
		setTimeout(resolve, 500);
	});
}


Как избежать пирамид кода?

В том случае, если получение какого-то набора данных требует нескольких зависимых между собой асинхронных вызовов, можно использовать фабрики:

function getUserName(data) {
	var userId = data.id;

	return new Promise(function(resolve) {
		$.ajax({
			done: function(result) {
				result(result);
			}
		});
	});
}

function getUserSettings(data) {
	var userName = data.name;

	return new Promise(function(resolve) {
		$.ajax({
			done: function(result) {
				result(result);
			}
		});
	});
}

getUserData().then(getUserName).then(getUserSettings);


Работа с потоком данных

Если ваш код клиентский, то, в общем-то, обещания можно использовать для работы с потоками данных (на сервере для эти целей следует использовать другие средства). Код получается читаемым, можно проследить историю изменения данных:

function calculateRealSpeed(startSpeed) {
	return startSpeed + 100;
}

function calculateTime(spped) {
	return 700 / spped;
}

Promise.resolve(speed).then(calculateRealSpeed).then(calculateTime);


Promise.race( )

Одним из примеров использования, казалось бы, странного метода Promise.race, разрешающего общение ответом от общения, которое разрешится первым, является добавление максимального времени выполнения запроса:

function timeout(promise, time) {
  return Promise.race([promise, delay(time)]).then(function () {
    throw new Error('Operation timed out');
  })]);
}

Данная фабрика вернет обещание, которое разрешится либо ошибкой, если раньше выполнится таймер, либо ответом от другого обещания.

Ещё о способе применения обещаний

Ещё одним способ применения обещаний, упрощающим жизнь, является использование обещаний при работе с общим одинаковыми запросами. Представьте, что различные контроллеры в различные моменты времени запросили у одного и того же сервиса данные. Вместо того, чтобы делать несколько различных запросов, мы можем все контроллерам вернуть ссылку на одно и тоже закэшированное обещание.

Отслеживание изменений свойств объекта

19 сентября 2014, 23:13

Пока весь мир ждет быстрый Object.observe, мир развивающийся, в котором все еще требуется поддержка InternerExplorer 7, может насладиться медленным отслеживанием изменений свойств объекта. Поддерижваются все браузеры. Почему же медленно? Дело в том, что в старых версиях Ie есть возможность повесить обработчик изменений свойств только на DOM-объекты. Ответственная это часть выглядит так:

var watchWrap = document.createElement('div'),
    watchDom = watchObject;

function attachChangeEvent(){
	watchDom[propertyName] = getFunction;
	watchDom[propertyName].toString = getFunction;
	watchDom.attachEvent('onpropertychange', onPropertyChange);
};

function onPropertyChange(event) {
	if (event.propertyName === propertyName) {

		watchDom.detachEvent('onpropertychange', onPropertyChange);

		setFunction(watchDom[propertyName]);
		attachChangeEvent();
	}
};

if(watchDom.nodeType === undefined){
	watchDom = document.createElement('WatchPropery');

	for(var readProperty in watchObject){
		if(Object.prototype.hasOwnProperty.call(watchObject, readProperty)){
			watchDom[readProperty] = watchObject[readProperty];
		}
	}
}


watchWrap.appendChild(watchDom);
attachChangeEvent();
watchWrap.removeChild(watchDom);

return watchDom;

Можно заметить, что объект мы вставляем в такой же DOM-объект, причиная тому — поведения браузера, обработка событий начинается только после действий над объектом,(например вставка в другой DOM-объект). В заверешении свойство оригинального объект приходится копировать в озданный DOM-элемент, в нем они начинают жить как атрибуты. Оригинальный объект, соотвественно, подменять на этот, созданный в памяти. Стоит ли наблюдение таких жертв? Ну, если вам надо любой ценой еще немного протянуть срок жизни старого браузера, почему нет. Для небольших задач решения хватит.

Пример кода:

var testObject = {
	property1: 1,
	property2: 2
};

testObject = WatchPropery(testObject, 'property1', function(newValue){
	alert('property1 was changed to: ' + newValue);
});


testObject = WatchPropery(testObject, 'property2', function(newValue){
	alert('property2 was changed to: ' + newValue);
});

Update: Из стандарта ES7 Object.observe убрали

Ветер крепчает

3 сентября 2014, 1:52

Даже если не пытаться найти какой-то смысл, а просто наслаждаться просмотром, картина получилась очень интересная и трогательная. Красиво нарисованная, с любовью к мелочам. Это история про любовь. Первая идея, которая приходит на ум после просмотра — не растрачивайте свою жизнь зря. Читается странное (для нас) отношение японцев к работе, в истории у главного героя немного времени, ему надо успеть воплотить свою мечту в жизнь, но при этом успеть хотя бы немного осчастливить свою возлюбленную. Думаю понятно, какой выбор он делает, путь к мечте в одной цитате: «Всё, чем я хотел заниматься, — это делать что-то прекрасное». С другой стороны, в японской анимации и любовь всегда выглядела немного странной — герои влюблены, но при этом во много дистанцированы друг от друга, благодаря чему на протяжении долгого времени в отношениях сохраняется волшебство романтики. Да, в фильме встречаются интересные факты. Оценка 8/10.

Про хранение данных на клиенте

19 августа 2014, 2:11

WebStorage
Медленный, ограниченный по размеру, но простой и хорошо поддерживаемый способ. Работает и в старом Internet Explorer 8. Спецификация не описывает правила физического хранения данных, поэтому реализация может отличаться от браузера к браузеру, на практике это значит, то разные браузеры позволяют хранить различное количество данных. Chrome, например, хранит в формате SQLite, преобразовывая строки в двоичные данные, старая Opera хранит в XML, преобразовывая в Base64.

Web SQL Database
Стандарт начал свою жизнь ярко (база данных на клиенте, светлое будущее же), но жил не очень долго. И это немного забавно, ведь, кроме быстрой скорости работы, база данных получила широкую поддержку в браузерах, даже в мобильных. Одна из версий отказа — сложность создания стандарта такого уровня «как SQL». То, что это обертка SQLite — это, просто, выбор механизма, используемого для реализации стандарта (с тем же успехом, например, IndexDB может использовать SQLite). В общем, браузерами поддерживается (в рамках черновика), но W3C использовать не рекомендует.

Indexed Database
Самый последний и самый модный способ хранения данных. Обратная ситуация относительно WebDB, стандарт пишется, но на момент записи, поддержки во всех браузерах все еще нет (касается мобильных браузеров, а так же браузера Safari), что, я считаю, является серьезным тормозом к началу использования.

Еще немного о конструкторах

4 июля 2014, 9:38

Коротко о главном. Все объекты порождаются конструкторами (для массивов [] и объектов предусмотрен синтаксический сахар {}). Любая функция становится конструктором как только перед ней появляется оператор new. В прототип конструктора класса автоматически добавляется свойство constructor, которое хранит ссылку на функцию-конструктор. Зачем же это свойство используется?
Т. к. в JavaScript отсутствуют классы в привычном понимании (хотя реализовать концепцию несложно), используя это свойство можно получить «класс» объекта, например, чтобы создать еще один схожий объект:

var OtherObject = new OneObject.constructor();

Другой случай применения более частный. В одном из популярных подходов в реализации наследования в конструкторе потомка сохраняется ссылка super или parent на прототип родителя (ссылка сохраняется именно в конструкторе, а не в прототипе, чтобы не загрязнять объект инородными свойствами):

var Constr1 = function(){ console.log('Constructor 1');};

// Конструктор унаследованного "класса"
var Constr2 = function(){
    Constr2.parent.constructor.call(this);
    console.log('Constructor 2');
};
Constr2.prototype = Object.create= Constr1.prototype;
Constr2.prototype.constructor = Constr2;

// Конструктор унаследованного "класса"
var Constr3 = function(){
    Constr3.parent.constructor.call(this);
    console.log('Constructor 3');
};
Constr3.prototype = Object.create= Constr2.prototype;
Constr3.prototype.constructor = Constr3;

var Object1 = new Constr1();
// Constructor 1
var Object2 = new Constr2();
// Constructor 1
// Constructor 2
var Object3 = new Constr3();
// Constructor 1
// Constructor 2
// Constructor 3

Все три конструктора стали родителями:

Object3 instanceof Constr3 
//true
Object3 instanceof Constr2
//true
Object3 instanceof Constr1
//true

Что же будет, если переопределить свойство конструктора в прототипе на некорректное? Не буду приводить пример с кодом, но ответ прост — ничего страшного не будет. Метод instanceof будет по-прежнему работать корректно. Вспоминается забавный случай, когда я проходил собеседование в одной очень крупной московской компании, ведущий разработчик тогда поругал extend-подход в наследовании, аргументируя тем, что instanceof будет работать некорректно. И это было бы странно, поскольку эти два выражения эквиваленты между собой:

Object1 instanceof Constr1;
Constr1.prototype.isPrototypeOf(Object1);

Как видно, свойство constructor здесь вообще не используется, поэтому его переопределение и не ломает функцию.
В завершении сделаю небольшую заметку по поводу метода bind. Данный метод заменит контекст выполнения на переданный только в том случае, если функция не является конструктором.

Жизнь примитивов

30 апреля 2014, 4:20

Примитивы — интересный предмет, они как бы объекты, но как бы и нет. Шутка. Примитивы — это, конечно же, не объекты. Но у начинающего программиста сразу возникнет вопрос, об ответе на который он догадывался, но боялся признаться. Почему, если добавить метод в прототип конструктора, например, String, то и примитивы строк получают этот метод?

String.prototype.myMethod = function(){ return 'Result'};
'What'.myMethod();
// "Result"

А дело все в том, что в момент работы с примитивом JavaScript создает временную «обертку», объект, который в качестве прототипа содержит прототип конструктора соответствующего типа его само значение. Временный он по той причине, что ссылок на него не создается, поэтому сборщик быстро убирает его из памяти, не создавая проблем. Но мы можем его немного обхитрить сборщик, чтобы проверить эту теорию:

Number.prototype.returnThis = function() {
    return this;
};
typeof 2014
// "number"
typeof 2014..returnThis()
// "object"

Мы добавили свой метод, возвращающий ссылку на этот временный объект. Можем убедиться, что значение действительно равно 2014, кстати, по умолчанию для числа значение равно 0:

var myNumber = 2014..returnThis()
// proto__: Number
//   ...
//   [[PrimitiveValue]]: 0
//   [[PrimitiveValue]]: 2014

И так, мы поняли, как как примитивы заворачиваются в объекты. Разберемся с обратной ситуацией. Как объекты становятся примитивами?

new Number(2000) + 14
// 2014

У каждого объекта есть унаследованный метод valueOf, он-то и отвечает за получение значений примитивов:

myNumber.someProperty = 'night';
myNumber.valueOf().someProperty = 'day';
myNumber .someProperty;
// "night"

Как вы уже догадались, для своих объект данный метод можно переопределить. Вычислим вес Михаила после поедания двух килограмм картошки:

var Man = function(weight) {
    this.weight = weight;
};

Man.prototype.valueOf = function() {
    return this.weight;
}

var food = 2,
    mike = new Man(65),
    afterEat = food + mike;

console.log(afterEat);
// 67

«Не могу получить имя класса»

14 апреля 2014, 7:05

Вспомнил недавнее недовольство моего C#-коллеги, которому пришлось столкнуться с Node.js и JavaScript. Он был очень расстроен тем фактом, что не мог понять от какого класса произошел объект (в том числе не мог получить имя этого класса). И не понимал, почему ничего не придумали для этого. А разница тут в отличиях от классово-ориентированных языков. Классов в обычном понимании у нас нет, все уже является «экземпляром» (Object instanceof Object), а «экземпляры» хранятся в памяти и передаются по ссылке. В результате, получается, что у объектов (и прототипов) — никаких имен нет. Может, быть, тогда в качестве имени, логично было бы использовать имя конструктора? Хотя мы и можем использовать именованные функциональные выражения, мы можем их так же и не использовать, поэтому «источника» имени у нас снова нет. Функция — это полноценный объект, тоже хранящийся в памяти. А ссылаться на объекты можно сразу из нескольких источников. Например:

var SomeObject = {
    someFunction: function(){ console.log('Hello!'); }
};
SomeObject.someOtherLink = SomeObject.someFunction;
delete SomeObject.someFunction;
console.log(SomeObject.someOtherLink);
// function (){ console.log('Hello!'); }
var NewObject = new SomeObject.someOtherLink();

Как видно, функция стала хранится в другом свойстве, а затем была использована в качестве конструктора. Поэтому, даже если бы мы захотели получить имя, нам бы его неоткуда было получать.
В завершении — немного про  именованные функциональные выражения. Думаю, можно догадаться зачем они. В этом хрупком мире, где объекты без имен хранятся в памяти, есть случае, когда, все же, надо суметь вызвать себя. Такие выражения используются очень редко, да, для рекурсивных вызовов. Рассмотрим код:

function CheckMe(count){
    count--;
    console.log(count);
    count > 0 && CheckMe(count);
};

var NewCheck = CheckMe;
CheckMe = function(){console.log('Problem?');};
NewCheck(2);
// 1
// Problem?

Как видно, работает он не так, как ожидается, поскольку ссылка CheckMe стала хранить другую функцию. Это проблему легко исправить:

var CheckMe = function RealCheck(count){
    count--;
    console.log(count);
    count > 0 && RealCheck(count);
};

var NewCheck = CheckMe;
CheckMe = function(){console.log('Problem?');};
NewCheck(2);
// 1
// 0

Теперь мы видим ожидаемый результат.

Проверка на существование переменной

1 апреля 2014, 21:04

Сегодня в стороннем коде системы показа баннеров был обнаружен небольшой, но некорректный код, приводиший к ошибке в том случае, для которого он и был написан. Код был примерно такой:

var bannerOptions = {};
if (constData) bannerOptions.constData = constData; 
// ReferenceError: constData is not defined

Переменная constData всегда существовала, поэтому об ошибке никто не знал, пока переменную не убрали. Правильнее было бы сделать так:

var bannerOptions = {};

if(typeof constData !== 'undefined') bannerOptions. constData = constData;

Это забавно, что кто-то об этом не знал и не пытался проверить, обратите внимание, что typeof возвращает именно строку:

console.log(typeof (typeof undefined))
// "string"

Проблем бы не было, если бы мы проверяли существование свойства:

var dataObject = {},
      bannerOptions = {};

if (dataObject.constData) bannerOptions.constData = dataObject.constData;

Добавлю, что проверка существования переменной — ситуация редкая. Обычно программисты знают, какие переменные определены, а какие нет.

Прототипный подход, отказ от конструкторов

17 марта 2014, 4:36

Жить без конструкторов очень просто:

var Object1 = {a: 'A'},
    Object2 = Object.create(Object1),
    Object3 = Object.create(Object2);

При этом, при желании мы можем по-прежнему проинициализировать наш объект с помощью функции:

var ObjectConstructor = function(){};
ObjectConstructor.call(Object1);

В этом случае мы теряем возможность использовать функцию instanceof, которая позволяет установить родителей, поскольку конструктором будет выступать Object, но взамен всегда можно использовать метод isPrototypeOf.

Object1.isPrototypeOf(Object3);
// true
Object2.isPrototypeOf(Object3);
// true

Как видно — оба родителя признали своего потомка. Можно вспомнить про отсутствие ссылки на конструктор, которая может помочь быстро установить родителя (да, фактические она есть, но ведет она на все тот же Object), но и эта проблема решается с помощью Object.getPrototypeOf :

Object.getPrototypeOf(Object3) === Object2
//true

Возникает вопрос, если же прототипное наследование хорошо обходится без new и без конструкторов, зачем их вообще тогда использовать? Все просто — этот способ понятнее большинству программистов. По большому счету, разница между класс-ориентированным наследованием и прототипным заключается в том, что в первом случаем мы наследуемся от класса, а во втором от объекта. Нет смысла отказываться от инициализирующих функций (конструкторов), и да, пусть в JavaScript, по сути, мы всегда работаем с экземплярами объектов, появлением, так скажем, базовых экземпляров, все равно предсказуемо. И вот мы видим, по сути, классы. Разработчики стандарта ECMAScript6 понимали это, поэтому там появился синтаксических сахар, повторяющий использование конструкторов. Эти два куска кода эквиваленты:

// ECMAScript 6
class MyClass {
    constructor(some_a){
        this.a = some_a;
    }
    method(){
        return this.a;
    }
}

// Текущий подход
function MyClass(some_a){
    this.a = some_a;
}
MyClass.prototype.method = function(){
    return this.a;
}