Володя Колесников
Нетривиальный синтаксис 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
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) // передаваемое методу значение
Итак по порядку:
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.
© 19952024 Студия Артемия Лебедева
|