После того, как с классическим (от слова класс) наследованием в JS стало вроде-бы все понятно, я решил разобраться с реализацией другого способа повторного использвоания кода — примесями. Несмотря на довольно непривычное название, способ этот чертовски прост.
Примесь (mixin) — это объект с набором функций, который сам по себе (отдельно от других объектов) не используется.
Вот, например, прекрасная примесь:
При попытке вызвать метод say просто так, нас ждет облом, потому что ни this.name, ни this.THOUGHTS в нашем объекте, почему-то, просто нет.
На самом деле все правильно, чтобы использовать примесь по назначению нам нужен другой объект, и метод, который копирует все свойства переданных ему объектов-примесей в прототип функции конструктора — обычно такой метод называют extend, или mix, или как-нибудь в этом духе:
Легко и не напрягаясь — допустим, у нас есть парочка примесей:
И, кому-то, возможно, уже знакомая, эволюционная цепочка:
Каждый «класс» теперь не зависит от другого, а весь повторяющийся функционал реализован с помощью примесей.
Теперь стоит все проверить — вдруг заработает:
И консоль говорит что таки да, все как надо:
Собственно все. В отличие от «классического» наследования, реализация примесей очень простая и понятная. Существуют конечно некоторые вариации, но так или иначе ядро выглядит именно так.
UPD. Как подсказал один очень хороший человек с ником lalaki, метод extend можно реализовать немножко по-другому:
С таким подходом можно на лету подменять методы в примеси, и эти изменения сразу будут доступны для всех объектов эту примесь использующих.
Очень-очень маленьким недостатком является то, что не слишком умная подсказка в среде разработки будет всегда показывать что у методов нет аргументов.
Весь код и примеры лежат себе спокойно вот тут
1. Примеси — это, собственно, что ?
Примесь (mixin) — это объект с набором функций, который сам по себе (отдельно от других объектов) не используется.
Вот, например, прекрасная примесь:
var Mixin_Babbler =
{
say: function ()
{
console.log("My name is " + this.name + " and i think:'" + this.THOUGHTS + "'");
},
argue: function() { console.log("You're totally wrong"); }
};
При попытке вызвать метод say просто так, нас ждет облом, потому что ни this.name, ни this.THOUGHTS в нашем объекте, почему-то, просто нет.
На самом деле все правильно, чтобы использовать примесь по назначению нам нужен другой объект, и метод, который копирует все свойства переданных ему объектов-примесей в прототип функции конструктора — обычно такой метод называют extend, или mix, или как-нибудь в этом духе:
function extend(object)
{
var mixins = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < mixins.length; ++i)
{
for (var prop in mixins[i])
{
if (typeof object.prototype[prop] === "undefined")
{
object.prototype[prop] = mixins[i][prop];
}
}
}
}
2. Ну и как это использовать?
Легко и не напрягаясь — допустим, у нас есть парочка примесей:
var Mixin_Babbler =
{
say: function () { console.log("My name is " + this.name + " and i think:'" + this.THOUGHTS + "'"); },
argue: function() { console.log("You're totally wrong"); }
};
var Mixin_BeverageLover =
{
drink: function () { console.log("* drinking " + this.FAVORITE_BEVERAGE + " *"); }
};
И, кому-то, возможно, уже знакомая, эволюционная цепочка:
function Man(name)
{
this.name = name;
}
Man.prototype =
{
constructor: Man,
THOUGHTS: "I like soccer"
}
extend(Man, Mixin_Babbler);
function Gentleman(name)
{
this.name = name;
}
Gentleman.prototype =
{
constructor: Gentleman,
THOUGHTS: "I like Earl Grey",
FAVORITE_BEVERAGE: "Tea"
}
extend(Gentleman, Mixin_Babbler, Mixin_BeverageLover);
function Programmer(name)
{
this.name = name;
}
Programmer.prototype =
{
constructor: Programmer,
THOUGHTS: "MVC, MVVM, MVP *___* like it!",
FAVORITE_BEVERAGE: "Cofee",
write_good_code: function () { console.log("*writing best code ever*"); this.drink(); }
}
extend(Programmer, Mixin_Babbler, Mixin_BeverageLover);
Каждый «класс» теперь не зависит от другого, а весь повторяющийся функционал реализован с помощью примесей.
Теперь стоит все проверить — вдруг заработает:
var man = new Man("Bob");
var gentleman = new Gentleman("Bill");
var programmer = new Programmer("Benjamin");
man.say();
man.argue();
gentleman.say();
gentleman.drink();
programmer.say();
programmer.write_good_code();
И консоль говорит что таки да, все как надо:
My name is Bob and i think:'I like soccer'
*You're totally wrong*
My name is Bill and i think:'I like Earl Grey'
*drinking Tea*
My name is Benjamin and i think:'MVC, MVVM, MVP like *__* it!'
*writing best code ever*
*drinking Cofee*
Собственно все. В отличие от «классического» наследования, реализация примесей очень простая и понятная. Существуют конечно некоторые вариации, но так или иначе ядро выглядит именно так.
UPD. Как подсказал один очень хороший человек с ником lalaki, метод extend можно реализовать немножко по-другому:
function extend_2(object)
{
var mixins = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < mixins.length; ++i)
{
for (var prop in mixins[i])
{
if (typeof object.prototype[prop] === "undefined")
{
bindMethod = function (mixin, prop)
{
return function () { mixin[prop].apply(this, arguments) }
}
object.prototype[prop] = bindMethod(mixins[i], prop);
}
}
}
}
С таким подходом можно на лету подменять методы в примеси, и эти изменения сразу будут доступны для всех объектов эту примесь использующих.
Очень-очень маленьким недостатком является то, что не слишком умная подсказка в среде разработки будет всегда показывать что у методов нет аргументов.
Весь код и примеры лежат себе спокойно вот тут