пятница, 10 октября 2014 г.

JavaScript: objects from scratch.

Из стандарта Ecma-Script:

ES - язык, основанный на объектах (object-based) и любая программа на нем представляет собой скопление взаимодействующих объектов.
Каждый объект представляет собой набор свойств, а каждое свойство, в свою очередь, имеет ряд атрибутов, определяющих как это свойство может быть использовано (например, быть доступным для чтения).
Свойство объекта также должно быть объектом (или функцией как разновидностью объекта или же литералом). 

В JavaScript объекты можно создавать разными способами. Наиболее употребимые из них - вызов функции-конструктора с помощью оператора new (при этом создастся пустой объект с дополнительными свойствами заданными в конструкторе):

var obj = new fnConstructor(value);

или же используя т.н. "синтаксический сахар", создавая пустой объект при помощи присваивания {}.

var obj = {};

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

var obj = Object.create(null);

Вновьсозданному объекту можно назначить свойства при помощи следующего вызова:

Object.defineProperty(object, name, config);

где object - ссылка на объект, name - строковый литерал, содержащий имя назначаемого свойства, а config - объект, содержащий ряд атрибутов свойств объекта (т.н. дескриптор):

value: '42' - предустановленное значение настраиваемого свойства, по умолчанию - undefined. Если свойство должно являться методом (содержать некоторый код), то его определение выглядит как value: function() { ... }

writable: true - возможность записывать значения в свойства (false дает readonly свойства). По умолчанию установлено в false. Исключение TypeError генерируется при записи в readonly свойство объекта только в strict mode.

enumerable: true - свойство объекта будет доступно при переборе с помощью конструкции (for key in obj) или вызова Object.keys(). В любом случае вызов Object.getOwnPropertyNames(obj) покажет даже такие "скрытые" свойства. По умолчанию свойство устанавливается в false.

configurable: true - свойство объекта можно переопределять или удалять. Будучи раз установленным в false свойство переопределению уже не поддаётся (кроме изменения параметра writable на false), а любые попытки изменить значения параметров value, enumerable, configurable, writable (на true), get/set приведут к исключению TypeError даже в обычном режиме (а попытка удалить свойство в strict mode).

get/set: эти свойства требуют указания функций геттера и сеттера, которые будут автоматически вызываться при попытке считать или установить значение (при этом устанавливаемое значение будет передаваться функции сеттеру в качестве параметра). С помощью этих свойств можно реализовать протоколирование считываний-назначений или какую-либо сложную обработку получаемых значений, приведение их к определенному типу, а также изменить свойства на функции при изменении кода с необходимостью сохранения API (например, если вместо хранения возраста в объекте начала храниться дата рождения, возврат возраста можно сделать как геттер-функцию от даты рождения).
Свойства get/set нельзя использовать одновременно со свойствами value и writeable (вызовет TypeError даже в обычном режиме).

Наряду с методом Object.defineProperty() можно использовать Object.defineProperties() для установки целого ряда свойств в одном вызове. Важно помнить, что при вызове defineProperty() свойства writeable, enumerable, configurable по умолчанию установлены в false, тогда как при создании свойства при помощи конструкции:

obj.prop = value;

эти свойства установлены в true.

defineProperty() можно вызывать из конструктора (в качестве ссылки на объект предпочтительно использовать this), а для улучшения читаемости кода можно использовать заготовки типа:

propWEC = { writeable: true, enumerable: true, configurable: true };

Свойства можно назначать при объявлении объекта в форме литерала, однако при этом устанавливается ряд умолчаний:

var a = { 
  name: 'Mikhail', 
  surname: 'Ivanov, 
  get fullName() { return name + surname },
  set fullName(newname) { 
                  var names = newname.trim().split(/\s+/); 
                  this.name = names[0] || '';
                  this.surname = names[1] || ''; }
}

Для каждого свойства объекта происходит упрощенное назначение name: value, при этом модификаторы writable, enumerable, configurable устанавливаются в true. А в качестве геттеров и сеттеров можно определить только анонимные функции, для предопределенных придётся использовать вызов defineProperty().

Для получения набора атрибутов, которые имеет то или иное свойство объекта используется вызов getOwnPropertyDescriptor(obj, 'property');

Помимо этого объект, определенный через {} получает в качестве предка Object.prototype, с которым наследует ряд стандартных свойств объекта JavaScript (типа toString() и т.п.).

Литералы могут быть объявлены только как часть выражения. В противном случае возможна трактовка их как блочных выражений с меткой:

{ a: 10 }   // Метка а
({ a: 10 }) // Объект со свойством a.

Свойства, для которых в дескрипторе атрибут configurable установлен в true, можно удалять: 

delete a.name;
console.log(a.name); // > undefined

Для каждого объекта набор присущих ему свойств можно получить тремя способами (см. описание атрибута enumerable).


среда, 8 октября 2014 г.

Функция как объект

JavaScript придерживается идеологии: "все - объект". В этом отношении функции мало чем отличаются от других объектов - они так же могут иметь собственные методы и свойства.

function f() {};
f.x = 10;
f.y = function() { console.log (f.x) };

f.y(); // > 10

Функцию можно создать при помощи конструктора Function:

var f = new Function(a, 'console.log(a)');

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

Код выше эквивалентен обычному объявлению функции:

function f(a) { console.log(a) }

В отличие от всех других объектов объект типа function имеет скрытое свойство [[Call]], которое и содержит выполняемый код функции. Выполнение происходит когда имя функции (или функциональное выражение) сопровождается парой скобок (возможно с передаваемыми параметрами):

f(10);
или
function f(a){ console.log(a) }(10);

В результате "превратить" обычный объект в функцию или создать функцию путем объявления объекта через фигурные скобки не удастся - только через непосредственное объявление или через конструктор Function (второй способ используется достаточно редко).

Каждая объявленная в глобальной области видимости функция является методом объекта global (как и каждая объявленная в ней переменная - ее свойством). По сути методы являются исполняемыми свойствами. А в JavaScript код функции - недоступное для модификации исполняемое свойство функции как объекта.




понедельник, 6 октября 2014 г.

Переменные и область видимости

Конспект из You Don't know JS, book 1: Scope and Closure.

Переменная, которая встречается в коде, может интерпретироваться в одном из двух контекстов, называемых RHS и LHS - right-hand-side и left-hand-side, часто встречаются значения lvalue и rvalue. Стороны выбираются относительно оператора присваивания:

a = b;

Левая сторона (lvalue) представляет собой некий контейнер, в который помещаются значения и задача компилятора сводится только к определению адреса этого контейнера по имени переменной, по которому это значение можно разместить.

Правая сторона (rvalue) представляет собой контейнер хранящий значения и важность представляют сами значения. В правостороннем контексте компилятор пытается эти значения извлечь.

В вызове console.log(a) на a ссылаются в правостороннем контексте, значение аргумента не меняется в ходе вызова функции (метода).

В присваивании a = 2 на а ссылаются в левостороннем контексте, т.к. изначальное значение а не важно компилятору, оно меняется в процессе присваивания.

При вызове любой функции, например: f(2) используются оба контекста. Имя функции обрабатывается компилятором в правостороннем контексте, по имени f он получает некоторое значение (которое представляет собой некоторый адрес, указывающий на начало блока с некоторым кодом, представляющим собой тело функции). В процессе вызова происходит неявное присваивание аргументу (численный литерал 2) формальному параметру функции. Если формальные параметры не объявлены в заголовке, то значение получит свойство объекта arguments с ключом 0. Если параметры были объявлены, то значение arguments[0] дополнительно скопируется в первый из объявленных в заголовке формальных параметров.

function foo(a) {
  var b = a;
  return a + b;
}

var c = foo( 2 );

RHS-ссылки: вызов foo, b = a, значение a и b при возврате результата выполнения функции.
LHS-ссылки: a = 2 при вызове foo, b = a при определении значения переменной b, c = foo() - присваивание результата вызова функции переменной.

Область видимости - набор правил в соответствии с которым движок может находить переменную по ее имени. При наличии вложенных функций каждая из них получает свою область видимости:

var x = 1, y = 3;

function a() {
  var y = 2;
  function b() {
    function c() {
      x = y;
    }
  }
}

Функции a, b, c имеют свои области видимости (с вложена в b, b вложена в а), кроме того существует глобальная область видимости (соответствующая объектам window или global).

Когда компилятор обнаруживает лексему y он последовательно проводит поиск в области видимости c, затем b, затем a, где и находит переменную. То же происходит с переменной x, но поиск завершается успехом в глобальной области видимости. Как только переменная найдена, поиск прекращается, что позволяет иметь переменные с одинаковыми именами на разных областях видимости. y, объявленная на глобальном уровне, будет "затенена" y, объявленной в области видимости a. Однако глобальные переменные доступны в любом месте программы как свойства глобального объекта.

В том случае, если RHS-поиск не увенчается успехом в глобальной области видимости, движок сгенерирует ошибку ReferenceError. LHS-поиск завершается в глобальной области видимости и если требуемое имя не находится и там, автоматически создается переменная с запрашиваемым именем. Однако в Strict mode поиск завершается ошибкой ReferenceError по аналогии с RHS.

TypeError генерируется в том случае, когда RHS-поиск завершился успешно, однако программа пытается выполнить действие, не предусмотренное для данного значения (например, вызывает как функцию переменную, не являющуюся ссылкой на функцию или пытается вызвать метод у переменной, содержащей значения undefined или null).

JavaScript использует лексическую область видимости, помимо этого существует динамическая область видимости, используемая, например, в языке сценариев Bash и некоторых режимах Perl. Лексическая область времени определяется на момент компиляции (выполнения), функция видит переменные в зависимости от того где она определена. В динамической области видимости функция видит переменные в зависимости от того где она вызвана.

function foo() {
  console.log( a );
}

function bar() {
  var a = 3;
  foo();
}

var a = 2;

bar();

Результатом выполнения bar() в JavaScript будет 2, потому что вызываемая в ней функция foo() определена в глобальном контексте и переменную a она ищет там же. Переменной а, объявленной внутри функции bar() для нее не существует. В языках с динамической областью видимости результатом выполнения будет 3, потому что функция foo() вызывается  после определения переменной a внутри функции bar() и пользуется именно этой "версией" переменной. В то же время поведение переменной this подчиняется правилам динамической области видимости (она получает разные ссылки в зависимости от места вызова), но об этом в другом разделе.

Область видимости определяется для каждого блока в процессе компиляции приложения, однако eval и with позволяют "обмануть" компилятор. eval может внести изменения в уже  сложившуюся область видимости, если в ходе его выполнения, например, будет объявлена новая переменная, затеняющая одну из ранее объявленных. with же создает новую область видимости в контексте того объекта, для которого вызывается.

И with, и eval замедляют скорость выполнения скрипта, делая невозможным ряд оптимизаций на этапе компиляции. Лучше не использовать их вообще, тем более, что они запрещены в strict mode (разрешен только непрямой eval).

В JavaScript отсутствует блочная видимость, она будет введена только со стандартом языка ES6. Однако конструкция try/catch позволяет локализовать переменные:

try {
  undefined()
} catch(err) {
  console.log(err);
}

console.log(err); // > ReferenceError

Для объявления переменной, видимой только в пределах блока (это может быть просто { }, if() { }, for() { ], while() { }) используется конструкция

let variable = value;

В отличие от объявления через var, объявление через let происходит строго в том месте, где расположен let. Поэтому:

console.log(a); // > undefined
var a = 5;

потому что объявление var a отделяется от присваивания компилятором и переносится в начало области видимости (hoisting - подъем), тогда как

console.log(a); // > ReferenceError - переменная считается необъявленной.
let a = 5;

Для эмуляции поведения let в ES5 можно использовать такой код:

try{throw value}
catch(variable){
   // use variable;

}

Если работа ведется с крупным объектом, то сборщик мусора гарантированно очистит память, удалив из нее объект, сразу после окончания блока catch. Во всех остальных случаях этот код несколько громоздок. Однако использование вместо него IFFE тоже имеет ряд недостатков: внутри нее меняется значение this, а также усложняется использование return, break и continue.

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


function outer() {
  var a = 2;
  function inner() {
    console.log( a );
  }
  return inner;
}

var clousure = outer();


clousre(); // > 2

Мы объявили функцию inner() внутри функции outer() и получили способ ее вызова вне той области видимости, в которой она была объявлена. На момент вызова closure() функция outer() уже прекратила свою работу и исчезла ее внутренняя переменная a. Однако closure() имеет доступ как к переменным, объявленным внутри функции inner(), так и внутри функции outer().

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

function polymorph(arg) {
  function ones() { console.log('I like ones!'); }
  function twos() { console.log('I like twos!'); }

  if (arg === 1) return ones;
  if (arg === 2) return twos;
  return function() { console.log('I like noones and notwos') };
}

onef = polymorph(0); onef(); // > I like ones!
nof  = polymorph(0); nof();  // > I like noones and notwos

"Проброс" к внутренней функции может быть косвенным:

var fn;

function outer1() {
    var a = 2;
    function inner() {
        console.log( a ); // 2
    }
    fn = inner;
}

function outer2() {
    fn();
}

outer1();

outer2(); // 2

outer2() получает доступ к функции inner() через внешнюю переменную fn, с которой транспортируется ссылка на внутреннюю функцию. При этом вместе с кодом функции inner() fn выносит за пределы outer() и всю область видимости outer() со всеми определенными в ней переменными.

Рассмотрим следующий код:

for (var i=1; i<=5; i++) {
  setTimeout( function timer(){
    console.log(i);
  }, i*1000 );
}

Функция timer() вызывается спустя некоторое время, которое достаточно для того, чтобы цикл успел завершиться. Поэтому на момент выполнения даже первой итерации i уже равно 6 и вся конструкция выведет последовательность 6 6 6 6 6
Кроме того каждая итерация создаёт новый экземпляр функции timer(), однако область видимости у всех них одна и та же и используют они одно и то же значение переменной i.

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

for (var i=1; i<=5; i++) {
 (function(j){
  setTimeout( function timer(){
    console.log(i);
  }, j*1000 );
  )(i);
}

IIFE получает дополнительный параметр, т.к. без  него мы получим только изолированную область видимости внутри замыкания, но она будет пуста. Передавая параметр (присваивание можно выполнять непосредственно временной переменной внутри IIFE) мы тем самым сохраняем текущее значение i на каждой итерации цикла. При этом у каждой итерации будет свое собственное значение j, которое и используется отложенной функцией при ее вызове. IIFE становится ячейкой, консервирующей нужное нам значение.

В стандарте ES6 можно использовать let вместо var и замыкания:

"use strict";
for (var i = 1; i<=5; i++) {
    let j = i;
      setTimeout(function timer() {
          console.log(j);
      }, j * 1000);
}

Циклы в ES6 имеют собственную область видимости, а переменная, объявленная с помощью let, уникальная в пределах этой области видимости.

По стандарту переменная, объявленная через let, должна сохранять свое значение в пределах каждой итерации цикла. Поэтому при полном соблюдении стандарта будет работать и такой вариант:

"use strict";
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
          console.log(i);
  }, i * 1000);
}


суббота, 4 октября 2014 г.

Отложенное выполнение

Для отложенного выполнения кода используются следующие системные функции:

setTimeout(callback, delay); 
setInterval(callback, delay);

Первая функция вызывает функцию callback разово через интервал времени не меньший delay. До того времени система продолжает выполнять имеющийся код (для заморозки процесса выполнения нужно использовать функцию sleep()).

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

/**

* @param {Function} callback Функция, вызываемая по окончании работы.

*/
function foo(callback) {
// Инициализируем переменные
(function inner() {
// Выполняем действия
if (/* больше ничего делать не надо */) {
callback();
} else {
setTimeout(inner, 500);
}
})();
}


setTimeout возвращает идентификатор таймера, который можно отменить, если потребность в вызове функции отпала с помощью функции clearTimeout(timerId).


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

/** * Вызывает функцию указанное количество миллисекунд в контексте ctx 
* с аргументами args. * @param {Number} millis * @param {Object} ctx * @param {Array} args * @return {Number} Идентификатор таймаута. */ Function.prototype.defer = function(timeout, ctx, args) { var that = this; return setTimeout(function() { that.apply(ctx, args || []); }, timeout); };

Теперь отложенные вызовы значительно упростятся:

foo.defer(1000, this, [1, 2])

Иногда возникает необходимость вызывать какую-либо функцию только если в течение некоторого времени не происходит пользовательской активности. Для этого используется такой шаблон:

/** * Возвращает функцию, вызывающую исходную с задержкой delay в контексте ctx. * Если во время задержки функция была вызвана еще раз, то предыдующий вызов * отменяется, а таймер обновляется. Таким образом из нескольких вызовов, * совершающихся чаще, чем delay, реально будет вызван только последний. * @param {Number} delay * @param {Object} ctx * @return {Function} */ Function.prototype.debounce = function(delay, ctx) { var fn = this, timer; return function() { var args = arguments, that = this; clearTimeout(timer); timer = setTimeout(function() { fn.apply(ctx || that, args); }, delay); }; };

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

document.getElementById('text').onkeypress = suggest.debounce(500);

среда, 1 октября 2014 г.

JavaScript: Массивы

Создание пустого массива:

var a = [];

Элементами массива могут быть любые значения - строковые, числовые и булевские литералы, объекты и функции. Создание массива с элементами:

var b = [1, true, "foo", 
    { x: 25, text: "source", f: function() { return 10 } },
    function() { return 25 }];

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

b[0];      // > 1
b[3].text; // > 'source'
b[3]['x']; // > 25
b[3].f();  // > 10
b[4]();    // > 25

Количество элементов массива хранится в свойстве length:

a.length(); // > 0
b.length(); // > 5

Индексация начинается с 0 и заканчивается (length - 1).


Изменяя свойство length можно менять длину массива. При установке length меньше текущей величины, "лишние" элементы массива теряются безвозвратно, при установке length больше текущей величины, массив дополняется до нужной длины элементами со значением undefined. Можно полностью обнулить массив, задав length = 0;


При обращении к элементу с несуществующим индексом вернётся значение undefined;

b[5]; // > undefined


При установке элемента с несуществующим индексом массив автоматически увеличит размерность, а элементы со всеми отсутствующими индексами будут установлены в undefined;

z = [1];
z[3] = 5;
z;         // [ 1, , , 5]


Для добавления в конец массива элемента можно использовать следующую конструкцию:

b[b.length()] = 100;

b.length();        // 6
b[b.length() - 1]; // > 100

Массив можно использовать как стек, добавляя в конец и убирая последнее значения при помощи типовых стековых методов push/pop:

a.push(10);
a.push(20);
a;       // > [ 10, 20 ]
a.pop(); // > 20
a;       // > [ 10 ]

Методы unshift/shift делают то же самое с началом массива:

a.unshift(1);
a.unshift(5)
a;         // > [5, 1, 10]
a.shift(); // > 5
a;         // > [ 1, 10 ];

Эти методы гораздо медленнее push/pop, т.к. требуют сдвига всего массива.

Методы unshift и push возвращают число элементов после добавления. Добавить можно произвольное число элементов, перечислив их через скобки:

с = [ 1 ];
c.unshift(10, 20, 30); // > 4
c.push(100, 200, 300); // > 7
c;                     // > [ 10, 20, 30, 1, 100, 200, 300 ]


Массив может создаваться как объект типа Array с использованием new. Если конструктор Array имеет один численный аргумент n, то создается пустой массив, содержащий n элементов со значением undefined. В противном случае массив заполняется значениями, переданными в качестве параметров конструктора.

var x = new Array(3);
x;                                // > [ , , ]
var y = new Array('a', 'b', 'c');
y;                                // > [ 'a', 'b', 'c' ]

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

var matrix = [ [1, 2, 3], [4], [7, 8, 9] ];
matrix[0][0]; // > 1
matrix[1][2]; // > undefined
matrix[1][2] = 6;
matrix;       // > [ [ 1, 2, 3 ], [ 4, , 6 ], [ 7, 8, 9 ] ];

Массив, подобно любому объекту, может содержать нестандартные свойства и даже элементы с отрицательными индексами:

var p = [ 1, 2];
p.x = 10;
p[-1] = 20;

При этом отрицательные индексы как свойства массива выводятся в виде строки.

p; // > [ 1, 2, x: 10, '-1': 20 ]

"Нестандартные" свойства не участвуют, например, в сортировке и прочих методах. работающих с элементами массива, а работа браузера с такими массивами замедляется.


Реверс строки.

Наиболее очевидный способ - преобразовать строку в массив, "развернуть" и склеить:

str.splice("").reverse().join("");

Наиболее производительный способ в JS, учитывая неизменяемость литералов - считывать из строки по одному символу, начиная с конца, и прибавлять их к новой строке при помощи конкатенации:

function str_reverse2(str) {
    var result = '',
        cursor = str.length;
    while (length--) {
        result += str[length];
    }
    return result;
}

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

понедельник, 29 сентября 2014 г.

Три роли функций

Функции в JS могут использоваться в трёх ролях:

1. Собственно функции
2. Методы
3. Конструкторы

Функция-метод практически непригодна к использованию как обычная функция, особенно если использует this, т.к. в случае вызова вне контекста объекта this будет указывать на глобальный объект (или будет равен undefined в strict mode).

Основной способ использования конструктора - вызов его для создания нового объекта с ключевым словом new. Использование без new является ошибкой, о способах противодействия этому можно узнать в отдельном разделе блога.

Конструктор при вызове с new неявно получает новый объект при вызове (на него ссылается this) и неявно возвращает новый объект, если return не используется в коде конструктора или же если return возвращает не объект.

Фактически, конструктор может вернуть произвольный объект любого типа, если в качестве аргумента оператора return будет использоваться объект:

mouse = { name: "Jerry", likesCheese: true, voice: function() { console.log("pi-pi-pi") } };

function Cat(name) {
    this.name = name;
    return mouse;
}

tom = new Cat("Tom");
tom.name;           // > "Jerry"
tom.voice();        // > pi-pi-pi
tom instanceof Cat; // > false

Одно из применений для подобного поведения - паттерн Singleton, позволяющий в конструкторе реализовать создание только единственного экземпляра объекта.

function Singleton() {

    if ( Singleton.instance ) {
        return( Singleton.instance );
    }
    return ( Singleton.instance = this );
 }

Только для такой (или подобной) реализации объекта две переменных, создаваемых через new, будут ссылаться на один и тот же объект, а следовательно будут равны.

var a = new Singleton();
var b = new Singleton();

a === b; // > true

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

var c = {}, 
    d = {};

c === d; // > false