├── .gitignore ├── .spmignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── class.js ├── docs └── competitors.md ├── package.json └── tests ├── class-spec.js └── data ├── animal.js └── dog.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .ipr 4 | .iws 5 | *~ 6 | ~* 7 | *.diff 8 | *.patch 9 | *.bak 10 | .DS_Store 11 | Thumbs.db 12 | .project 13 | .*proj 14 | .svn/ 15 | *.swp 16 | *.pyc 17 | *.pyo 18 | _site 19 | _theme 20 | sea-modules 21 | node_modules 22 | dist/ 23 | -------------------------------------------------------------------------------- /.spmignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | docs/ 3 | dist/ 4 | .travis.yml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.10" 5 | 6 | install: 7 | - npm install spm@ninja coveralls 8 | 9 | before_script: 10 | - node_modules/spm/bin/spm-install 11 | 12 | script: 13 | - node_modules/spm/bin/spm-test 14 | 15 | after_success: 16 | - node_modules/spm/bin/spm-test --coveralls | node_modules/.bin/coveralls 17 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | --- 4 | 5 | ## 1.2.0 6 | 7 | `improved` 按照 spm@3.x 规范升级。 8 | 9 | ## 1.1.0 10 | 11 | `changed` 去除 module 信息,seajs 也已经去掉这个 API 了 [#1](https://github.com/aralejs/class/issues/1) 12 | 13 | ## 1.0.0 14 | 15 | 正式版本 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Class 2 | 3 | --- 4 | 5 | [![spm package](http://spmjs.io/badge/arale-class)](http://spmjs.io/package/arale-class) 6 | [![Build Status](https://secure.travis-ci.org/aralejs/class.png?branch=master)](https://travis-ci.org/aralejs/class) 7 | [![Coverage Status](https://coveralls.io/repos/aralejs/class/badge.png?branch=master)](https://coveralls.io/r/aralejs/class) 8 | 9 | 10 | 提供简洁的 OO 实现。 11 | 12 | --- 13 | 14 | 15 | ## 使用说明 16 | 17 | 18 | ### create `Class.create([parent], [properties])` 19 | 20 | 创建一个新类。参数 `parent` 是继承的父类,`properties` 是要混入的实例属性。 21 | 22 | 来看一个简单的例子(线上演示): 23 | 24 | ```js 25 | /* pig.js */ 26 | define(function(require, exports, module) { 27 | var Class = require('class'); 28 | 29 | var Pig = Class.create({ 30 | initialize: function(name) { 31 | this.name = name; 32 | }, 33 | 34 | talk: function() { 35 | alert('我是' + this.name); 36 | } 37 | }); 38 | 39 | module.exports = Pig; 40 | }); 41 | ``` 42 | 43 | `initialize` 属性,标明初始化方法,会在构建实例时调用。 44 | 45 | 使用 `create` 方法创建的类,拥有 `extend` 方法,可以继续创建子类: 46 | 47 | ```js 48 | /* red-pig.js */ 49 | define(function(require, exports, module) { 50 | var Pig = require('./pig'); 51 | 52 | var RedPig = Pig.extend({ 53 | initialize: function(name) { 54 | RedPig.superclass.initialize.call(this, name); 55 | }, 56 | 57 | color: '红色' 58 | }); 59 | 60 | module.exports = RedPig; 61 | }); 62 | ``` 63 | 64 | **注意**:需要在子类方法中,调用父类中的同名方法时,JavaScript 语言自身并没有提供类似 `super` 65 | 的方式来轻松实现。用 `create` 或 `extend` 方法创建类时,可以使用 `superclass.methodName` 66 | 来显式调用父类方法。之所以不提供 `super` 方法,原因有二: 67 | 68 | 1. 实现起来较麻烦。现有类库的实现方案,都不完美。 69 | 2. 在 JavaScript 编程中,调用 `super` 的需求并不多。简单通过 `superclass.methodName` 70 | 来调用已经能够满足需求,并很灵活、清晰。 71 | 72 | `properties` 参数中,除了支持用 `initialize` 来标明初始化方法,还可以用 `Implements` 73 | 来标明所创建的类需要从哪些类中混入属性: 74 | 75 | ```js 76 | /* flyable.js */ 77 | define(function(require, exports, module) { 78 | exports.fly = function() { 79 | alert('我飞起来了'); 80 | }; 81 | }); 82 | ``` 83 | 84 | ```js 85 | /* flyable-red-pig.js */ 86 | define(function(require, exports, module) { 87 | var RedPig = require('./red-pig'); 88 | var Flyable = require('./flyable'); 89 | 90 | var FlyableRedPig = RedPig.extend({ 91 | Implements: Flyable, 92 | 93 | initialize: function(name) { 94 | FlyableRedPig.superclass.initialize.call(this, name); 95 | } 96 | }); 97 | 98 | module.exports = FlyableRedPig; 99 | }); 100 | ``` 101 | 102 | **注意**:`Implements` 采用首字母大写,是因为小写的 `implements` 是 JavaScript 103 | 保留字。大写也表示其特殊性,与 MooTools 的方式一致。 104 | 105 | 除了 `Implements`, 还有一个特殊属性: 106 | 107 | - `Extends` - 用来指定继承的父类,注意只能有一个父类,不支持多继承。 108 | 109 | 110 | ### implement `SomeClass.implement(properties)` 111 | 112 | 该方法与 `Implements` 属性的功能类似。当某个类已存在,需要动态修改时,用 `implement` 113 | 方法更便捷。 114 | 115 | 116 | ```js 117 | /* flyable-red-pig-extension.js */ 118 | define(function(require, exports, module) { 119 | var FlyableRedPig = require('./flyable-red-pig'); 120 | 121 | FlyableRedPig.implement({ 122 | swim: function() { 123 | alert('我还会游泳'); 124 | } 125 | }); 126 | }); 127 | ``` 128 | 129 | 这样,我们得到了会说话、会飞、还会游泳的飞天红猪侠: 130 | 131 | ```js 132 | /* test.js */ 133 | define(function(require, exports, module) { 134 | var FlyableRedPig = require('./flyable-red-pig'); 135 | require('./flyable-red-pig-extension'); 136 | 137 | var pig = new FlyableRedPig('飞天红猪侠'); 138 | pig.talk(); // alerts '我是飞天红猪侠' 139 | pig.fly(); // alerts '我飞起来了' 140 | pig.swim(); // alerts '我还会游泳' 141 | }); 142 | ``` 143 | 144 | 145 | ### extend `SomeClass.extend(properties)` 146 | 147 | 由 `Class.create` 创建的类,自动具有 `extend` 方法,功能与 `Class.create` 148 | 完全一样,只是继承的父类是 `SomeClass` 自身,前面的例子中已说明,不赘述。 149 | 150 | 151 | ### Class `Class(fn)` 152 | 153 | 将已经存在的 function 函数转换为 Class 类: 154 | 155 | ```js 156 | function Animal() { 157 | } 158 | Animal.prototype.talk = function() {}; 159 | 160 | var Dog = Class(Animal).extend({ 161 | swim: function() {} 162 | }); 163 | ``` 164 | 165 | 166 | ## 性能对比 167 | 168 | - 169 | 170 | -------------------------------------------------------------------------------- /class.js: -------------------------------------------------------------------------------- 1 | // Class 2 | // ----------------- 3 | // Thanks to: 4 | // - http://mootools.net/docs/core/Class/Class 5 | // - http://ejohn.org/blog/simple-javascript-inheritance/ 6 | // - https://github.com/ded/klass 7 | // - http://documentcloud.github.com/backbone/#Model-extend 8 | // - https://github.com/joyent/node/blob/master/lib/util.js 9 | // - https://github.com/kissyteam/kissy/blob/master/src/seed/src/kissy.js 10 | 11 | 12 | // The base Class implementation. 13 | function Class(o) { 14 | // Convert existed function to Class. 15 | if (!(this instanceof Class) && isFunction(o)) { 16 | return classify(o) 17 | } 18 | } 19 | 20 | module.exports = Class 21 | 22 | 23 | // Create a new Class. 24 | // 25 | // var SuperPig = Class.create({ 26 | // Extends: Animal, 27 | // Implements: Flyable, 28 | // initialize: function() { 29 | // SuperPig.superclass.initialize.apply(this, arguments) 30 | // }, 31 | // Statics: { 32 | // COLOR: 'red' 33 | // } 34 | // }) 35 | // 36 | Class.create = function(parent, properties) { 37 | if (!isFunction(parent)) { 38 | properties = parent 39 | parent = null 40 | } 41 | 42 | properties || (properties = {}) 43 | parent || (parent = properties.Extends || Class) 44 | properties.Extends = parent 45 | 46 | // The created class constructor 47 | function SubClass() { 48 | // Call the parent constructor. 49 | parent.apply(this, arguments) 50 | 51 | // Only call initialize in self constructor. 52 | if (this.constructor === SubClass && this.initialize) { 53 | this.initialize.apply(this, arguments) 54 | } 55 | } 56 | 57 | // Inherit class (static) properties from parent. 58 | if (parent !== Class) { 59 | mix(SubClass, parent, parent.StaticsWhiteList) 60 | } 61 | 62 | // Add instance properties to the subclass. 63 | implement.call(SubClass, properties) 64 | 65 | // Make subclass extendable. 66 | return classify(SubClass) 67 | } 68 | 69 | 70 | function implement(properties) { 71 | var key, value 72 | 73 | for (key in properties) { 74 | value = properties[key] 75 | 76 | if (Class.Mutators.hasOwnProperty(key)) { 77 | Class.Mutators[key].call(this, value) 78 | } else { 79 | this.prototype[key] = value 80 | } 81 | } 82 | } 83 | 84 | 85 | // Create a sub Class based on `Class`. 86 | Class.extend = function(properties) { 87 | properties || (properties = {}) 88 | properties.Extends = this 89 | 90 | return Class.create(properties) 91 | } 92 | 93 | 94 | function classify(cls) { 95 | cls.extend = Class.extend 96 | cls.implement = implement 97 | return cls 98 | } 99 | 100 | 101 | // Mutators define special properties. 102 | Class.Mutators = { 103 | 104 | 'Extends': function(parent) { 105 | var existed = this.prototype 106 | var proto = createProto(parent.prototype) 107 | 108 | // Keep existed properties. 109 | mix(proto, existed) 110 | 111 | // Enforce the constructor to be what we expect. 112 | proto.constructor = this 113 | 114 | // Set the prototype chain to inherit from `parent`. 115 | this.prototype = proto 116 | 117 | // Set a convenience property in case the parent's prototype is 118 | // needed later. 119 | this.superclass = parent.prototype 120 | }, 121 | 122 | 'Implements': function(items) { 123 | isArray(items) || (items = [items]) 124 | var proto = this.prototype, item 125 | 126 | while (item = items.shift()) { 127 | mix(proto, item.prototype || item) 128 | } 129 | }, 130 | 131 | 'Statics': function(staticProperties) { 132 | mix(this, staticProperties) 133 | } 134 | } 135 | 136 | 137 | // Shared empty constructor function to aid in prototype-chain creation. 138 | function Ctor() { 139 | } 140 | 141 | // See: http://jsperf.com/object-create-vs-new-ctor 142 | var createProto = Object.__proto__ ? 143 | function(proto) { 144 | return { __proto__: proto } 145 | } : 146 | function(proto) { 147 | Ctor.prototype = proto 148 | return new Ctor() 149 | } 150 | 151 | 152 | // Helpers 153 | // ------------ 154 | 155 | function mix(r, s, wl) { 156 | // Copy "all" properties including inherited ones. 157 | for (var p in s) { 158 | if (s.hasOwnProperty(p)) { 159 | if (wl && indexOf(wl, p) === -1) continue 160 | 161 | // 在 iPhone 1 代等设备的 Safari 中,prototype 也会被枚举出来,需排除 162 | if (p !== 'prototype') { 163 | r[p] = s[p] 164 | } 165 | } 166 | } 167 | } 168 | 169 | 170 | var toString = Object.prototype.toString 171 | 172 | var isArray = Array.isArray || function(val) { 173 | return toString.call(val) === '[object Array]' 174 | } 175 | 176 | var isFunction = function(val) { 177 | return toString.call(val) === '[object Function]' 178 | } 179 | 180 | var indexOf = Array.prototype.indexOf ? 181 | function(arr, item) { 182 | return arr.indexOf(item) 183 | } : 184 | function(arr, item) { 185 | for (var i = 0, len = arr.length; i < len; i++) { 186 | if (arr[i] === item) { 187 | return i 188 | } 189 | } 190 | return -1 191 | } 192 | -------------------------------------------------------------------------------- /docs/competitors.md: -------------------------------------------------------------------------------- 1 | # OO 模拟那些事儿 2 | 3 | --- 4 | 5 | > 感谢所有老前辈们,感谢所有同类代码。因为有了你们,世界才丰富多采。 6 | 7 | 8 | ## Douglas Crockford 的尝试与悟道 9 | 10 | 关于类继承,Douglas 有一篇经典文章: 11 | 12 | - [Classical Inheritance in JavaScript](http://javascript.crockford.com/inheritance.html) 13 | 14 | 这篇文章里,老道分析了为什么要在 JavaScript 里模拟类继承:主要目的是复用。老道的实现方式是给 15 | `Function.prototype` 增加 `method` 和 `inherits` 两个方法,并提供了 `uber` 语法糖。 16 | 17 | 悲催的是,大神实现的 `inherits` 和 `uber` 存在不少缺陷,国内和国外都有不少人剖析过,可以参考 18 | 2008 年时的一篇讨论帖子: 19 | 20 | - [Crockford uber 方法中的陷阱](http://www.iteye.com/topic/248933) 21 | 22 | 老道最后悟出了一段经常被引用的话: 23 | 24 | > 我编写 JavaScript 已经 8 个年头了,从来没有一次觉得需要使用 uber 方法。在类模式中,super 25 | 的概念相当重要;但是在原型和函数式模式中,super 的概念看起来是不必要的。现在回顾起来,我早期在 26 | JavaScript 中支持类模型的尝试是一个错误。 27 | 28 | 由此,老道又写了一篇经典文章,推崇在 JavaScript 里,直接使用原型继承: 29 | 30 | - [Prototypal Inheritance in JavaScript](http://javascript.crockford.com/prototypal.html) 31 | 32 | 继续悲催的是,老道的实现,依旧有不足之处。可以参考两篇博文: 33 | 34 | - [Debunking object\(\)](http://www.nczonline.net/blog/2006/10/14/debunking-object/) 35 | - [Javascript – How Prototypal Inheritance really works](http://blog.vjeux.com/2011/javascript/how-prototypal-inheritance-really-works.html) 36 | 37 | 老道的代码虽然不尽完美,但老道的尝试和对原型继承的呼吁依旧非常值得我们尊敬和思考。 38 | 39 | 研究到此,如果大家都能理解原型继承,问题其实已经终结,特别是在 webkit 等支持 `__proto__` 的运行环境下。比如: 40 | 41 | ```js 42 | function Animal() {} 43 | function Dog() {} 44 | 45 | // 要让 Dog 继承 Animal, 只需: 46 | Dog.prototype.__proto__ == Animal.prototype; 47 | 48 | // 实例化后 49 | var dog = new Dog(); 50 | // dog.__proto__ 指向 Dog.prototype 51 | // dog.__proto__.__proto__ 指向 Animal.prototype 52 | // 原型链已成功建立起来,而且很清晰 53 | ``` 54 | 55 | 老道的 `inherits` 和 NCZ 的 `inherit` 本质上都是设置好 `__proto__` 56 | 属性。看清楚这一点,一切都很简单。 57 | 58 | 原型继承的确已经够用,但这需要大家都能深入理解原型继承,对 `__proto__`, `prototype` 和 `new` 59 | 等关键点有清晰的认识。通过 `inherits` 等方法,可以简化部分细节。但用户在使用时,依旧需要面对 60 | `prototype` 等属性,并且很容易写出有隐患的代码,比如: 61 | 62 | ```js 63 | function Animal() {} 64 | function Dog() {} 65 | 66 | util.inherits(Dog, Animal); 67 | 68 | Dog.prototype = { 69 | talk: function() {}, 70 | run: function() {} 71 | }; 72 | ``` 73 | 74 | 上面的代码,你知道问题在哪吗?请继续阅读。 75 | 76 | 77 | ## YUI 之路 78 | 79 | YUI 团队是 Douglas 的铁杆粉丝团。从 YUI2 到 YUI3, 都高度贯彻了 Douglas 的精神。在 YUI 80 | 里,提供了 `extend` 方法: 81 | 82 | ```js 83 | function Animal() {} 84 | function Dog() {} 85 | 86 | Y.extend(Dog, Animal, { 87 | talk: fn, 88 | run: fn 89 | }); 90 | ``` 91 | 92 | YUI 还提供了 `augment`, `mix` 等方法来混入原型和静态方法。理论上足够用了,但对普通使用者来说,依旧存在陷阱: 93 | 94 | ```js 95 | function Animal() {} 96 | function Dog() {} 97 | 98 | Y.extend(Dog, Animal); 99 | 100 | Dog.prototype = { 101 | talk: fn, 102 | run: fn 103 | }; 104 | 105 | var dog = new Dog(); 106 | alert(dog instanceof Dog); // false 107 | ``` 108 | 109 | 上面的写法,破坏了 `Dog.prototype.constructor`, 导致 `instanceof` 不能正常工作。正确的写法是: 110 | 111 | ```js 112 | Dog.prototype = { 113 | constructor: Dog, 114 | talk: fn, 115 | run: fn 116 | }; 117 | ``` 118 | 119 | 或 120 | 121 | ```js 122 | Dog.prototype.talk = fn; 123 | Dog.prototype.run = fn; 124 | ``` 125 | 126 | 通过 `extend` 等方式来实现原型继承,写法上很灵活。`constructor` 是个不小不大的问题, 127 | 但对于类库来说,任何小问题,都有可能成为大问题。 128 | 129 | `extend` 的方式,仅仅是对 JavaScript 语言中原型继承的简单封装,需要有一定 JavaScript 130 | 编程经验后才能娴熟使用。(我个人其实蛮喜欢简简单单的 `extend`)。 131 | 132 | 此外,`extend` 的灵活性也是一种“伤害”。定义一个类时,我们更希望能有一种比较固定的书写模式, 133 | 什么东西写在什么地方,都能更简单,更一目了然。 134 | 135 | JavaScript 是一门大众语言,在类继承模式当道的今天,直接让用户去面对灵活的原型继承,未必是最好的选择。 136 | 137 | 世界的进步在于人类的不满足。作为前端的我们,只是想用更简单更舒适的方式来书写代码。JavaScript 138 | 新的语言规范里,已经提出了 `class` 概念。但在规范确定和浏览器原生支持前,故事还得继续。 139 | 140 | **注**:YUI3 里,除了 `extend` 方式,也提供了 `Base.create` 141 | 来创建新类,但是该方法比较重量级了,用起来不轻便。 142 | 143 | 144 | ## Dean Edwards 的 Base.js 145 | 146 | Dean Edwards 是前端界的一位老前辈。老前辈做过一个当时很著名的 JavaScript 类库: 147 | Base.js, 其中有一套非常不错的 OO 实现: 148 | 149 | - [A Base Class for JavaScript Inheritance](http://dean.edwards.name/weblog/2006/03/base/) 150 | 151 | 这个方案开辟了一条阳光大道:通过精心构造的 `Base` 基类来实现类继承。同一时期,JavaScript 152 | 界 OO 模拟蔚然成风,万马奔腾。让我们继续考考古。 153 | 154 | 155 | ## Prototype 的 Class 156 | 157 | 作为一名前端,如果没用过 Prototype, 那么恭喜你,说明你还年轻,潜力无限。来看一名老前端的吐槽: 158 | 159 | - [Prototype 1.6 的超级符咒](http://hax.iteye.com/blog/167131) 160 | 161 | Prototype 目前已经 v1.7 了。从官方文档来看,Class 继承已经很成熟: 162 | 163 | - [Defining classes and inheritance](http://prototypejs.org/learn/class-inheritance) 164 | 165 | `Class.create` 的写法已经比较优美。然而悲催的是,`$super` 的约定真让人无语。`super` 166 | 虽然很难实现,但也不要这样实现呀:代码一压缩就都浮云了。 167 | 168 | 169 | ## John Resig 的实现 170 | 171 | jQuery 专注于 DOM 操作,因此无论现在还是以后,应该都不会去模拟类继承。但在风云变幻的年代里,jQuery 172 | 作者 John Resig 也忍不住掺合一脚: 173 | 174 | - [Simple JavaScript Inheritance](http://ejohn.org/blog/simple-javascript-inheritance/) 175 | 176 | 与 Base2 和 Prototype 相比,John Resig 的实现无疑更漂亮。`_super` 177 | 的实现方案也简单有效,不过在 JavaScript 实现原生的 `class` 之前,所有 `super` 178 | 方案都很难完美。比如: 179 | 180 | ```js 181 | var Animal = Class.extend({ 182 | talk: function() { 183 | alert('I am talking.'); 184 | }, 185 | sleep: function() { 186 | alert('I am sleeping.') 187 | } 188 | }); 189 | 190 | var Dog = Animal.extend({ 191 | talk: function() { 192 | this._super(); 193 | } 194 | }); 195 | 196 | // 在另一个文件里,扩展 Dog 对象: 197 | Dog.prototype.sleep = function() { 198 | this._super(); // 会报错 199 | }; 200 | ``` 201 | 202 | 很明显,要使用 `_super`, 必须严格按照固定模式来写。面对灵活的 JavaScript, 所有 `super` 203 | 都是美丽的谎言。 204 | 205 | 206 | ## MooTools Class 207 | 208 | MooTools 的全称是 My OO Tools, 有一套口碑很不错的 Class 机制: 209 | 210 | - [Class](http://mootools.net/docs/core/Class/Class) 211 | 212 | `new Class` 的方式很优美,`Extends` 和 `Implements` 的首字母大写,看习惯了也觉得挺好。 213 | 214 | `Class` 和所创建的类上,也都有 `extend` 方法,与 John Resig 版本相同。 215 | 216 | `super` 语法糖,MooTools 采用了 `this.parent()` 的形式。原理与 John Resig 217 | 的差不多,都是采用 `wrap` 的方式,但 MooTools 利用了非标准属性 `caller` 来实现。 218 | 219 | 所有 `wrap` 的实现方式,都要求使用者彻底忘记 `prototype`. 以下代码在 MooTools 里也会出问题: 220 | 221 | ```js 222 | Dog.prototype.sleep = function() { 223 | this.parent(); // 会报错 224 | }; 225 | ``` 226 | 227 | 在 MooTools 里,需要这样写: 228 | 229 | ```js 230 | Dog.implement({ 231 | sleep: function() { 232 | this.parent(); 233 | } 234 | }); 235 | ``` 236 | 237 | 238 | ## 还有很多很多 239 | 240 | JavaScript 的世界里,OO 的实现还有很多很多,比较有名气的还有: 241 | 242 | - [Joose](http://joose.it/) 243 | - [JS Class](http://jsclass.jcoglan.com/inheritance.html) 244 | - [Dojo](http://dojotoolkit.org/reference-guide/1.7/dojo/declare.html#dojo-declare) 245 | - [Klass](https://github.com/ded/klass) 246 | - [Backbone.Model.extend](http://documentcloud.github.com/backbone/#Model-extend) 247 | 248 | 还有一个很有意思、崇尚组合的:[Traits.js](http://soft.vub.ac.be/~tvcutsem/traitsjs/) 249 | 250 | 实现方式上都大同小异,有兴趣的可以逐一看看。 251 | 252 | 写文档比写代码还累呀,终于快接近尾声了 —— 最重要的尾声部分。如果你在家里的话,强烈建议去洗把冷水脸,清爽一下后再来看。 253 | 254 | 255 | ## 我们的选择 256 | 257 | Arale 2.0 的核心设计原则是 KISS: 258 | 259 | 1. 如无必要,勿增实体 —— Simple 260 | 2. 一目了然,容易学习 —— Stupid 261 | 262 | 这两个原则是我们选择的权衡点。从 Simple 原则出发,`Y.extend` 就很好了。但从 Stupid 263 | 的原则考虑,明显 `Class.create` 的形式更一目了然,同时在功能上也具有 `Y.extend` 的简洁实用性。 264 | 265 | 权衡考虑后,我们选择 `Class.create` 风格,部分细节考虑如下: 266 | 267 | 1. 主要 API 与 MooTools 保持一致,但不用 `new Class`, 而用 `Class.create` 和 `SomeClass.extend`。 268 | 1. `Implements` 接收的参数就是普通对象,与 `implement` 方法保持一致。MooTools 中 `Implements` 属性需要是类。 269 | 1. 去除 `this.parent()` 语法糖,需要调用时,和 Backbone 类似,推荐直接使用 `SuperClass.prototype.methodName` 来调用。 270 | 1. 为了方便调用父类中的方法,提供 `superclass` 语法糖,与 YUI 类似。 271 | 272 | 273 | 欢迎交流反馈。 274 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arale-class", 3 | "version": "1.2.0", 4 | "keywords": ["infrastructure"], 5 | "description": "提供简洁的 OO 实现。", 6 | "homepage": "http://aralejs.org/class/", 7 | "author": "玉伯 ", 8 | "maintainers": [ 9 | "玉伯 ", 10 | "贯高 " 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/aralejs/class.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/aralejs/class/issues" 18 | }, 19 | "spm": { 20 | "main": "class.js", 21 | "devDependencies": { 22 | "expect": "0.3.1" 23 | }, 24 | "engines": { 25 | "seajs": "2.2.1" 26 | }, 27 | "tests": "tests/*-spec.js" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/class-spec.js: -------------------------------------------------------------------------------- 1 | var Class = require('../class') 2 | var expect = require('expect') 3 | 4 | describe('Class', function() { 5 | 6 | it('Class.create(parent)', function() { 7 | function Animal(name) { 8 | this.name = name 9 | } 10 | 11 | Animal.prototype.getName = function() { 12 | return this.name 13 | } 14 | 15 | var Dog = Class.create(Animal) 16 | var dog = new Dog('Jack') 17 | 18 | expect(dog.constructor).to.equal(Dog) 19 | expect(dog.name).to.equal('Jack') 20 | expect(dog.getName()).to.equal('Jack') 21 | }) 22 | 23 | it('Class.create(null)', function() { 24 | var Dog = Class.create(null) 25 | var dog = new Dog() 26 | expect(dog.constructor).to.equal(Dog) 27 | expect(Dog.superclass.constructor).to.equal(Class) 28 | 29 | Dog = Class.create() 30 | new Dog() 31 | expect(Dog.superclass.constructor).to.equal(Class) 32 | }) 33 | 34 | it('Class.create(parent, properties)', function() { 35 | function Animal(name) { 36 | this.name = name 37 | } 38 | 39 | Animal.prototype.getName = function() { 40 | return this.name 41 | } 42 | 43 | var Dog = Class.create(Animal, { 44 | talk: function() { 45 | return 'I am ' + this.name 46 | } 47 | }) 48 | var dog = new Dog('Jack') 49 | 50 | expect(dog.name).to.equal('Jack') 51 | expect(dog.talk()).to.equal('I am Jack') 52 | }) 53 | 54 | it('call initialize method properly', function() { 55 | var counter = 0 56 | 57 | var Animal = Class.create({ 58 | initialize: function() { 59 | counter++ 60 | } 61 | }) 62 | 63 | var Dog = Class.create(Animal, { 64 | initialize: function() { 65 | counter++ 66 | } 67 | }) 68 | 69 | new Dog() 70 | 71 | // Dog 有 initialize 时,只调用 Dog 的 initialize 72 | expect(counter).to.equal(1) 73 | 74 | counter = 0 75 | Dog = Class.create(Animal) 76 | 77 | new Dog() 78 | 79 | // Dog 没有 initialize 时,会自动调用父类中最近的 initialize 80 | expect(counter).to.equal(1) 81 | }) 82 | 83 | it('pass arguments to initialize method properly', function() { 84 | 85 | var Animal = Class.create({ 86 | initialize: function(firstName, lastName) { 87 | this.fullName = firstName + ' ' + lastName 88 | } 89 | }) 90 | 91 | var Bird = Animal.extend({ 92 | fly: function() { 93 | } 94 | }) 95 | 96 | var bird = new Bird('Frank', 'Wang') 97 | 98 | expect(bird.fullName).to.equal('Frank Wang') 99 | }) 100 | 101 | it('superclass', function() { 102 | var counter = 0 103 | 104 | var Animal = Class.create({ 105 | initialize: function() { 106 | counter++ 107 | }, 108 | talk: function() { 109 | return 'I am an animal' 110 | } 111 | }) 112 | 113 | var Dog = Class.create(Animal, { 114 | initialize: function() { 115 | Dog.superclass.initialize() 116 | }, 117 | talk: function() { 118 | return Dog.superclass.talk() 119 | } 120 | }) 121 | 122 | var dog = new Dog() 123 | 124 | expect(counter).to.equal(1) 125 | expect(dog.talk()).to.equal('I am an animal') 126 | }) 127 | 128 | it('Extends', function() { 129 | function Animal(name) { 130 | this.name = name 131 | } 132 | 133 | Animal.prototype.getName = function() { 134 | return this.name 135 | } 136 | 137 | var Dog = Class.create({ 138 | Extends: Animal, 139 | talk: function() { 140 | return 'I am ' + this.name 141 | } 142 | }) 143 | 144 | var dog = new Dog('Jack') 145 | 146 | expect(dog.name).to.equal('Jack') 147 | expect(dog.getName()).to.equal('Jack') 148 | expect(dog.talk()).to.equal('I am Jack') 149 | }) 150 | 151 | it('Implements', function() { 152 | var Animal = Class.create(function(name) { 153 | this.name = name 154 | }, { 155 | getName: function() { 156 | return this.name 157 | } 158 | 159 | }) 160 | 161 | var Flyable = { 162 | fly: function() { 163 | return 'I am flying' 164 | } 165 | } 166 | 167 | var Talkable = function() { 168 | } 169 | Talkable.prototype.talk = function() { 170 | return 'I am ' + this.name 171 | } 172 | 173 | var Dog = Class.create({ 174 | Extends: Animal, 175 | Implements: [Flyable, Talkable] 176 | }) 177 | 178 | var dog = new Dog('Jack') 179 | 180 | expect(dog.name).to.equal('Jack') 181 | expect(dog.getName()).to.equal('Jack') 182 | expect(dog.fly()).to.equal('I am flying') 183 | expect(dog.talk()).to.equal('I am Jack') 184 | }) 185 | 186 | it('Statics', function() { 187 | var Dog = Class.create({ 188 | initialize: function(name) { 189 | this.name = name 190 | }, 191 | Statics: { 192 | COLOR: 'red' 193 | } 194 | }) 195 | 196 | var dog = new Dog('Jack') 197 | 198 | expect(dog.name).to.equal('Jack') 199 | expect(Dog.COLOR).to.equal('red') 200 | }) 201 | 202 | it('statics inherited from parent', function() { 203 | var Animal = Class.create() 204 | Animal.LEGS = 4 205 | 206 | var Dog = Class.create({ 207 | Extends: Animal, 208 | 209 | Statics: { 210 | COLOR: 'red' 211 | }, 212 | 213 | initialize: function(name) { 214 | this.name = name 215 | } 216 | }) 217 | 218 | expect(Dog.LEGS).to.equal(4) 219 | expect(Dog.COLOR).to.equal('red') 220 | 221 | var Pig = Class.create(Class) 222 | 223 | expect(typeof Pig.implement).to.equal('function') 224 | expect(typeof Pig.extend).to.equal('function') 225 | expect(typeof Pig.Mutators).to.equal('undefined') 226 | expect(typeof Pig.create).to.equal('undefined') 227 | }) 228 | 229 | it('Class.extend', function() { 230 | var Dog = Class.extend({ 231 | initialize: function(name) { 232 | this.name = name 233 | } 234 | }) 235 | 236 | var dog = new Dog('Jack') 237 | 238 | expect(dog.name).to.equal('Jack') 239 | expect(Dog.superclass.constructor).to.equal(Class) 240 | }) 241 | 242 | it('SubClass.extend', function() { 243 | var Animal = Class.create(function(name) { 244 | this.name = name 245 | }) 246 | 247 | var Dog = Animal.extend() 248 | var dog = new Dog('Jack') 249 | 250 | expect(dog.name).to.equal('Jack') 251 | expect(Dog.superclass.constructor).to.equal(Animal) 252 | }) 253 | 254 | it('SubClass.implement', function() { 255 | var Animal = Class.create(function(name) { 256 | this.name = name 257 | }) 258 | 259 | var Dog = Animal.extend() 260 | Dog.implement({ 261 | talk: function() { 262 | return 'I am ' + this.name 263 | } 264 | }) 265 | 266 | var dog = new Dog('Jack') 267 | 268 | expect(dog.name).to.equal('Jack') 269 | expect(dog.talk()).to.equal('I am Jack') 270 | expect(Dog.superclass.constructor).to.equal(Animal) 271 | }) 272 | 273 | it('convert existed function to Class', function() { 274 | function Dog(name) { 275 | this.name = name 276 | } 277 | 278 | Class(Dog).implement({ 279 | getName: function() { 280 | return this.name 281 | } 282 | }) 283 | 284 | var dog = new Dog('Jack') 285 | 286 | expect(dog.name).to.equal('Jack') 287 | expect(dog.getName()).to.equal('Jack') 288 | 289 | var MyDog = Dog.extend({ 290 | talk: function() { 291 | return 'I am ' + this.name 292 | } 293 | }) 294 | 295 | var myDog = new MyDog('Frank') 296 | expect(myDog.name).to.equal('Frank') 297 | }) 298 | 299 | it('new AnotherClass() in initialize', function() { 300 | var called = [] 301 | 302 | var Animal = Class.create({ 303 | initialize: function() { 304 | called.push('Animal') 305 | } 306 | }) 307 | 308 | var Pig = Class.create(Animal, { 309 | initialize: function() { 310 | called.push('Pig') 311 | } 312 | }) 313 | 314 | var Dog = Class.create(Animal, { 315 | initialize: function() { 316 | new Pig() 317 | called.push('Dog') 318 | } 319 | }) 320 | 321 | new Dog() 322 | expect(called.join(' ')).to.equal('Pig Dog') 323 | 324 | }) 325 | 326 | it('StaticsWhiteList', function() { 327 | 328 | var A = Class.create() 329 | A.a = 1 330 | A.b = 1 331 | A.StaticsWhiteList = ['a'] 332 | var B = A.extend(A) 333 | 334 | expect(B.a).to.equal(1) 335 | expect(B.b).to.equal(undefined) 336 | expect(B.StaticsWhiteList).to.equal(undefined) 337 | 338 | }) 339 | 340 | it('Meta information', function() { 341 | 342 | var Dog = require('./data/dog') 343 | var dog = new Dog() 344 | //seajs.log(dog, 'dir') 345 | 346 | expect(dog.isAnimal).to.equal(true) 347 | expect(dog.isDog).to.equal(true) 348 | }) 349 | 350 | }) 351 | -------------------------------------------------------------------------------- /tests/data/animal.js: -------------------------------------------------------------------------------- 1 | var Class = require('../../class') 2 | 3 | module.exports = Class.create({ 4 | isAnimal: true 5 | }) 6 | -------------------------------------------------------------------------------- /tests/data/dog.js: -------------------------------------------------------------------------------- 1 | var Animal = require('./animal') 2 | 3 | module.exports = Animal.extend({ 4 | isDog: true 5 | }) 6 | --------------------------------------------------------------------------------