Limbo: 混沌位面 ———— 这里既空虚又充实,没有规则,没有约束,创造来自思考,生存依赖想像,现实源自梦想。
首页 给我发Email! 订阅 跟踪我的twitter信息

[内销转出口]寻找最好的JavaScript面向对象模式和封装结构

好久不见,这次发的不是笔记啦,是我在公司内部的前端wiki上更新的文档……这个抛弃所有wiki语法要求用户直接手写语义化html用json配置导航的wiki排版相当漂亮,让我这样的懒人也有了码字的欲望,发起人小麦实在系功德无量……

picture-2.pngpicture-3.png

这篇文章去年就准备写,想用循序渐进的形式推演出一个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来说并不是必须的。

  1. var dog = function(options){
  2.     var privateAttr = 1; //私有属性
  3.     var private_method = function(){}; //私有方法
  4.     return {
  5.         option: options, //实例属性
  6.         method1: function(){}
  7.     };
  8. };
  9.  
  10. var xiaobai = dog({});
  • 对私有属性/方法的支持比较好
  • 最适合单例模式(Singleton)
  • 延迟单例的初始化,提高页面初始化的速度
  • 缺点:对继承的支持不佳
  • 缺点:在需要频繁创建大量对象,而方法非常多的场合,浪费资源(因为每个实例的方法指向的都是不同的函数对象,每次实例化都要重新生成所有函数)

在第2个缺点的场合,传统的prototype写法效率更高,支持继承,但是代码分散,不易读,TUI.clone提供了更好的写法——

传统的、支持继承的、仿Ruby风格的Class写法:

  1. //创建新类
  2. var Dog = TUI.newClass({
  3.     initialize: function(options){ //构造函数
  4.         this.option = options;
  5.     }
  6. });
  7.  
  8. //实例化
  9. var xiaobai = new Dog();
  10.  
  11. //继承
  12. var Cat = TUI.newClass(dog, { //只允许单继承
  13.     mixin: [TUI.event], //混入其他方法
  14.     initialize: function(options){
  15.         this.superClass.call(this, arguments); //调用父类构造函数
  16.     }
  17. });
  • 风格类似mootoolsnew Class
  • TUI.newClass只是TUI.clone的封装
  • mixin相当于在构造函数里$.extend(this.prototype, TUI.event)
  • 实际上应该少用继承,多用mixin和组合模式,后者更符合JS的特点
  • 缺点:封装效果不好,不支持私有属性和方法

TUI.clone既可以复制构造函数的prototype,也可以直接复制对象(跟jQuery.extend不同,依靠原型继承),所以利用它直接生成实例。

这样可以在不引入prototype和语法糖的情况下把第一种方法改造的同样高效——

对象克隆工厂的写法:

  1. var dog = (function(jQuery, TUI){ //传入需要访问的全局命名空间
  2.     var a = "xxx"; //不作为状态的私有属性
  3.     var private_method = function(){ //私有方法
  4.         alert(this.option);
  5.     };
  6.  
  7.     var PublicObj = { //相当于prototype对象
  8.         method1: function(){
  9.             private_method.call(this); //访问私有方法, 共享状态
  10.         }
  11.     };
  12.  
  13.     return function(options){ //对象工厂
  14.         var obj = TUI.clone(PublicObj); //克隆
  15.         obj.aption = options; //构造函数中的常规任务
  16.         $.extend(obj, cat); //mixin其他对象的方法
  17.         return obj;
  18.     };
  19. })(jQuery, TUI);
  20.  
  21. var xiaobai = dog({});
  • 效率高,封装好,耦合少易于修改和扩展
  • 缺点:私有属性不能作为实例状态

把以上第一种方法和第三种方法结合互补,为module模式——

模块化 Module Pattern

核心工具是TUI.module

  1. TUI.widget.dog = (function($, TUI){ //除了库的命名空间,尽量不访问全局变量
  2.     //所有模块代码都封闭在这个区域内
  3.    
  4.     var a = "xxx"; //不作为状态的私有属性
  5.     var private_method = function(){ //私有方法
  6.         alert(this.option);
  7.     };
  8.     var privateObj = {}; //可作为状态的私有属性
  9.     var privateAttr = function(name, value){ //读写私有属性的方法,只能在内部使用
  10.         var p = private[this.objectId];
  11.         if (!p)
  12.             p = private[this.objectId] = {};
  13.         if (value)
  14.             p[name] = value;
  15.         return p[name];
  16.     };
  17.  
  18.     TUI.dog = TUI.newClass(parentClass, {
  19.         mixin: [TUI.event], //混入其他方法
  20.         initialize: function(options){
  21.             this.superClass.call(this, arguments); //访问父类构造函数
  22.         },
  23.         public_method: function(){
  24.             var b = a + privateAttr.call(this, "c"); //访问私有属性
  25.             private_method.call(this); //访问私有方法
  26.         }
  27.     });   
  28.  
  29.  
  30.     //工厂方法,给别人使用的接口
  31.     return function(options){
  32.         var obj = new TUI.dog(options)
  33.         //实例加上唯一的ID,类似Ruby,注意只用+new Date()不能避免重复
  34.         obj.objectId = "tui_object" + ( +new Date()*10000 + Math.random(1)*10000 );
  35.         return obj;
  36.     };
  37. })(jQuery, TUI);

为了便于理解,以上代码是集中在一起的简单实现,进一步封装之后,有些步骤可以省略:

  • objectId的初始化已经由TUI.clone实现
  • privateAttr方法和私有属性map由TUI.newModule实现
  • TUI.newModuleTUI.namespaceTUI.module.create的封装

基于 TUI.clone 和 TUI.newModule 的写法

  1. TUI.newModule("TUI.widget.dog", function(sandbox, $, TUI){
  2.     TUI.dog = TUI.newClass(parentClass, {
  3.         mixin: [TUI.event],
  4.         sandbox: sandbox, //有sandbox属性传入时,attr属性会被转化为外部无法访问的私有属性
  5.         attr: { //初始化私有属性的默认值
  6.             a: 1,
  7.             b: 2
  8.         },
  9.         initialize: function(options){
  10.             this.superClass.call(this, arguments); //访问父类构造函数
  11.         },
  12.         public_method: function(){
  13.             var b = this.attr(sandbox, "a") + this.attr(sandbox, "c"); //访问私有属性,通过sandbox参数来验证身份
  14.         }
  15.     });   
  16.  
  17.     return function(options){
  18.         var obj = new TUI.dog(options)
  19.         return obj;
  20.     };
  21. }, [jQuery, TUI]);
  22.  
  23. var xiaobai = TUI.widget.dog({});
  24. console.log(xiaobai.sandbox); //undefined
  25. console.log(xiaobai.attr);    //undefined

这里的sandbox其实还可以做很多事——

沙盒,模块间的解耦,与外部通信

TUI.module.create方法其实来自TUI.moduleClass的实例,其他独立应用同样可以继承TUI.moduleClass,构造自己的沙盒对象

  1. var Douwan = TUI.newClass(TUI.moduleClass, {
  2.     notify: TUI.clone(TUI.event), //事件在这里可以理解为通信器
  3.     initialize: function(options){
  4.         var me = this;
  5.  
  6.         this._sandbox.notify = this.notify; //把通信器指向自己,避免跟全局的事件命名冲突
  7.  
  8.         //给沙盒增加ajax方法,让module内的代码通过沙盒来通信,屏蔽url路径和响应格式之类的细节
  9.         var url = "http://www.tudou.com/path/service.action"
  10.         this._sandbox.getJSON = function(param, fn){ //模块只需要传url参数
  11.             me.getJSON(url, function(text){
  12.                 var jsondata = me.toJSON(text);
  13.                 fn(jsondata); //无论返回格式是什么,都传json给模块
  14.             });
  15.         };
  16.     },
  17.     toJSON: function(){},
  18.     getJSON: function(){}
  19. });

让应用的不同模块之间解耦,避免在模块内部直接使用外部的命名(除了库的api)

  1. var douwanObj = new Douwan({});
  2.  
  3. var module1 = douwanObj.module.create(function(sandbox, $, TUI){
  4.     sandbox.getJSON({ iid: 10000 }, function(json){
  5.         //通过通信器调用module2.update
  6.         sondbox.notify.fire("module2-update", [json.date]);
  7.     });
  8. }, [JQuery, TUI]);
  9.  
  10. var module2 = douwanObj.module.create(function(sandbox, $, TUI){
  11.     var obj = {
  12.         update: function(){}
  13.     };
  14.     //注册一个update消息的接收器
  15.     sondbox.notify.bind("module2-update", function(date){
  16.         obj.update(date);
  17.     });
  18.  
  19.     return obj;
  20. }, [JQuery, TUI]);

页面初始化时并不一定需要渲染或操作所有模块,因此有些模块的代码可以放在外部文件里,需要的时候再注入到页面里,类似Pythonimport

按需加载模块 On-demand Lazy Load

自动管理各个模块之间的依赖关系,根据代码内容加载不同的文件,这样做的成本太高,适合大型企业应用,我们的设计原则是“恰到好处”,所以通过文件名来管理模块

注册模块

  1. 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等写法

使用模块

  1. TUI.module.use("/fn/saleloader", function(){
  2.     adExtension.load(); //这个是saleloader.js内的方法,必须等js加载完后执行
  3.  
  4.     //这个区域是执行saleloader.js内代码的安全空间
  5. });
  • 类似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 继承一个类或复制一个对象
 * @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 {
		var c = { _super: oldone, _sandbox: null, _attr: null, _default: null };
		newobj = function(){ //构造函数
			this.objectId = "TUI-object" + TUI.juid(); //实例的唯一ID, 注意不能只用当前毫秒数,连续生成实例时会重复!
			var p = c;
			this.superClass = p._super || Object; //在子类的构造函数中可以用this.superClass访问父类
			if (p._attr) {
				this.attr = p._attr; //绑定访问私有属性的方法
				if (p._default)
					this.attr(p._sandbox, p._default); //初始化私有属性的默认值
			}
			if(constructorFn) //执行构造函数的自定义部分
				constructorFn.apply(this, arguments);
		};
		if (oldone) { //需要继承
			newobj.prototype = arguments.callee.call(this, oldone.prototype); //原型继承
			$.extend(newobj.prototype, ex); //子类方法
		} else {
			newobj.prototype = ex;
		}
		if (ex.sandbox) { //为module内部定义的类提供相关方法
			c._sandbox = ex.sandbox;
			c._default = ex.attr;
			c._attr = function(sandbox, attrname, value){ //通过sandbox参数杜绝来自外部的访问
				return sandbox.attr.call(this, attrname, value);
			};
			delete ex.attr;
			delete ex.sandbox; //沙盒一定要删除,不能暴露出去
		}
		if (ex.mixin) { //混入其他接口的方法
			ex.mixin.unshift(newobj.prototype);
			$.extend.apply($, ex.mixin);
		}
		return newobj;
	}
},

TUI.moduleClass


/**
 * 存放实例的私有状态
 * @private
 */
var privateAttr = {};
/**
 * module的抽象类
 * @note 可以继承到其他应用上, 构造独有的sandbox
 */
TUI.moduleClass = TUI.newClass({
	initialize: function(){
		this.notify = TUI.clone(TUI.event),
		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}
	 */
	create: function(wrap, args){
		var sandbox = TUI.clone(this._sandbox);
		args.unshift(sandbox);
		return wrap.apply(window, args);
	}
});

$.extend(TUI.module, new TUI.moduleClass());

- 分类: 代码, JavaScript, 土豆网

    发表评论
  • 2009.09.21 00:37, YY粉丝 说:

    本来就已经够关注你的了
    不如为何,从小麦那猛点又进来了。神奇
    TUI准备跟YUI来两下?
    请教:
    tudou.com是如何在神奇的IE6下实现png透视?

    (PS:本人菜鸟一个,期盼YY的答复)

    回复

  • 2009.09.21 01:21, 小麦 说:

    @YY粉丝: 请google “IE6 png 透明” -_-b

    回复

    • 2009.09.21 11:40, YY粉丝 说:

      小麦神奇的出现了。
      我早就G过与bing过,想知道TuDou前端team们用了哪种方法,或者另有绝经?

      对那张n5.png感兴趣

      原来是索引色,无需代码
      呵呵,真神奇

      回复

  • […] 尋找最好的JavaScript面向對象模式和封裝結構 […]

    回复

  • 2009.09.23 17:30, 勤卓 说:

    想请教:
    对象克隆工厂的写法中的缺点:私有属性不能作为实例状态,这儿的实例状态是指什么?
    另,能否公开下TUI.newModule的代码?
    以上,多谢。

    回复

    • 2009.09.23 18:02, 勤卓 说:

      看到了newModule代码了,就在moduleClass的create里,研究中

      回复

    • 2009.09.23 22:30, dexter_yy 说:

      “实例状态”在这里指实例独有的内部状态,不与外部共享,也不可直接访问,对象克隆工厂的“私有属性”是被所有实例共享的,相当于其他OOP语言里的protected属性,要实现真正的private,只能在构造函数/工厂函数里定义,但这样的话,能访问这些私有属性的方法也必须定义在构造函数/工厂函数里,这样就退回到第一种方法了

      回复

      • 2009.09.24 10:44, 勤卓 说:

        实例状态是否可以说成实例的私有属性?

        另外,在模块化 Module Pattern代码中的10,12中的private应为privateObj

        全部看完了,思路很巧妙,受益良多。

        回复

  • 2010.01.21 12:12, army8735 说:

    原来是这玩意儿……晕……

    回复

  • 2010.02.08 22:28, 开心凡人 说:

    文章不错,呵呵

    回复

  • 2010.03.09 11:43, qbaty 说:

    抽空看看TUI,土豆前端有个WIKI,真是好,还是不错的氛围,感觉上海杭州那边氛围都不错

    回复

(一定要填写)

(一定要填写, 不会公开喔!)

GOSPEL OF YY

    Good judgment comes from experience, and experience comes from bad judgment.

    -- Randy Pausch