"Хорошие" практики для реализации функции Declare()
Вступление
В настоящее время меня интересует реализация declare()
функция, которая должна позволять мне объявлять классы javascript с использованием наследования прототипа (или что-то в этом роде, потому что javascript использует другую объектную модель, а не классическую ООП). До сих пор я обнаружил некоторые проблемы, по которым я хотел бы узнать чье-то мнение и разъяснения (если это возможно).
Вот "сценарий воспроизведения" (упрощенный для воспроизведения проблем), который можно выполнить в консоли:
function namespace(){
if(arguments.length > 1){
var m, map, result;
for(m = 0; map = arguments[m], m < arguments.length; m++){
result = namespace(map);
}
return result;
}
var scope = window,
parts = arguments[0].split('.'),
part, p;
for(p = 0; part = parts[p], p < parts.length; p++){
if(typeof scope[part] === 'undefined'){
scope[part] = {};
}
scope = scope[part];
}
return scope;
}
function inherit(child, parent){
child.prototype = Object.create(parent);
child.prototype.constructor = child;
child.prototype.$parent = parent.prototype;
}
function mixin(target, source){
var value;
target = target || {};
if(typeof source == 'object'){
for(var property in source){
target[property] = source[property];
}
}
return target;
}
function extend(){
var mixins = Array.prototype.slice.call(arguments, 0),
object = mixins.shift() || {},
length = mixins.length,
m, mixin;
for(m = 0; mixin = mixins[m], m < length; mixin(object, mixin), m++);
return object;
}
function declare(config){
var map = config.object.split('.'),
name = map.pop(),
ns = namespace(map.join('.'));
ns[name] = function(){
this.constructor.apply(this, arguments);
};
if(config.parent){
if(typeof config.parent == 'string'){
config.parent = namespace(config.parent);
}
inherit(ns[name], config.parent);
}
if(config.mixins){
extend.apply(null, [ ns[name].prototype ].concat(config.mixins));
}
if(config.definition){
mixin(ns[name].prototype, config.definition);
}
}
declare({
object: 'Test.A',
definition: {
constructor: function(){
this.a = 1;
},
test: function(){
return this.a;
}
}
});
declare({
object: 'Test.B',
parent: 'Test.A',
definition: {
constructor: function(){
this.$parent.constructor.call(this);
this.b = 1;
},
test: function(){
return this.$parent.test.call(this) + this.b;
}
}
});
declare({
object: 'Test.C',
definition: {
x: 1
}
});
var a = new Test.A(),
b = new Test.B();
console.log('a.test() = ' + a.test());
console.log('b.test() = ' + b.test());
// var c = new Test.C();
Концепция
declare()
следует объединить функциональность extend()
, inherit()
а также mixin()
функции. В качестве аргумента требуется config
объект со следующими разделами:
- object - имя класса объекта (обязательно);
- parent - имя класса объекта для наследования (не обязательно);
- mixins - объекты / классы, свойства и метод которых необходимо включить в прототип результирующего класса / объекта (не обязательно);
- определение - свойства и методы прототипа класса результата.
Проблемы
Вопрос № 1 о конструкторе: если config.definition
приходит без constructor
метод, то я получаю RangeError: Maximum call stack size exceeded
ошибка, которая означает, что моя "временная" функция конструктора
ns[name] = function(){
this.constructor.apply(this, arguments);
};
Начал называть себя в бесконечном цикле. Для воспроизведения вы можете раскомментировать var c = new Test.C();
линия.
Вопросы: я должен проверить config.definition
на constructor
метод существования и вставка пустой функции, где нет constructor
метод, указанный, чтобы избежать этого? Есть ли другие возможные подходы без существенного влияния на производительность?
Проблема № 2 об отладке: когда я пытаюсь войти a
или же b
переменные тогда я получаю ns.(anonymous function){ ... }
в консоли, что означает, что я потерял пространство имен и имя класса / объекта при выполнении "динамического объявления".
ns[name] = function(){ ... };
Возможно, что проблема в анонимной функции без имени, поэтому браузер пытается сохранить последние символы, где происходит присвоение. Я надеялся, что есть возможность динамически создавать функцию и определять для нее имя, и нашел этот вопрос, который предлагает использовать eval();
или же new Function(...)();
,
Вопросы: есть ли возможность сохранить пространство имен и имя класса без каких-либо evUl()
магия?
Например, вот что я бы оценил:
namespace('X.Y');
X.Y.Z = function(){ this.a = 1 };
var test = new X.Y.Z();
console.log(test);
Показывает:
X.Y.Z {a: 1}
^^^^^
Literaly, what I want to achieve.
Я действительно ценю ваши усилия по оказанию помощи. Благодарю.
1 ответ
Должен ли я проверить config.definition на существование метода конструктора и внедрить пустую функцию, где не указан метод конструктора, чтобы избежать этого? Есть ли другие возможные подходы без существенного влияния на производительность?
Да, внедрение пустой функции в качестве конструктора фактически уменьшит влияние на производительность.
И вместо function(){this.constructor.apply(this, arguments);}
обертка, вы должны просто использовать сам конструктор (если вы не уверены, что он не возвращает объект):
ns[name] = config.definition && config.definition.constructor || function(){};
Есть ли возможность сохранить пространство имен и имя класса без какой-либо магии eval()?
Нет. То, что ваш отладчик / инспектор использует здесь для описания экземпляров, является .name
функции конструктора. Вы не можете установить другое с помощью именованной функции, и они не могут содержать точек в своих именах.
№ 3 проблема ваша inherits
функция. Вместо
child.prototype = Object.create(parent);
это должно быть
child.prototype = Object.create(parent.prototype);