Продвинутый JavaScript 1. Объектно-ориентированное программирование

Статические свойства и статические методы

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

Переменные, объявленные свойствами функции-конструктора, называются статическими свойствами, а методы функции-конструктора — статическими методами. Такие свойства и методы удобны для хранения каких-либо значений, либо осуществления каких-либо действий, общих для всех объектов.

function Connection() {
    this.isOpened = false;
    this.isPermanent = false;
    Connection.count++;
}
Connection.count = 0;
Connection.server = "localhost";
Connection.dropConnections = function() {
    this.count = 0;
};

Приведенный выше код автоматически подсчитывает количество созданных объектов-соединений, а с помощью статического метода можно это значение сбросить.

Фабричные методы

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

function Vector (x, y) {
    this.x = x;
    this.y = y;
}

Но некоторые участки нашей программы работают с полярными координатами и нам бы хотелось создавать новые вектора в них. Т.к. и в этом случае конструктору так же передавалось бы два числа, то реализовать пересчет координат на основании типов переданных аргументов не получилось бы. Единственный способ решить эту проблему — добавление ещё одного аргумента, в котором бы передавался тип передаваемых координат:

function Vector (a, b, type) {
    if (type == "decartes") {
    // a -> x , b -> y
        this.x = a;
        this.y = b;
    } else {
    // a -> r , b -> theta
        this.x = a * Math.cos(b);
        this.y = a * Math.sin(b);
    }
}

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

Vector.createFromPolar = function (r , theta) {
    var x = r * Math.cos(theta);
    var y = r * Math.sin(theta);
    var vector = new Vector(x, y);
    return vector;
}

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

Метод call

Благодаря тому, что ключевое слово this резольвится в момент выполнения функции, а не заранее, даже в обычной функции, не являющейся методом какого-либо объекта, можно использовать this. Но для использования этой функции в контексте определенного объекта приходилось копировать функцию в метод этого объекта:

var changeConnection = function(newOpened, newPermanent) {
    this.isOpened = newOpened;
    this.isPermanent = newPermanent;
};

var myConnection = new Connection;
myConnection.change = changeConnection;

Можно обойтись без копирования; воспользовавшись методом call, можно выполнить функцию, явно указав ей контекст выполнения:

changeConnection.call(myConnection, true, true);

«Одалживание метода»

Благодаря тому, что функции, как и любые объекты, могут копироваться в другие переменные и методы, мы можем добавлять ("одалживать") методы от одних объектов другим. Например, нам нужно объединить все аргументы функции в строку и вернуть её:

function toStr() {
    return arguments.join('->');
}
toStr(1,2,3); // ошибка!

Т.к. arguments не является массивом, то у него нет метода join и потому такая функция работать не будет. Зато такой метод есть у "настоящего" массива и его можно скопировать в arguments. Такое копирование называется "одалживанием".

function toStr() {
    arguments.join = [].join;
    return arguments.join('->');
}
toStr(1,2,3); // 1->2->3

Это работает, т.к. в своем внутреннем устройстве join производит перебор значений массива, т.е. оперирует индексами и length, а это всё есть и у arguments.

Такое копирование не всегда безопасно, т.к. у объекта-"должника" может быть свой метод с таким же именем и специфической реализацией, и этот метод будет потерян. Потому безопаснее использовать call

function toStr() {
    return [].join.call(arguments, '->');
}
toStr(1,2,3); // 1->2->3

Метод apply

Метод apply аналогичен методу call, единственное отличие: call принимает параметры для передачи в исполняемую функцию по отдельности, а apply — в виде массива.

changeConnection.call(myConnection, true, true);
changeConnection.apply(myConnection, [true, true]);

Apply удобно использовать, когда количество передаваемых параметров заранее не извесно, и потому проще передать их массивом, чем по отдельности прописывать в call.

Apply так же может быть полезен в следующем случае:

var ages = [19, 23, 22, 25];
console.log( Math.max.apply(null, ages) );

Сам по себе массив не имеет метода для нахождения максимального значения, зато такой метод есть у объекта Math. В качестве контекста выполнения можно указать что угодно, т.к. контекст при поиске максимума методом max не учитывается. Max принимает на вход список аргументов, а не массив, но благодаря способности apply "разворачивать" массивы в список аргументов такая конструкция работает.

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

У особенности использования this есть обратная сторона: при некоторых условиях контекст может потеряться и тогда функция не будет работать так, как мы ожидаем.

var connect = {
    isOpen: true,
    close: function () {
        this.isOpen = false;
    }
};

setTimeout( connect.close, 1000);

В setTimeout передалась функция для выполнения, но не передался контекст её выполнения.

Вариантов решения этой проблемы несколько. Во-первых, можно добавить функцию-обертку внутрь setTimeout:

var connect = {
    isOpen: true,
    close: function () {
        this.isOpen = false;
    }
};

setTimeout( function() {
    connect.close();
}, 1000);

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

function bind(f, context) {
    return function () {
        return f.apply(context, arguments);
    };
}

var connect = {
    isOpen: true,
    close: function () {
        this.isOpen = false;
    }
};

setTimeout( bind(connect.close, connect), 1000);

Если используется любой современный браузер или IE>8, то можно воспрользоваться встроенным методом bind. Он работает так же, как и созданный вручную в предыдущем примере:

setTimeout( connect.close.bind(connect), 1000);

Функции-обёртки, декораторы

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

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

var stupidF = function () {
    var n = 0;
    for (var i = 0; i < 1e8; i++) {
        n = i + 1;
    }
};

var totalTime = 0;
var decorator = function (f) {
    return function() {
        var startTime = Date.now();
        var result = f.apply(this, arguments);
        totalTime += Date.now() - startTime;
        return result;
    }
}

stupidF = decorator(stupidF);

stupidF();
stupidF();
console.log(totalTime);

Другой пример использования декоратора — форматирование вывода функции:

var multiply = function (a, b) {
    return a * b;
};

var decorOutput = function (f) {
    return function() {
        var result = f.apply(this, arguments);
        result = parseFloat( result.toFixed(2) );
        result = result.toString().replace('.', ',');
        return result;
    }
}

multiply = decorOutput(multiply);

multiply(.22, .33);// 0,07
multiply(.44, .33);// 0,15

Таким образом можно модифицировать поведение функции, не меняя код самой функции. Можно применять несколько декораторов последовательно.