• Техногрет
  • 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.

    Еще 

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

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