Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу).
Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу). Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу). Homepage Карта сайта Версия для печати

Джентльменский набор Web-разработчика   Ларри Уолл о Perl6   Наблы Система Orphus
 

39. Большие хитрости JavaScript

[5 марта 2004 г.] обсудить статью в форуме

  Нести ничего не стоит так сложно,
Тем более, если нести далеко.

Наутилус Помпилиус

Чайник 

Данная набла является продолжением предыдущей.

В предыдущей набле я довольно часто использовал инструкцию var для создания новой переменной. Вы, наверное, обратили внимание, что в большинстве случаев можно было бы обойтись и без нее. Сравните:

Листинг 1
x = "Hello";
alert("x="+x);

и

Листинг 2
var y = "Hello";
alert("y="+y);

Казалось бы, обе эти программы работают одинаково. Так есть ли разница? Чтобы узнать об этом, запустим следующий код:

Листинг 3
function Test() {
  x = "Hello";
  var y = "Hello";
  alert("x="+x);  
  alert("y="+y);
  alert("window.x="+window.x);
  alert("window.y="+window.y);
}
Test();

Вы увидите интересную вещь. Оказывается, если переменной присваивается значение в функции без использования var, то она попадает в объект window. Иными словами, x=1 эквивалентно window.x=1, и префикс window. можно опускать.

Лирическое отступление 
Тем не менее, код alert(non_existent_variable) выдает ошибку, а код alert(window.non_existent_variable) — просто печатает "undefined".

Наверное, вы уже поняли, что при помощи var создаются переменные, локальные внутри функции (их еще называют лексическими, потому что они «не видны» нигде снаружи относительно фигурных скобок функции). Незачем лишний раз захламлять глобальную область видимости (объект window).

Чайник 

Т.к. для каждого фрейма на странице существует свой объект window, глобальные области видимости скриптов, запущенных в разных фреймах, не пересекаются.

Замыкания в JavaScript

Perl славен тем, что это язык, который работает с кодом точно так же, как с данными. Иными словами, код (функции) для него — точно такие же данные, как и все остальное. Можно генерировать код во время работы программы (например, по шаблону) и потом передавать ему управление. Можно создавать новые функции и удалять старые, переименовывать их и т. д.

Как ни странно, JavaScript в этом отношении наиболее похож на Perl. Возможно, вы и не догадывались, что привычный всем код

Листинг 4
function hello(world) { alert("Hello, "+world) }

в действительности обозначает для JavaScript-интерпретатора обыкновенный оператор присваивания:

Листинг 5
hello = function(world) { alert("Hello, "+world) };

Такой вот интересный оператор. Видите, в JavaScript тоже можно создавать функции во время работы программы (а именно, в момент выполнения оператора присваивания).

Может быть, вы спросите, какая разница, где создавать функции — непосредственно в операторе присваивания или отдельно? Если бы оператор function всегда создавал идентичные функции, конечно, смысла в нем не было бы никакого. Однако, как и в Perl, в JavaScript поддерживаются замыкания.

Замыкание — это функция, плюс все те лексические переменные из охватывающего контекста, которые она использует. Когда мы используем оператор function, мы всегда создаем не функцию, а именно замыкание.

Проиллюстрирую сказанное на примере. Создадим массив из 100 функций, каждая из которых будет печатать квадрат некоторого числа.

Листинг 6
// Создает одну функцию, которая печатает квадрат 
// указанного числа.
function createFunc(n) {
  return function() { alert(n*n) };
}
// Создает number таких функций и возвращает их массив.
function create(number) {
  var arr = [];
  for (var i=1; i<=number; i++) {
    arr[i] = createFunc(i);
  }
  return arr;
}
// Теперь создаем все функции...
var arr = create(100);
// ...и запускаем четвертую по счету.
arr[4]();

В данном примере замыкание создается в функции createFunc(), и лексическая переменная, которая сохраняется в замыкании, — это n. Таким образом, вызвав 100 раз createFunc() с различными значениями n, мы получим 100 различных функций, каждая из которых будет печатать свою собственную n.

Отличия от Perl

К сожалению, замыкания в JavaScript все же работают чуть по-другому, чем в Perl. В последнем каждая функция-замыкание имеет свой собственный набор лексических переменных, никак друг с другом не связанных. Мы могли бы написать так:

Листинг 7
// Создает number таких функций и возвращает их массив.
function create(number) {
  var arr = [];
  for (var i=1; i<number; i++) {
    arr[i] = function() { alert(i*i) };
  }
  return arr;
}

в надежде, что это заработает. Однако в JavaScript данный способ создаст 100 одинаковых функций, каждая из которых будет печатать 100*100! Почему же так происходит?

Дело в том, что в замыкание попадает не сама переменная, а ссылка на нее. (Это и неудивительно: ведь в JavaScript все переменные хранят ссылки на объекты.) Таким образом, в последнем примере все 100 замыканий, созданных в цикле, будут разделять одну-единственную общую переменную i. При выходе из функции ее значение — 100. Вот почему все функции печатают 100*100 = 10000, в то время как в Perl подобный код создал бы 100 разных подпрограмм.

Для того, чтобы исправить ситуацию, мы написали отдельную функцию createFunc(), в которой и создаем замыкание. Т.к. при вызове функций и передачи им в параметрах строк и чисел последние копируются по значению (а не передаются по ссылке — так сделали специально), все работает, как мы и хотели: каждая из сотни функций имеет свою собственную переменную n.

Оказывается, можно обойтись и без создания функции createFunc(), однако это потребует от нас написания двух замыканий на каждом обороте цикла:

Листинг 8
function create(n) {
  var arr = [];
  for (var i=1; i<n; i++) {
    // Создаем функцию...
    arr[i] = function(x) { 
      // создание замыкания с лексической x
    return function() { alert(x*x) } 
  }(i); // и тут же ее вызываем с параметром i!
  }
  return arr;
}

Это работает по той же самой причине, что и код выше — за счет копирования i во вновь создаваемую при каждом вызове переменную x.

Чайник 

Я догадываюсь, что данный материал может показаться весьма сложным для первоначального понимания (особенно для тех, кто первый раз слышит о замыканиях как таковых). К сожалению, это не просто кажется — он таким и является. Так что, если хотите разобраться, перечитывайте и экспериментируйте самостоятельно.

Создание классов

Я видел в нескольких руководствах (и даже книгах!) по JavaScript примерно следующий код для создания нового класса (для примера — NewClass):

Листинг 9
// Конструктор.
function NewClass() {
  this.property = 123;
  this.method1 = NewClass_method1;
  this.method2 = NewClass_method2;
  // ...
}

// Методы.
function NewClass_method1(x) {
  alert("Вызван method1("+x+"), property="+this.property);
}

function NewClass_method2(x) {
  alert("Вызван method2("+x+")");
  this.method1();
}

// Создаем объект и проверяем работу.
var obj = new NewClass();
obj.method1(10);

Но позвольте, это же так же нелепо, как писать:

Листинг 10
var zero = 0;
var one = 1;
var ten = 10;
...
// везде вместо цифр используем "константы"
alert(one * ten);

Это настолько же некрасиво, как использовать

Листинг 11
#define begin {
#define end }
#define then
#define write printf

с тем, чтобы писать на Си паскалеподобные программы:

Листинг 12
if (a > 10) then begin
  write(a);
end;

В общем, когда классы сложные, и в них много методов, использование приведенного выше способа — весьма утомительное занятие. Действительно, для добавления нового метода к классу мы вынуждены изменять исходники в двух местах: во-первых, в конструкторе, а во-вторых, в конце файла, там, где определяются тела функций. При необходимости переименовать метод нам также нужно вносить изменения в несколько мест.

Использование прототипов

Что же можно предложить взамен? Код, который я увидел полгода назад в Mozilla, создавал классы так:

Листинг 13
function NewClass() { /* пусто */ }
// Определяем первый метод.
NewClass.prototype.method1 = function(x) {
  alert("Вызван method1("+x+")");
}
// Определяем второй метод.
NewClass.prototype.method2 = function(x) {
  alert("Вызван method2("+x+")");
  this.method1();
}
// Создаем объект и проверяем работу.
var obj = new NewClass();
obj.method1(10);

Считается, что это — самый правильный способ. Что же такое prototype? Прототип — это некоторый хэш, в котором хранятся свойства и методы, присущие классу по умолчанию. Каждый объект (хэш) имеет ассоциированный с ним хэш-прототип. Когда мы пишем: obj.prop, вначале свойство prop ищется в хэше-объекте obj, а если его там нет, то в его прототипе, в прототипе его прототипа и т. д. по цепочке. Это же относится и к методам.

Фактически, класс — это всего лишь функция, с которой ассоциирован отдельный прототип. Оператор new создает новый объект, присваивает его значение специальной переменной this, а затем вызывает функцию-конструктор и делает доступным this внутри нее. Вызов obj.someMethod() также присваивает переменной this ссылку obj, передает ее в функцию someMethod() и запускает последнюю на выполнение.

Прямое создание методов

Способ с прототипами довольно стандартен, однако он имеет и недостатки. При объявлении каждого метода мы вынуждены без конца твердить имя класса, к которому он принадлежит. Если вдруг понадобится это имя поменять, придется делать это во всем исходнике. Существует еще один метод, который мало того, лишен описанных недостатков, но еще и позволяет создавать закрытые (private) свойства класса. Вот он:

Листинг 14
// Конструктор.
function NewClass() {
  this.property = 123;
  // Создаем методы класса прямо в конструкторе.
  this.method1 = function(x) {
    alert("Вызван method1("+x+")");
  }
  // То же самое.
  this.method2 = function(x) {
    alert("Вызван method2("+x+")");
    this.method1();
  }
}
// Создаем объект и проверяем работу.
var obj = new NewClass();
obj.method1(10);

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

За счет использования замыканий можно реализовать private-свойства класса:

Листинг 15
// Конструктор.
function NewClass() {
  this.from = "author";   // открытое свойство (this)
  var name  = "somebody"; // закрытое свойство (var)

  // Приветствие.
  this.hello = function() {
    // закрытые свойства пишем без this
    alert("Hello, "+name+", from "+this.from);
  }
  
  // Метод для установки значения закрытого свойства.
  this.setName = function(n) {
    name = n;
  }
}

// Создаем объект и проверяем работу.
var obj = new NewClass();
obj.setName("world");
obj.hello();

// Другой объект - для проверки.
var obj1 = new NewClass();
obj1.hello();

Данный пример позволяет убедиться, что obj и obj1 имеют индивидуальные переменные name, доступ к которым возможен только через функцию setName() и никак иначе.

Дзен JavaScript-а

Возможно, вы в данную секунду ощущаете некоторую путаницу в мыслях. Если это так, она означает только одно: вы смутно почувствовали, что в JavaScript ... нет никаких классов! Может быть, вы этого еще не осознаете. Все, чем мы оперируем в JavaScript, является объектами, и только. Функция, целое число, хэш, массив — это все объекты. У каждого объекта есть прототип, который также является объектом. Если в некотором объекте (хэше) не удается найти некоторое свойство, поиск продолжается в прототипе этого хэша.

Также нет никакой существенной разницы между свойствами и методами. Метод — всего лишь свойство, хранящее ссылку на некоторый код. Чтобы вызвать метод (или функцию), достаточно после выражения поставить круглые скобки, а в них — указать параметры:

Листинг 16
// Это корректный код!
(function(n) { alert(n) })(10);

Кроме того, «просто функций» также не существует: любая функция является в действительности методом — либо указанного вами объекта, либо же объекта window (когда мы объявляем функцию как function F() {}).

В общем, JavaScript — это такой дзенский язык программирования, в котором все — объекты, и нет никакой разницы между хэшем и объектом, между объектом и классом, между свойством и методом, между методом и обычной функцией...

Наследование

Как известно, без пруда не выловишь и рыбку из него. Эта пословица особенно хорошо применима к JavaScript: раз в нем нет классов, их и наследовать нельзя.

Лирическое отступление 
Еще одна аналогия. В буддийской концепции ум нельзя уничтожить. Но не потому, что он прочен, как скала, непоколебим и неуничтожим, вовсе нет! Просто ум не обладает ни одним свойством, к которому можно было бы применить термин «уничтожить». А раз нельзя уничтожить часть, нельзя избавиться и от целого. Точно так же, в JavaScript наследовать классы нельзя.

Тем не менее, можно наследовать объекты. Собственно, все «наследование» сводится к присваиванию нового значения прототипу объекта (свойству prototype).

Листинг 17
// Базовый "класс".
function Base() {}
Base.prototype.f1 = function() { alert(1) }

// Производный "класс".
function Derive() {}
Derive.prototype = new Base(); // без new нельзя!
Derive.prototype.f2 = function() { alert(2) }

var obj = new Derive();
obj.f1(); // вызывается функция базового объекта

Видите, мы фактически делаем класс, производный от объекта, а не от другого класса — используется оператор new. По этой причине в JavaScript лучше не нагружать конструктор ничем лишним, а оставить на его долю лишь заполнение свойств начальными значениями, а методов — кодом.

Изменение стандартных прототипов

При помощи прототипов можно делать интересные вещи. Например, вы можете добавить в стандартный класс Number (любое число принадлежит этому классу) метод sqr(), который будет возвращать его квадрат:

Листинг 18
var x = 10;
Number.prototype.sqr = function() { return this*this }
alert(x.sqr());

Обратите внимание на то, что прототип класса был изменен уже после создания переменной x. И, тем не менее, в объекте x, как по волшебству, появился новый метод — sqr().

Также можно менять прототипы и любых других встроенных «классов» (Object, Array, String, Function и т. д.). Например, вы можете добавить функцию dup() для дублирования строки и последующей склейки дубликатов:

Листинг 19
String.prototype.dup = function(n) {
  var s = "", t = this.toString();
  while (--n >= 0) s += t;
  return s;
}

Запустив этот код, вы можете в любом месте программы писать что-то вроде alert(a.dup(10)) или даже " ".dup(10) (для вывода 10 пробелов).

Наконец, давайте перепишем функцию Dump() из предыдущей наблы и определим с ее помощью стандартный метод Object.toString() (и в Array тоже), который автоматически вызывается при конвертировании объекта в строку. Сделав это, мы сможем распечатывать объекты простым вызовом alert(obj)!

Листинг 20
Array.prototype.toString = 
Object.prototype.toString = function() {
  var cont = [];
  var addslashes = function(s) {
    // Использовать replace НЕЛЬЗЯ - в Опере
    // происходит зацикливание, т.к. из replace
    // зачем-то вызывается Object.toString().
    return 
      s.split('\\').join('\\\\').split('"').join('\\"');
  }
  for (var k in this) {
    if (cont.length) cont[cont.length-1] += ",";
    var v = this[k];
    var vs = '';
    if (v.constructor == String) 
      vs = '"' + addslashes(v) + '"';
    else 
      vs = v.toString();
        if (this.constructor == Array)
      cont[cont.length]
        else 
      cont[cont.length] = k + ": " + vs;
  }
  // Здесь тоже нельзя делать replace()! 
  cont = "  " + cont.join("\n").split("\n").join("\n  ");
  var s = cont;
  if (this.constructor == Object) {
    s = "{\n"+cont+"\n}";
  } else if (this.constructor == Array) {
    s = "[\n"+cont+"\n]";
  }
  return s;
}

Пример использования:

Листинг 21
var hash = {
  color:    "red",
  artefact: "pill",
  actors: {
    supplier: "Morp\"heus",
    consumer: "Neo"
  },
  numbers: [10, 20, 30],
  slashquote: "with \\ (slash) and \" (quote)"
}
alert(hash);

Итого

JavaScript напоминает Perl, но не во всем. Хэши, замыкания, регулярные выражения — общая часть. В то же время, объектно-ориентированные механизмы языков различаются.

Материал, описанный в данной набле, конечно же, не уникален. Вы можете найти в Гугле массу статей и даже книг на эту тему. Например, первое, что мне попалось: http://wdh.suncloud.ru/js01.htm.

обсудить статью в форуме

 
Рекламный блок
   

На странице:
    39. Большие хитрости JavaScript
Замыкания в JavaScript
Отличия от Perl
Создание классов
Использование прототипов
Прямое создание методов
Дзен JavaScript-а
Наследование
Изменение стандартных прототипов
Итого

Важное объявление:
    автор категорически против копирования и распространения в Интернете всех статей «Куроводства» с возрастом, меньшим 6 месяцев. Печальный опыт «расползания» чрезвычайно устаревших ошибочных версий статьи про Apache действительно объясняет такое решение.

Орфография на «Куроводстве»:
    если вы заметили орфографическую, стилистическую или другую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter. Выделенный текст будет немедленно отослан вебмастеру, а Вы даже ничего и не заметите — настолько быстро все произойдет.

На заметку:
    если вы уже вскипели насчет дизайна этой страницы, то присмотритесь повнимательнее к названию, почитайте FAQ, сходите по лебедевским местам, как это уже предлагалось выше. Можно ли считать пародию плагиатом? Надеюсь, что нет.

Параметры этой страницы
   
GZip

Ссылки от спонсоров
   


Дмитрий Котеров | 5 марта 2004 г. ©1999-2014 | Генеральный спонсор: Хостинг «Джино» | Контакт Вернуться к оглавлению