抽象的
我正在开发一个使用 Angular 作为客户端框架的应用程序,Angular 目前很稳定,我很高兴使用它,尽管现在我发现我经常复制和粘贴我想组织到类层次结构中的代码. 例如,对话框共享一组通用功能,它们需要打开、关闭,提供typeahead
功能的代码也是从某些父 BaseTypeaheadClass 继承的第一个候选对象,尽管我在 Angular 中找不到的一件事是标准的组织这些层次结构。控制器,服务,提供者都使用普通的javascript函数,可以通过 扩展prototype
,所以我的问题是:
问题
组织我的类函数的角度方式是什么,是否有任何标准机制允许从另一个类派生一个类
附言
我对这个问题的猜测:
- 将基类的实现定义为服务,因此它们将很容易地注入到需要该特定类的任何控制器或其他服务中
- 定义服务并提供用于创建基类/派生类的
OOP
方法,例如define
,等derive
编辑
从我最初提出问题的时间开始,已经过去了一段时间。从那以后,我提出了我在几个项目中成功使用的方法,我非常喜欢并希望与大家分享。
目前 Angular 没有提供任何用于组织类层次结构的结构,很遗憾,因为或多或少的大型应用程序仅能满足 Model/View/Controller/... 结构,它必须将其代码组织成 OOP 对象。
我已经在 Web 开发领域工作了很长时间,我什至还没有看到一个企业项目大量利用 JavaScript 的 OOP。我看到的是巨大且组织良好的服务器端/数据库端逻辑+接近于无限的javascript意大利面条,在客户端上涂满了框架和库的动物园。
没有任何 MVVM、MVP 框架(如 knockout.js、backbone、other...)能够替代 OOP。如果你没有使用诸如类、对象、继承、抽象、多态等面向编程的核心原则,你就会陷入大麻烦,你最终会得到一个超长的 javascript 意大利面条。
关于 Angular,我认为它是一个与 knockout.js/backbone.js/任何其他 MVV-anything 框架非常不同的框架,但根据我的实践,它也不是能够替代 OOP 的灵丹妙药。当我试图不将 OOP 与 Angular 一起使用时,我最终会得到主要位于控制器中的重复逻辑。不幸的是,没有(我发现没有)干净和有角度的方式来解决这个问题。
但我已经成功(我认为)解决了这个问题。
我使用了刚刚实现的紧凑、零依赖库John Resig's Simple JavaScript Inheritance
(https://github.com/tracker1/core-js/blob/master/js-extensions/040-Class.js)。在该库的帮助下,我能够创建/继承/创建抽象方法/覆盖它们,换句话说,我可以在服务器端完成我已经习惯的所有事情。
这是一个示例用法:
Application.factory('SomeChildObject', ['$http', 'SomeParentClass', function ($http, SomeParentClass) {
var SomeChildClass = SomeParentClass.extend({
init: function() { // Constructor
this._super.init(123, 231); // call base constructor
},
someFunction: function() {
// Notice that your OOP now knows everything that can be injected into angular service, which is pretty cool :)
$http({method: 'GET', url: '/someUrl'}).then(function(){
this._super.someFunction(); // call base function implementation
});
}
});
// return new SomeChildClass(); // We are not returning instance here!
return SomeChildClass; // Service is a function definition not an instance of an object
}]);
// So now we can both use this service in angular and have the ability to extend it using the `extend` method call, like so:
Application.controller('MegaController', ['$scope', 'SomeChildClass', function ($scope, SomeChildClass) {
$scope.someObject = new SomeChildClass();
}]);
OOP + Angular 配合得非常好,在 Angular 上下文下创建的对象可以通过服务自动利用依赖注入,因此您不必将实例注入您的 OOP 构造函数,这一事实使您的 OOP 层次结构非常纤细并且没有不相关的东西需要(并且是)由 angular.js 处理
因此,请使用这种方法并在此处反馈您获得的结果或遇到的问题,
另一个编辑
最近我在原来的 Class.js 实现中遇到了一些问题,如下:
1) 如果您将对实例方法的引用作为对其他方法的回调传递,那么这些方法的工作方式可能与您期望的方式不同。他们将失去对this
. 在这种情况下,您将期望在内部看到当前对象,this
但它可能是顶级Window
对象或其他上下文对象,具体取决于回调如何调用您的方法。这是由于 JavaScript 架构而发生的。为了解决这个问题ClassMember
,提供了一个特殊的函数,该函数指示Class
在创建方法时将其绑定到对象上下文(请查看Usage
下面的进一步指导)。
2)显然原始Class.js
实现对控制器方法声明的角度类型一无所知,即
Class.extend('YourClassDisplayName', {
ctor: function () {
// Some useful constructor logic
},
controller: ['$scope', '$attrs', function ($scope, $attrs) {
// Do something with $scope and $attrs
}]
});
当前实现理解上述语法
3)如果在没有适当处理的情况下使用上述方法,它会破坏 angular $$annotate
'on 过程,因此参考上述示例将无法注入$scope
和$attrs
进入ClassMember
方法,或使用this.base(...)
调用的覆盖方法。所以这也是固定的。
陷阱:
1)this.base(...)
在异步操作处理程序(类似$http.get(..., function() { self.base(...); })
)中使用时,请注意this.base(...)
调用的生命周期有限,并且只要方法返回就this.base(...)
停止存在。因此,如果您打算以异步方式调用基本方法,则应显式保存对基本方法的引用。IE:
...
var self = this;
var base = this.base;
...
$http.get(..., function () {
base.call(self, ...); // or base.apply(self, ...), or base() if you don't care about `this`
})
我已经解决了以上所有问题(除了一个由于 JavaScript 架构而无法解决的问题),并希望与大家分享,希望您能从中受益:
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*
* Inspired by base2 and Prototype
* Angular adaptations by Denis Yaremov http://github.com/lu4
* Usage:
---------------------------------
var X = Class.extend('X', {
ctor: function () {
this.name = "I'm X";
},
myOrdinaryMethod: function (x, y, z) {
console.log([this.name, x, y, z]);
},
myClassMemberMethod: ClassMember(function (x, y, z) {
console.log([this.name, x, y, z]);
})
});
var Y = Class.extend('Y', {
ctor: function () {
this.name = "I'm Y";
},
myOrdinaryMethod: function (x, y, z) {
console.log([this.name, x, y, z]);
},
myClassMemberMethod: ClassMember(function (x, y, z) {
console.log([this.name, x, y, z]);
})
});
var x = new X();
var y = new Y();
x.myClassMemberMethod('a', 'b', 'c'); // ["I'm X", "a", "b", "c"]
y.myClassMemberMethod('u', 'v', 'm'); // ["I'm Y", "u", "v", "m"]
x.myOrdinaryMethod('a', 'b', 'c'); // ["I'm X", "a", "b", "c"]
y.myOrdinaryMethod('u', 'v', 'm'); // ["I'm Y", "u", "v", "m"]
y.theirOrdinaryMethod = x.myOrdinaryMethod;
y.theirClassMemberMethod = x.myClassMemberMethod;
y.theirOrdinaryMethod('a', 'b', 'c'); // ["I'm Y", "a", "b", "c"]
y.theirClassMemberMethod('u', 'v', 'm'); // ["I'm X", "u", "v", "m"]
*/
angular.module('app').factory('ClassMember', function () {
return function ClassMember(fn) {
if (this instanceof ClassMember) {
this.fn = fn;
} else {
return new ClassMember(fn);
}
};
});
angular.module('app').factory('Class', function (ClassMember) {
var runtime = { initializing: false },
fnTest = /xyz/.test(function() { xyz; }) ? /\bbase\b/ : /.*/,
FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m,
STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var toString = Object.prototype.toString;
// The base Class implementation (does nothing)
function Class() { };
Class.members = { };
// Create a new Class that inherits from this class
Class.extend = function extend(displayName, properties) {
var array;
var targetMembers = {};
var sourceMembers = this.members;
for (var memberName in sourceMembers) {
if (sourceMembers.hasOwnProperty(memberName)) {
targetMembers[memberName] = sourceMembers[memberName];
}
}
var base = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the ctor constructor)
runtime.initializing = true;
var prototype = new this();
runtime.initializing = false;
// Copy the properties over onto the new prototype
for (var name in properties) {
if (properties.hasOwnProperty(name)) {
// Check if we're overwriting an existing function
var property = properties[name];
// Support angular's controller/service/factory declaration notation
if (toString.call(property) === '[object Array]') {
array = property;
var item = array[array.length - 1];
if (toString.call(item) === '[object Function]' || item instanceof ClassMember) {
property = array[array.length - 1];
} else {
array = null;
}
} else {
array = null;
}
var isClassMember = property instanceof ClassMember;
if (isClassMember) {
property = property.fn;
}
if (typeof property === "function") {
if (typeof base[name] === "function" && fnTest.test(property)) {
property = (function (propertyName, fn) {
var args = fn.toString().replace(STRIP_COMMENTS, '').match(FN_ARGS)[1];
return (new Function('propertyName', 'fn', 'base', 'return function (' + args + ') {\n\
var prevBase = this.base;\n\
var hasBase = "base" in this;\n\
\n\
// Add a new .base() method that is the same method\n\
// but on the super-class\n\
\n\
this.base = base[propertyName];\n\
\n\
// The method only need to be bound temporarily, so we\n\
// remove it when we\'re done executing\n\
var ret = fn.call(this' + (!!args ? (', ' + args) : args) + ');\n\
\n\
if (hasBase) {\n\
this.base = prevBase;\n\
} else {\n\
delete this["base"];\n\
}\n\
return ret;\n\
}'))(propertyName, fn, base);
})(name, property);
}
if (isClassMember) {
targetMembers[name] = property;
} else if (name in targetMembers) {
delete targetMembers[name];
}
if (array) {
array[array.length - 1] = property;
property = array;
}
prototype[name] = property;
} else {
prototype[name] = property;
}
}
}
var membersArray = [];
for (var i in targetMembers) {
if (targetMembers.hasOwnProperty(i)) {
membersArray.push({ name: i, fn: targetMembers[i] });
}
}
// All construction is actually done in the ctor method
var ChildClass = (new Function("runtime", "members", "FN_ARGS", "STRIP_COMMENTS", "return function " + (displayName || "Class") + "() {\n\
if (!runtime.initializing && this.ctor)\n\
{\n\
var length = members.length;\n\
for (var i = 0; i < length; i++)\n\
{\n\
var item = members[i];\n\
this[item.name] = (function (me, fn) {\n\
var args = fn.toString().replace(STRIP_COMMENTS, '').match(FN_ARGS)[1];\n\
return args ? (new Function('me', 'fn', 'return function (' + args + ') { return fn.call(me, ' + args + '); }'))(me, fn) : function () { return fn.call(me); };\n\
})(this, item.fn);\n\
\n\
}\n\
this.ctor.apply(this, arguments);\n\
}\n\
}"))(runtime, membersArray, FN_ARGS, STRIP_COMMENTS);
ChildClass.members = targetMembers;
// Populate our constructed prototype object
ChildClass.prototype = prototype;
// Enforce the constructor to be what we expect
ChildClass.prototype.constructor = ChildClass;
// And make this class extendable
ChildClass.extend = extend;
return ChildClass;
};
return Class;
});
另一个编辑
最终,我偶然发现了另一个与原始 John Resig 的实现相关的问题,该问题与使用 Function.prototype.toString() 和一些正则表达式的 angular 的注释过程(用于依赖注入)有关目的是提取依赖项的名称。原始实现的问题在于它不期望这样,因此您无法声明接受依赖项的方法,因此我对实现进行了一些调整以处理先前描述的问题,这里是:
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*
* Inspired by base2 and Prototype
* Angular adaptations by Denis Yaremov http://github.com/lu4
* Usage:
---------------------------------
var X = Class.extend('X', {
ctor: function () {
this.name = "I'm X";
},
myOrdinaryMethod: function (x, y, z) {
console.log([this.name, x, y, z]);
},
myClassMemberMethod: ClassMember(function (x, y, z) {
console.log([this.name, x, y, z]);
})
});
var Y = Class.extend('Y', {
ctor: function () {
this.name = "I'm Y";
},
myOrdinaryMethod: function (x, y, z) {
console.log([this.name, x, y, z]);
},
myClassMemberMethod: ClassMember(function (x, y, z) {
console.log([this.name, x, y, z]);
})
});
var x = new X();
var y = new Y();
x.myClassMemberMethod('a', 'b', 'c'); // ["I'm X", "a", "b", "c"]
y.myClassMemberMethod('u', 'v', 'm'); // ["I'm Y", "u", "v", "m"]
x.myOrdinaryMethod('a', 'b', 'c'); // ["I'm X", "a", "b", "c"]
y.myOrdinaryMethod('u', 'v', 'm'); // ["I'm Y", "u", "v", "m"]
y.theirOrdinaryMethod = x.myOrdinaryMethod;
y.theirClassMemberMethod = x.myClassMemberMethod;
y.theirOrdinaryMethod('a', 'b', 'c'); // ["I'm Y", "a", "b", "c"]
y.theirClassMemberMethod('u', 'v', 'm'); // ["I'm X", "u", "v", "m"]
*/
angular.module('homer').factory('Class', function () {
function ClassMember(fn) {
if (this instanceof ClassMember) {
this.fn = fn;
return this;
} else {
return new ClassMember(fn);
}
}
function ClassEvent() {
if (this instanceof ClassEvent) {
return this;
} else {
return new ClassEvent();
}
}
var runtime = { initializing: false },
fnTest = /xyz/.test(function () { xyz; }) ? /\bbase\b/ : /.*/,
fnArgs = /^function\s*[^\(]*\(\s*([^\)]*)\)/m,
stripComments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var toString = Object.prototype.toString;
// The base Class implementation (does nothing)
function Class() { };
Class.events = {};
Class.members = {};
// Create a new Class that inherits from this class
Class.extend = function Extend(displayName, properties) {
var array;
var targetEvents = {};
var sourceEvents = this.events;
var targetMembers = {};
var sourceMembers = this.members;
for (var eventName in sourceEvents) {
if (sourceEvents.hasOwnProperty(eventName)) {
targetEvents[eventName] = sourceEvents[eventName];
}
}
for (var memberName in sourceMembers) {
if (sourceMembers.hasOwnProperty(memberName)) {
targetMembers[memberName] = sourceMembers[memberName];
}
}
var base = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the ctor constructor)
runtime.initializing = true;
var prototype = new this();
runtime.initializing = false;
// Copy the properties over onto the new prototype
for (var name in properties) {
if (properties.hasOwnProperty(name)) {
// Check if we're overwriting an existing function
var property = properties[name];
// Support angular's controller/service/factory declaration notation
if (toString.call(property) === '[object Array]') {
array = property;
var item = array[array.length - 1];
if (toString.call(item) === '[object Function]' || item instanceof ClassMember) {
property = array[array.length - 1];
} else {
array = null;
}
} else {
array = null;
}
var isClassMember = property instanceof ClassMember;
if (isClassMember) {
property = property.fn;
}
var isClassEvent = property instanceof ClassEvent;
if (isClassEvent) {
property = (function() {
function Subscriber(fn) {
Subscriber.listeners.push(fn.bind(this));
};
Subscriber.listeners = [];
Subscriber.fire = function() {
var listeners = Subscriber.listeners;
for (var i = 0; i < listeners.length; i++) {
var result = listeners[i].apply(this, arguments);
if (result !== undefined) return result;
}
return void 0;
}
return Subscriber;
})();
}
if (typeof property === "function") {
if (typeof base[name] === "function" && fnTest.test(property)) {
property = (function (propertyName, fn) {
var args = fn.toString().replace(stripComments, '').match(fnArgs)[1];
return (new Function('propertyName', 'fn', 'base', 'return function (' + args + ') {\n\
var prevBase = this.base;\n\
var hasBase = "base" in this;\n\
\n\
// Add a new .base() method that is the same method\n\
// but on the super-class\n\
\n\
this.base = base[propertyName];\n\
\n\
// The method only need to be bound temporarily, so we\n\
// remove it when we\'re done executing\n\
var ret = fn.call(this' + (!!args ? (', ' + args) : args) + ');\n\
\n\
if (hasBase) {\n\
this.base = prevBase;\n\
} else {\n\
delete this["base"];\n\
}\n\
return ret;\n\
}'))(propertyName, fn, base);
})(name, property);
}
if (isClassEvent) {
targetEvents[name] = property;
} else {
delete targetEvents[name];
}
if (isClassMember) {
targetMembers[name] = property;
} else if (name in targetMembers) {
delete targetMembers[name];
}
if (array) {
array[array.length - 1] = property;
property = array;
}
prototype[name] = property;
} else {
prototype[name] = property;
}
}
}
var eventsArray = [];
for (var targetEventName in targetEvents) {
if (targetEvents.hasOwnProperty(targetEventName)) {
eventsArray.push({ name: targetEventName, fn: targetEvents[targetEventName] });
}
}
var membersArray = [];
for (var targetMemberName in targetMembers) {
if (targetMembers.hasOwnProperty(targetMemberName)) {
membersArray.push({ name: targetMemberName, fn: targetMembers[targetMemberName] });
}
}
// All construction is actually done in the ctor method
var ChildClass = (new Function("runtime", "events", "members", "FN_ARGS", "STRIP_COMMENTS", "return function " + (displayName || "Class") + "() {\n\
if (!runtime.initializing && this.ctor)\n\
{\n\
var length = members.length;\n\
var bind = function (me, $$fn$$) {\n\
var args = $$fn$$.toString().replace(STRIP_COMMENTS, '').match(FN_ARGS)[1];\n\
var result = args ? (new Function('me', '$$fn$$', 'return function (' + args + ') { return $$fn$$.apply(me, arguments); }'))(me, $$fn$$) : function () { return $$fn$$.apply(me, arguments); };\n\
return result;\n\
};\n\
for (var i = 0; i < length; i++)\n\
{\n\
var item = members[i];\n\
var fn = item.fn;\n\
var name = item.name;\n\
var property = this[name] = bind(this, fn);\n\
if (fn.fire) {\n\
property.fire = bind(this, fn.fire);\n\
}\n\
if (fn.listeners) {\n\
property.listeners = fn.listeners;\n\
}\n\
}\n\
\n\
var length = events.length;\n\
for (var i = 0; i < length; i++)\n\
{\n\
var item = events[i];\n\
var fn = item.fn;\n\
var name = item.name;\n\
var property = this[name] = bind(this, fn);\n\
if (fn.fire) {\n\
property.fire = bind(this, fn.fire);\n\
}\n\
if (fn.listeners) {\n\
property.listeners = fn.listeners;\n\
}\n\
}\n\
this.ctor.apply(this, arguments);\n\
}\n\
}"))(runtime, eventsArray, membersArray, fnArgs, stripComments);
ChildClass.members = targetMembers;
// Populate our constructed prototype object
ChildClass.prototype = prototype;
// Enforce the constructor to be what we expect
ChildClass.prototype.constructor = ChildClass;
// And make this class extendable
ChildClass.extend = Extend;
ChildClass.event = ClassEvent;
ChildClass.member = ClassMember;
return ChildClass;
};
Class.member = ClassMember;
Class.event = ClassEvent;
return Class;
});