Техногрет




Володя Колесников

Нетривиальный синтаксис 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, то пустую строку.

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

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

Циклы 

Обычный блок 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 createHanlder(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) // передаваемое методу значение
				

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

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

Результат при 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.

Еще 

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

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



Электропочта: mailbox@artlebedev.ru
Телефоны и адреса студии Наши магазины Телефон: (+7 495) 926-18-00