好久不见,这次发的不是笔记啦,是我在公司内部的前端wiki上更新的文档……这个抛弃所有wiki语法要求用户直接手写语义化html用json配置导航的wiki排版相当漂亮,让我这样的懒人也有了码字的欲望,发起人小麦实在系功德无量……
这篇文章去年就准备写,想用循序渐进的形式推演出一个Module Pattern的最佳实践,不过想法越多,归纳总结表达出来的成本就越高,所以一直拖延……这次发的文档是一个简化版,去掉了各式各样乱七八糟的写法,只包含几个常用的,说明文字也不多主要看代码-__-b……初衷是作为给土豆前端team里新来的同事看的提纲(对了由于某人叛逃到产品设计部门,现在又空出一个名额,有兴趣的同学抓紧时间投简历,这次是魔都总部的职位,不是成都的),所以要解释一下,文档中提到的TUI是一个js库(名字是很俗,不过我上次发现某年纪一大把的人也跟我一样俗),土豆一直采用双库并行(不要看成双工并行…)的形式,在紧跟开源社区发展的同时自己掌控所有环节和基础架构,没有使用jQuery UI和那套基于DOM的插件结构,而jQuery自己几乎不提供OOP工具(这是好事),实际上自己创建这类工具非常简单快捷,相关的代码我提取了一下直接帖在末尾了,仅供参考。
另外,为了符合Nicholas Zakas在最近的国际会议上传达的精神,我修改了若干变量名跟他ppt里的例子保持一致——这件事教育我们,平时多上slideshow.net对保持先进性是多么重要。
Tudou’s JavaScript Guideline — OOP and Module
介绍土豆在面向对象和模块化设计方面的工具和实践
“Don’t Repeat Yourself.” (DRY)
“Rather than construction, programming is more like gardening.”
Quote from: Andy Hunt and Dave Thomas, The Pragmatic Programmer
创建类,继承,混入,实例化
我们依赖的核心工具是TUI.clone
简洁的,支持私有属性,不需要prototype的写法:
JS是基于原型而不是基于类的面向对象语言,JS是“无类型”的,类是仿造出来的概念,实质只有对象。
new只是用来复制对象,构造函数只是用来返回对象, 两者对JS的OOP来说并不是必须的。
- var dog = function(options){
- var privateAttr = 1; //私有属性
- var private_method = function(){}; //私有方法
- return {
- option: options, //实例属性
- method1: function(){}
- };
- };
- var xiaobai = dog({});
- 对私有属性/方法的支持比较好
- 最适合单例模式(Singleton)
- 延迟单例的初始化,提高页面初始化的速度
- 缺点:对继承的支持不佳
- 缺点:在需要频繁创建大量对象,而方法非常多的场合,浪费资源(因为每个实例的方法指向的都是不同的函数对象,每次实例化都要重新生成所有函数)
在第2个缺点的场合,传统的prototype写法效率更高,支持继承,但是代码分散,不易读,TUI.clone提供了更好的写法——
传统的、支持继承的、仿Ruby风格的Class写法:
- //创建新类
- var Dog = TUI.newClass({
- initialize: function(options){ //构造函数
- this.option = options;
- }
- });
- //实例化
- var xiaobai = new Dog();
- //继承
- var Cat = TUI.newClass(dog, { //只允许单继承
- mixin: [TUI.event], //混入其他方法
- initialize: function(options){
- this.superClass.call(this, arguments); //调用父类构造函数
- }
- });
- 风格类似mootools的new Class
- TUI.newClass只是TUI.clone的封装
- mixin相当于在构造函数里$.extend(this.prototype, TUI.event)
- 实际上应该少用继承,多用mixin和组合模式,后者更符合JS的特点
- 缺点:封装效果不好,不支持私有属性和方法
TUI.clone既可以复制构造函数的prototype,也可以直接复制对象(跟jQuery.extend不同,依靠原型继承),所以利用它直接生成实例。
这样可以在不引入prototype和语法糖的情况下把第一种方法改造的同样高效——
对象克隆工厂的写法:
- var dog = (function(jQuery, TUI){ //传入需要访问的全局命名空间
- var a = "xxx"; //不作为状态的私有属性
- var private_method = function(){ //私有方法
- alert(this.option);
- };
- var PublicObj = { //相当于prototype对象
- method1: function(){
- private_method.call(this); //访问私有方法, 共享状态
- }
- };
- return function(options){ //对象工厂
- var obj = TUI.clone(PublicObj); //克隆
- obj.aption = options; //构造函数中的常规任务
- $.extend(obj, cat); //mixin其他对象的方法
- return obj;
- };
- })(jQuery, TUI);
- var xiaobai = dog({});
- 效率高,封装好,耦合少易于修改和扩展
- 缺点:私有属性不能作为实例状态
把以上第一种方法和第三种方法结合互补,为module模式——
模块化 Module Pattern
核心工具是TUI.module
- TUI.widget.dog = (function($, TUI){ //除了库的命名空间,尽量不访问全局变量
- //所有模块代码都封闭在这个区域内
- var a = "xxx"; //不作为状态的私有属性
- var private_method = function(){ //私有方法
- alert(this.option);
- };
- var privateObj = {}; //可作为状态的私有属性
- var privateAttr = function(name, value){ //读写私有属性的方法,只能在内部使用
- var p = private[this.objectId];
- if (!p)
- p = private[this.objectId] = {};
- if (value)
- p[name] = value;
- return p[name];
- };
- TUI.dog = TUI.newClass(parentClass, {
- mixin: [TUI.event], //混入其他方法
- initialize: function(options){
- this.superClass.call(this, arguments); //访问父类构造函数
- },
- public_method: function(){
- var b = a + privateAttr.call(this, "c"); //访问私有属性
- private_method.call(this); //访问私有方法
- }
- });
- //工厂方法,给别人使用的接口
- return function(options){
- var obj = new TUI.dog(options)
- //实例加上唯一的ID,类似Ruby,注意只用+new Date()不能避免重复
- obj.objectId = "tui_object" + ( +new Date()*10000 + Math.random(1)*10000 );
- return obj;
- };
- })(jQuery, TUI);
为了便于理解,以上代码是集中在一起的简单实现,进一步封装之后,有些步骤可以省略:
- objectId的初始化已经由TUI.clone实现
- privateAttr方法和私有属性map由TUI.newModule实现
- TUI.newModule是TUI.namespace和TUI.module.create的封装
基于 TUI.clone 和 TUI.newModule 的写法
- TUI.newModule("TUI.widget.dog", function(sandbox, $, TUI){
- TUI.dog = TUI.newClass(parentClass, {
- mixin: [TUI.event],
- sandbox: sandbox, //有sandbox属性传入时,attr属性会被转化为外部无法访问的私有属性
- attr: { //初始化私有属性的默认值
- a: 1,
- b: 2
- },
- initialize: function(options){
- this.superClass.call(this, arguments); //访问父类构造函数
- },
- public_method: function(){
- var b = this.attr(sandbox, "a") + this.attr(sandbox, "c"); //访问私有属性,通过sandbox参数来验证身份
- }
- });
- return function(options){
- var obj = new TUI.dog(options)
- return obj;
- };
- }, [jQuery, TUI]);
- var xiaobai = TUI.widget.dog({});
- console.log(xiaobai.sandbox); //undefined
- console.log(xiaobai.attr); //undefined
这里的sandbox其实还可以做很多事——
沙盒,模块间的解耦,与外部通信
TUI.module.create方法其实来自TUI.moduleClass的实例,其他独立应用同样可以继承TUI.moduleClass,构造自己的沙盒对象
- var Douwan = TUI.newClass(TUI.moduleClass, {
- notify: TUI.clone(TUI.event), //事件在这里可以理解为通信器
- initialize: function(options){
- var me = this;
- this._sandbox.notify = this.notify; //把通信器指向自己,避免跟全局的事件命名冲突
- //给沙盒增加ajax方法,让module内的代码通过沙盒来通信,屏蔽url路径和响应格式之类的细节
- var url = "http://www.tudou.com/path/service.action"
- this._sandbox.getJSON = function(param, fn){ //模块只需要传url参数
- me.getJSON(url, function(text){
- var jsondata = me.toJSON(text);
- fn(jsondata); //无论返回格式是什么,都传json给模块
- });
- };
- },
- toJSON: function(){},
- getJSON: function(){}
- });
让应用的不同模块之间解耦,避免在模块内部直接使用外部的命名(除了库的api)
- var douwanObj = new Douwan({});
- var module1 = douwanObj.module.create(function(sandbox, $, TUI){
- sandbox.getJSON({ iid: 10000 }, function(json){
- //通过通信器调用module2.update
- sondbox.notify.fire("module2-update", [json.date]);
- });
- }, [JQuery, TUI]);
- var module2 = douwanObj.module.create(function(sandbox, $, TUI){
- var obj = {
- update: function(){}
- };
- //注册一个update消息的接收器
- sondbox.notify.bind("module2-update", function(date){
- obj.update(date);
- });
- return obj;
- }, [JQuery, TUI]);
页面初始化时并不一定需要渲染或操作所有模块,因此有些模块的代码可以放在外部文件里,需要的时候再注入到页面里,类似Python的import
按需加载模块 On-demand Lazy Load
自动管理各个模块之间的依赖关系,根据代码内容加载不同的文件,这样做的成本太高,适合大型企业应用,我们的设计原则是“恰到好处”,所以通过文件名来管理模块
注册模块
- TUI.module.join("http://js.tudouui.com/js/fn/saleloader_10.js", { domain: "http://uidev.tudou.com", version: 0 });
- 配置从url里获取,第二个配置参数是可选的,优先级更高,用于调试(指向开发环境)
- 域名其实可以省略,js文件的域名通过TUI.domain和autodomain.js脚本来自动配置
- 版本号很重要,支持a_10.js/a.v10.js/a_v10.js等写法
使用模块
- TUI.module.use("/fn/saleloader", function(){
- adExtension.load(); //这个是saleloader.js内的方法,必须等js加载完后执行
- //这个区域是执行saleloader.js内代码的安全空间
- });
- 类似YUI3的Y().use,省去了版本冲突之类无用的特性
- 第一次use时会向页面里加载对应的script
- 函数区域内的代码是异步执行的,会等到saleloader.js加载完后依次执行,如果已经加载过了,直接执行,类似jQuery.ready
总结
- 以上方法虽然有递进关系,但并不是要表示最后面的才是最好的方法
- 最好的方法不是唯一的,要根据场合选择最适合的方法,以上方法都有适用场合
- 本文档涉及到的API:TUI.clone, TUI.newClass, TUI.moduleClass, TUI.module.create, TUI.newModule, TUI.module.join, TUI.module.use, TUI.event
=============开始帖源码的分割线=============
TUI.clone
/**
* @public 继承一个类或复制一个对象
* @note
* @param {object|function} oldone是需要继承的构造函数或需要复制的对象
* @param {object|function} ex为函数时,是子类的构造函数,或者用来加工新对象
* ex为对象时,为子类的方法, 其中initialize方法为构造函数, mixin为混入的超类
* @return {object|function}
*/
clone: function(oldone, ex){
var newobj,
isClass = !oldone || $.isFunction(oldone), //继承操作
constructorFn = ex && !$.isFunction(ex) && ex.initialize || ex; //子类构造函数
if (!isClass) {
newobj = function(){
if(constructorFn)
constructorFn.apply(this, arguments);
};
newobj.prototype = oldone;
return new newobj();
} else {
//为module内部定义的类提供相关方法
var c = { _sandbox: ex.sandbox, _default: ex.attr };
newobj = function(){ //构造函数
if (this.constructor === newobj) { // 如果this指向子类实例,已经执行过以下的初始化代码
this.objectId = "TUI-object-" + ++obj_uuid; //实例的唯一ID
var p = c;
if (p._sandbox && p._default)
this.attr(p._sandbox, p._default); //初始化私有属性的默认值
}
if(constructorFn) //执行构造函数的自定义部分
constructorFn.apply(this, arguments);
};
// 原型继承, 子类构造函数里需要显示调用父类构造函数
var newproto = oldone ? this.clone(oldone.prototype) : {};
// 混入其他超类方法
if (ex.mixin)
this.mix.apply(this, ([newproto]).concat(ex.mixin));
// 加入子类方法, 覆盖混入和继承
this.mix(newproto, ex, {
constructor: newobj, // 恢复
superClass: oldone || Object //在子类的构造函数中可以用this.superClass访问父类
});
delete newproto.initialize;
if (c._sandbox) {
delete newproto.sandbox; //沙盒一定要删除,不能暴露出去
newproto.attr = function(sandbox, attrname, value){ //通过sandbox参数杜绝来自外部的访问
return sandbox.attr.call(this, attrname, value);
};
}
newobj.prototype = newproto;
return newobj;
}
}
TUI.moduleClass
/**
* 存放实例的私有状态
* @private
*/
var privateAttr = {};
/**
* module的抽象类
* @note 可以继承到其他应用上, 构造独有的sandbox
*/
TUI.moduleClass = TUI.newClass({
initialize: function(){
this.notify = new TUI.eventClass();
this._sandbox.notify = this.notify; //被继承的时候,notify可以另外指向到别处
},
_sandbox: {
attr: function(name, value){ //this指向调用它的实例
if (typeof name === "object") {
privateAttr[this.objectId] = TUI.clone(name); //初始化私有变量的默认值
return true;
}
var p = privateAttr[this.objectId];
if (!p) //尽量在module里初始化
p = privateAttr[this.objectId] = {};
if (value)
p[name] = value;
return p[name];
}
},
/**
* 创建一个模块,提供封闭的代码作用域和沙盒
* @public
* @param {function}
* @param {object} args参数
*/
create: function(wrap, op){
var sandbox = TUI.clone(this._sandbox),
args = op.args || [],
ns = {};
args.unshift(sandbox);
return wrap.apply(ns, args) || ns;
}
});
$.extend(TUI.module, new TUI.moduleClass());


本来就已经够关注你的了
不如为何,从小麦那猛点又进来了。神奇
TUI准备跟YUI来两下?
请教:
tudou.com是如何在神奇的IE6下实现png透视?
(PS:本人菜鸟一个,期盼YY的答复)
Link | September 21st, 2009 at 12:37 am
@YY粉丝: 请google “IE6 png 透明” -_-b
Link | September 21st, 2009 at 1:21 am
小麦神奇的出现了。
我早就G过与bing过,想知道TuDou前端team们用了哪种方法,或者另有绝经?
对那张n5.png感兴趣
原来是索引色,无需代码
呵呵,真神奇
Link | September 21st, 2009 at 11:40 am
網站製作學習誌 » [Web] 連結分享 wrote:
[...] 尋找最好的JavaScript面向對象模式和封裝結構 [...]
Link | September 22nd, 2009 at 12:04 pm
想请教:
对象克隆工厂的写法中的缺点:私有属性不能作为实例状态,这儿的实例状态是指什么?
另,能否公开下TUI.newModule的代码?
以上,多谢。
Link | September 23rd, 2009 at 5:30 pm
看到了newModule代码了,就在moduleClass的create里,研究中
Link | September 23rd, 2009 at 6:02 pm
“实例状态”在这里指实例独有的内部状态,不与外部共享,也不可直接访问,对象克隆工厂的“私有属性”是被所有实例共享的,相当于其他OOP语言里的protected属性,要实现真正的private,只能在构造函数/工厂函数里定义,但这样的话,能访问这些私有属性的方法也必须定义在构造函数/工厂函数里,这样就退回到第一种方法了
Link | September 23rd, 2009 at 10:30 pm
实例状态是否可以说成实例的私有属性?
另外,在模块化 Module Pattern代码中的10,12中的private应为privateObj
全部看完了,思路很巧妙,受益良多。
Link | September 24th, 2009 at 10:44 am
原来是这玩意儿……晕……
Link | January 21st, 2010 at 12:12 pm
文章不错,呵呵
Link | February 8th, 2010 at 10:28 pm
抽空看看TUI,土豆前端有个WIKI,真是好,还是不错的氛围,感觉上海杭州那边氛围都不错
Link | March 9th, 2010 at 11:43 am
wiki 的风格挺好,可否分享一下?
Link | June 24th, 2010 at 12:54 am