| Володя Колесников
Нетривиальный синтаксис 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") //1234parseFloat("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 //truetrue + 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".
Обычно же, нужно выбрать только ключи самого объекта. Эта проблема решается так:
0102030405060708091011
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 если нет.
Когда создается функция, в ее теле разрешено использовать все переменные родительского блока. При последующем вызове созданной функции она будет «помнить» значения этих переменных. Например:
0102030405060708
function createSomething (a){
var b = 12;
function doSomething() {
return a + b;
}
return doSomething;
}
( createSomething(10) )(); //22
Примечание | «Помнить» означает, что для объекта функции интерпретатор создаст ссылки на переменные контекста, в котором она была описана. Так что, если в одном контексте созданы две разные функции, они будут ссылаться на одни и те же переменные. |
Если изменить значение переменных после создания функции, изменения коснутся и созданной функции:
010203040506070809
function createSomething (a){
var b = 12;
function doSomething() {
return a + b;
}
b *= b; // меняем b
return doSomething;
}
( createSomething(10) )(); //154
Возьмем пример посложнее
0102030405060708
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.
А нам нужно, чтобы каждый элемент выводил свой номер.
Чуть более понятным этот код станет если вынести создание обработчика с «алертом» в отдельную функцию:
010203040506070809
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) // передаваемое методу значение
Итак по порядку:
item «хороший» (item.isGood),
то вызывается метод add<typeName>
объекта good, если «плохой» объекта bad.item есть тип (item.typeName), то будет
вызван метод add<typeName>, в противном случае addDefault.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.