HTML и CSSXSLTJavaScriptИзображенияСофтEtc
Володя Колесников

Нетривиальный синтаксис 19 марта 2007


 
Задача. Разобраться в строке: (item.isGood ? good : bad)["add" + (item.typeName || "Default")]((item.process || function(x){return x})(item.node))

Совсем азы 

"1234" — строка «1234»;
1.234 — число 1,234;
1.534E+4 — число 15340;
["aaa", "bbb", 21] — массив из трех элементов: двух строк и числа;
{aaa: "aaa", "bbb":"bbb"} — объект со свойствами aaa и bbb;
[{foo: "bar", a: 12}, 25, ["hello"], new Date()] — массив, первый элемент, которого — объект со свойствами foo и а, второй элемент — число 25, третий — массив из одного элемента (строки "hello"), четвертый — объект типа Date.

Все объекты, включая массивы, в яваскрипте присваиваются по ссылке. Интерпретатор, встретив строку a = { field: 12}; b = a; не будет создавать два разных объекта для a и b. Вместо этого обе переменные будут ссылаться на один и тот же объект. И запись a.field будет равносильна b.field.

var a = {field: 12};
var b = a;
b.field = 7;
alert(a.field);
выведет 7. Аналогично

var a = [1,2,3]
var b = a;
b[0] = 5;
alert(a[0]);
выведет 5.
				

Азы 

Доступ к полям объекта возможен как с помощью «.», так и с помощью «[]». Конструкции obj.value = 1 и obj["value"] = 1 приводят к одному и тому же результату и выполняются с одинаковой скоростью. Тест.

Обращаться к полю объекта с помощью eval() — неоптимально. Следующая конструкция

var propertyName = "value";
eval("obj." + propertyName + "= 1")

выполняется в 10≈100 раз дольше, чем

obj[propertyName] = 1
				

Тернарный оператор поможет записать несложное условное выражение в одну строку. Конструкции

if(a > b) {
	Obj.value = a;
} else {
	Obj.value = b;
}
				

и

Obj.value = a > b ? a : b;
				

эквивалентны, но вторая заметно короче.

У тернарного оператора очень низкий приоритет. Поэтому
'hello ' + (2 == 2) ? 'world' : 'hell'
тоже самое, что
('hello ' + (2 == 2)) ? 'world' : 'hell'
и всегда возвращает "world ", а совсем не "hello world".

Операции с одновременным присвоением (/=, *=, -=, +=, %= и т. д.) иногда использую внутри других выражений. Операция присвоения всегда возвращает переменную, стоящую в левой части от знака равенства. Строка a = b = 3 присвоит b значение 3, а a — b, то есть 3. Операции
x = 5; y = x += 5
присвоят x и y значение 10.

Операции можно использовать внутри более сложных выражений:
Obj.value = (x /= range) * x;
равносильно
x = x / range; Obj.value = x*x;

Оператор ИЛИ (||) в отличие например от  C не возвращает булево значение. Он вернет либо первое значение, не равное false, либо, если таких нет, последнее встретившееся значение. Поэтому Obj.value = 0 || "hello" || 500 равно "hello", а совсем не true.

А Obj.value = null || "" || false || 0 равно 0.

Описанное выше удобно использовать при выставлении значений по умолчанию:

function concat(str1, str2) {
	str2 = str2 || "default";
	return str1 + str2;
}
				

Внутри конструкции ИЛИ интерпретатор вычисляет выражение только если предыдущие  были равны false. Поэтому в ситуации
Obj.value = true || myfunc()
myfunc не будет вызвана никогда.

Оператор И (&&) возвращает либо первое равное false, либо последнее встретившееся значение.

Оператор пригодится для последовательности действий, где каждое следующие должно выполнится только если предыдущее отработало успешно:
node && (tmp = node.getElementsByTagName('div')).length && tmp[0].innerHTML || ""
вернет innerHTML первого элемента div в node, если такой существует. Если дочерних узлов у node нет, или нет самого node, то пустую строку.

Преобразование типов 

Яваскрипт не является жестко типизированным языком. Тип переменной определяется тем, как ее использует программист. Иногда тип требуется изменить.

  • Из строки в число:
    parseInt("1234.999") //1234
    parseFloat("1234.999") //1234.999
    // более лаконичный вариант
    "1234.999" * 1.0 //1234.999
    "1234.999" / 1 //1234.999
    "1234.999" √ 0 //1234.999
    // но
    "1234.999" + 0 //"1234.9990"

  • Из числа в строку:
    (1234.999).toString(); //"1234.999"
    // 1234.999.toString() выдаст ошибку
    // более лаконичный вариант
    1234.999 + "" //"1234.999"
    '' + 1234.999 //"1234.999"

  • В булево значение
    !!15 //true

  • Булево значение в 1 или 0
    true + 0 //1

Циклы 

Обычный блок for состоит из трех частей, разделенных точкой с запятой. Вторая часть (проверка) выполняется на каждой итерации, поэтому для больших циклов стоит сделать ее максимально быстрой. Например
for(var i = 0, length = items.length; i < length; i++);
займет в среднем втрое меньше времени, чем
for(var i = 0; i < items.length; i++);
Тест.

Каждая из частей ≈ произвольное выражение. В любой из них можно вызывать функции или определять переменные. Следующая строка находит минимальный элемент в массиве:
for(var i = 1, min = items[0], length = items.length; i < length; min = Math.min(min, items[i]), i++);

Существует и другая версия оператора — for(var i in items){}. Она последовательно перебирает все индексы, свойства и методы items. For-in удобно использовать для того, чтобы перебрать все ключи хэша. Им можно воспользоваться и для перебора индексов массива, но такая операция будет неоправданно дорогой. Перед выполнением for-in интерпретатор составляет список всех ключей объекта. И уже по получившемуся списку пробегает обычным for. Поэтому for-in значительно (иногда на порядки) менее производительный чем обычный for. Тест.

Кроме того, for-in перебирает не только свойства самого объекта, но и свойства, определенные в его прототипе. Например, известная библиотека prototype.js определяет метод bind для всех функций. А значит, для любого созданного new obj() объекта, for-in будет возвращать ключ "bind". Обычно же, нужно выбрать только ключи самого объекта. Эта проблема решается так:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
hasOwnProperty = function(obj, prop) {
        if (Object.prototype.hasOwnProperty) {
            return obj.hasOwnProperty(prop);
        }
        return typeof obj[prop] != 'undefined' && 
                obj.constructor.prototype[prop] !== obj[prop];
}
for(var i in obj) {
	if(!hasOwnProperty(obj, i)) continue;
	// some code
}

				

Функции 

Создадим функцию одним из двух способов:
function a(x) { return 1 + x; }
или:
var a = function(x) { return 1 + x; }
(оба варианта равнозначны).

Каждая функция яваскрипта — объект. А значит, с ней возможно выполнять те же действия, что и с другими объектами. Например, присвоить ее переменной. Во втором варианте оператор function создает новый объект функции, который присваивается переменной а. С тем же успехом удастся присвоить созданную функцию и другой переменной:
var b = a;
А потом вызвать эту функцию:
b();

Как и у любого объекта, у функции могут быть свойства. Допустима, например, следующая запись:
b.someVar = 15;

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

var q = {};
q["someFunc"] = b;
q.someOtherFunc = function(){}
				

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

function createReturn(val){
	if(val > 10) {
		return function() { return val; }
	} else {
		return function() { return 0; }
	}
}
var a = createReturn(12);
var b = createReturn(12); // здесь в a и b одинаковые функции, но a != b
				

Функцию не обязательно чему-либо присваивать. Следующий код создает и сразу вызывает созданную функцию:
var a = (function(i){ return 2 + i; })(10); //12

С функциями разрешено использовать логические операторы. Например:
(funca || funcb)(12)
вызовет funca, если та определена и funcb — если нет.

Замыкания 

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

01 
02 
03 
04 
05 
06 
07 
08 
function createSomething (a){
var b = 12;
function doSomething() {
	return a + b;
}
return doSomething;
}
( createSomething(10) )(); //22
				

Примечание

«Помнить» означает, что для объекта функции интерпретатор создаст ссылки на переменные контекста, в котором она была описана. Так что, если в одном контексте созданы две разные функции, они будут ссылаться на одни и те же переменные.


Если изменить значение переменных после создания функции, изменения коснутся и созданной функции:

01 
02 
03 
04 
05 
06 
07 
08 
09 
function createSomething (a){
var b = 12;
function doSomething() {
	return a + b;
}
b *= b; // меняем b
return doSomething;
}
( createSomething(10) )(); //154
				

Возьмем пример посложнее

01 
02 
03 
04 
05 
06 
07 
08 
var tmp = node.getElementsByTagName('li');
for(var i = 0; i < tmp.length; i++){
	tmp[i].onclick = function(num){
		return function() {
			alert(num);
		}
	}(i);
}
				

Код выбирает все элементы li узла node. Каждому элементу скрипт добавляет обработчик события onclick. Обработчик выводит номер своего элемента.

Скрипт нельзя упростить до tmp[i].onclick = function(i){ alert(i); }.
В этом случае все элементы будут выводить последнее значение i, равное числу элементов в tmp. А нам нужно, чтобы каждый элемент выводил свой номер.

Чуть более понятным этот код станет если вынести создание обработчика с «алертом» в отдельную функцию:

01 
02 
03 
04 
05 
06 
07 
08 
09 
function createHandler(num) {
	return function() {
		alert(num);
	}
}
var tmp = node.getElementsByTagName('li');
for(var i = 0; i < tmp.length; i++){
	tmp[i].onclick = createHandler(i);
}

				

Примечание

Использование замыканий совместно с DOM-объектами часто приводит к утечкам памяти в Internet Explorer 5 и 6. Почитать про утечки памяти можно в статье «IE: where's my memory?».


Решение 

Вернемся к исходной строке, приведенной в задаче:
(item.isGood ? good : bad)["add" + (item.typeName || "Default")]((item.process || function(x){return x})(item.node))

Попробуем разобрать ее без окружающего контекста. Видно, что здесь используются объекты good, bad и item. У объекта item должны быть свойства isGood, typeName и node, а также метод process. У объектов good и bad ≈ методы вида add<typeName>.

Строку можно разбить на три части:

(item.isGood ? good : bad) // объект на котором будет вызван метод
["add" + (item.typeName ||  "Default")] // метод который будет вызван на объекте
(item.process || function(x){return x})(item.node) // передаваемое методу значение
				

Итак по порядку:

  • 1. Если item «хороший» (item.isGood), то вызывается метод add<typeName> объекта good, если «плохой» — объекта bad.
  • 2. Если у item есть тип (item.typeName), то будет вызван метод add<typeName>, в противном случае addDefault.
  • 3. Методу передается item.node, обработанный некоторой функцией. Если item определяет метод process, то node обрабатывается им. Если нет, создается пустая функция, которая не выполняя никакой обработки, возвращает сам node.

Пара примеров:

Результат при item.isGood = true, item.typeName = "String", item.process = null:
good["addString"](item.node)

Результат при item.isGood = false, item.typeName = null, item.process = function(node){...}:
bad["addDefault"](item.process(item.node))
				

Надо ли? 

То, что такие сложные конструкции существуют, еще не означает, что их нужно использовать. Вариант со строкой — пример того, как делать не надо. Разбираться в коде с такими «загадками» весьма сложно. Однако, знать о них полезно. По крайней мере для того, чтобы читать чужой код.

Примеры разумного использования можно посмотреть в коде библиотеки prototype.js.

Еще 

Я сознательно не касаюсь классов (и прототипов). Это тема отдельной статьи.

Вероятно, что не упомянуты какие-либо часто употребимые конструкции. Если вы знаете о такой, и напишете мне — буду вам признателен.