суббота, 11 апреля 2015 г.

Методы в JavaScript

Когда-нибудь каждый JavaScript-программист осознаёт, что JS — объектно-ориентированный язык. И здесь его подстерегают некоторые опасности, происходящие от непонимания того факта, что JS — язык не классов (как Паскаль или Цэ-два-креста), а прототипов.
Так, уже многое написано о проблеме наследования (котого в JS нет). Я же постараюсь рассказать о менее освещённом, но едва ли не более важном подводном камне: грамотной реализации методов.
Программисты пытаются объявлять классы в привычной для них форме, из-за чего возникают утечки памяти и прочие неприятные вещи. На самом деле нужно всего лишь научиться использовать прототипы.
Эта статья предназначена прежде всего для начинающих JS-программистов.

Ниже я буду использовать понятие «класс» в том смысле, в каком оно понимается в Паскале или Цэ-двух-крестах; хоть в JS таких классов, вообще говоря, нет, однако кое-что весьма сходно по форме и смыслу.
С самого начала всем становятся известны две базовые вещи:
класс описывается функцией-конструктором;
методы являются свойствами-функциями.

Поэтому программисты начинают писать весьма естественно:

  
    function Test(){
        // объявляем и инициализируем свойства
        this.x=5;
        this.y=3;
        // объявляем методы
        this.sum=function(){
            return this.x+this.y;
        }
        // выполняем иные конструктивные действия
        alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
    }

После этого вроде бы получается то, что мы и хотели: получается класс Test с двумя свойствами x (изначально 5) и y (изначально 3) и методом sum, вычисляющим сумму x и y. При конструировании выводится элёт с иксом, игреком и суммой. Но что происходит на самом деле? При конструировании объекта Test каждый раз вызывается функция Test. И каждый раз она создаёт новую анонимную функцию и присваивает её свойству sum! В результате в каждом объекте создаётся свой, отдельный метод sum. Если мы создадим сто объектов Test — получим где-то в памяти сто функций sum. Очевидно, так делать нельзя. И важно это осознать как можно скорее. После понимания этого факта начинающие программисты часто поступают следующим образом: создают отдельно функцию sum, а в конструкторе её присваивают свойству:

  function Test(){
        // объявляем и инициализируем свойства
        this.x=5;
        this.y=3;
        // прикручиваем методы
        this.sum=Test_sum;
        // выполняем иные конструктивные действия
        alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
    }
    
    // реализуем методы
    function Test_sum(){
        return this.x+this.y;
    }


В результате, действительно, функция Test_sum создаётся только один раз, а при каждом конструировании нового объекта Test создаётся только ссылка sum. В то же время это малограмотный вариант. Всё можно сделать гораздо красивее и правильнее, используя самую основу JavaScript: прототипы:
 function Test(){
        // объявляем и инициализируем свойства
        this.x=5;
        this.y=3;
        // выполняем иные конструктивные действия
        alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
    }
    
    // объявляем методы
    Test.prototype.sum=function(){
        return this.x+this.y;
    }
Мы создаём свойство sum не класса Test, а его прототипа. Поэтому у каждого объекта Test будет функция sum. Собственно, на то он и прототип, чтобы описывать вещи, которые есть у каждого объекта. Более того, обычные, не функциональные, свойства тоже было бы логично загнать в прототип:
 function Test(){
        // выполняем иные конструктивные действия
        alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
    }
    
    // объявляем, инициализируем, реализуем свойства и методы
    Test.prototype.x=5;
    Test.prototype.y=3;
    Test.prototype.sum=function(){
        return this.x+this.y;
    }


Плохо здесь то, что объявления свойств и методов идут после их использования в конструкторе. Но с этим придётся смириться… Ещё здесь неприятно многократное повторение Test.prototype. С какой-то точки зрения, было бы неплохо вспомнить, что JS — это не Цэ-два-креста, и у нас есть предложение with. С другой стороны, многие авторитетные люди не рекомендуют использовать with вообще. Поэтому нижеследующие варианты использовать не следует. Буквально сразу же нас подстерегает неприятный сюрприз: этот код не работает.
  function Test(){
        // выполняем иные конструктивные действия
        alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
    }
    
    // объявляем, инициализируем, реализуем свойства и методы
    with(Test.prototype){
        x=5;
        y=3;
        sum=function(){
            return this.x+this.y;
        }
    }
Почему не работает — в некотором роде загадка. Как ни крути, а слово prototype придётся повторять:
  function Test(){
        // выполняем иные конструктивные действия
        alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
    }
    
    // объявляем, инициализируем, реализуем свойства и методы
    with(Test){
        prototype.x=5;
        prototype.y=3;
        prototype.sum=function(){
            return this.x+this.y;
        }
    }
Преимущество здесь в группировании объявлений всей начинки класса Test в один блок — за исключением остающегося осторонь конструктора. Но и с этим можно справиться, если вспомнить, что функцию можно объявить через минимум три синтаксиса:
   with(Test=function(){
        // выполняем иные конструктивные действия
        alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
    }){
        // объявляем и инициализируем свойства
        prototype.x=5;
        prototype.y=3;
        // объявляем методы
        prototype.sum=function(){
            return this.x+this.y;
        }
    }
В результате получается почти та естественная запись, с которой мы начали, разве что слово this заменили на prototype; ну и переместили в начало «иные конструктивные действия» — как я уже сказал, с этим, к сожалению, придётся смириться. Впрочем, если от конструктора ничего, кроме создания свойств и методов, не требуется, получается и вовсе красота:
 with(Test=new Function){
        // объявляем и инициализируем свойства
        prototype.x=5;
        prototype.y=3;
        // объявляем методы
        prototype.sum=function(){
            return this.x+this.y;
        }
    }
Однако не будем забывать, что предложение with использовать не рекомендуется. Поэтому в итоге остановимся на третьем варианте объявления.
тест