论坛首页 Web前端技术论坛

两个 JavaScript 面向对象的方法

浏览 5702 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (11)
作者 正文
   发表时间:2008-10-02  
准备工作

为了演示或者您试验,请先准备好下面的 HTML 模板。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css">
body, body * {
	font: 10pt Arial;
}

.tbl {
	border-style: solid;
	border-width: 1px;
	width: 500px;
	table-layout: fixed;
}

.tbl thead {
	background-color: #555;
	color: #FFF;
}

.tbl tbody td {
	border-style: solid;
	border-width: 1px;
}
</style>
<script type="text/javascript">
</script>
</head>
<body>
<input type="button" value="Insert A Row" /><br />
<input type="text" id="indexText" />
<input type="button" value="Remove A Row" /><br />
<table class="tbl">
	<thead>
		<tr>
			<td>Col A</td>
			<td>Col B</td>
		</tr>
	</thead>
	<tbody id="tblbody">
	</tbody>
</table>
</body>
</html>

这个页面包含一个两列的表格。"Insert A Row" 这个按钮意在向此表中添加一行。"Remove A Row"意在从此表中删除一行。在文本框中指定要删除的行号(从一开始索引)。

第一种,基于单个对象

var tableObj = (function() {
	var count = 0;
	
	var validateIndex = function(index) {
		if (index >= 1 && index <= count) return true;
		return false;
	};
	
	return {
		addRow: function(a, b) {
			if (!a) a = "";
			if (!b) b = "";
			var tblBody = document.getElementById("tblbody");
			var newRow = document.createElement("tr");
			var colA = document.createElement("td");
			colA.innerText = a;
			var colB = document.createElement("td");
			colB.innerText = b;
			newRow.appendChild(colA);
			newRow.appendChild(colB);
			tblBody.appendChild(newRow);
			
			count += 1;
		},
		
		removeRow: function(index) {
			if (!index) index = 0;
			if (!validateIndex(index)) {
				throw "Index out of range: " + index;
				return;
			}
			var tblBody = document.getElementById("tblbody");
			var row = tblBody.childNodes[index];
			tblBody.removeChild(row);
			
			count -= 1;
		}
	};
})();

这种方法被称为块模式(Module Pattern)。最后的“()”会导致那个匿名函数立即执行,从而返回 return 块的对象。这个返回的对象被赋给了 tableObj。其中,count 为 tableObj 的私有成员变量,validateIndex() 函数为其私有成员函数。

为了看看效果,将上面的代码放在 <script> 标签之中。然后为“Insert A Row”按钮添加 onclick="insertARow()",为“Remove A Row”按钮添加 onclick="removeARow()"。最后,在 <script> 标签中再加入下面的两个函数,
function insertARow() {
	tableObj.addRow("hi", "hello");
}

function removeARow() {
	var index = document.getElementById("indexText").value;
	if (/^\d+$/.test(index)) {
		index = index / 1;
	} else {
		alert("Not a number");
		return;
	}
	try {
		tableObj.removeRow(index);
	} catch (err) {
		alert(err);
	}
}

这样,按钮的事件响应函数就添加完成了。现在您打开页面,应该就可以试验一下效果了。这种方法的优点是短平快,缺点是复用性较差。当然,这里指的不是 copy-paste 式的复用

假设有这样一个用例,还是这个页面,现在我需要再添加一个结构一样的表。难道要我们把上面的代码复制一遍吗?当然不。如果能支持以 new 的方法创建的话是最理想的。下面将介绍的方法基于类型,即构造函数。它是可以支持以 new 的方式创建对象的。

第二种,基于类型

var TableClass;
(function() {
	/* Private static members
	 */
	var _debug = function(src, msg) {
		if (window.console) {
			window.console.log("[TableClass] " + src + ": " + msg);
		}
	};

	// Constructor
	TableClass = function(tbodyId) {
		TableClass.instances.push(this);
		
		/* Private member fields
		 */
		var count = 0;
		
		/* Private member functions
		 */
		var validateIndex = function(index) {
			_debug("validateIndex()", "count: " + count);
			if (index >= 1 && index <= count) return true;
			return false;
		};

		/* Initializer
		 */
		(function() {
			TableClass.instances.push(this);
		})();

		/* Public member funcitons
		 */
		this.addRow = function(a, b) {
			_debug("addRow()", "count: " + count);
			if (!a) a = "";
			if (!b) b = "";
			var tblBody = document.getElementById(tbodyId);
			var newRow = document.createElement("tr");
			var colA = document.createElement("td");
			colA.innerText = a;
			var colB = document.createElement("td");
			colB.innerText = b;
			newRow.appendChild(colA);
			newRow.appendChild(colB);
			tblBody.appendChild(newRow);
			
			count += 1;
		};
		
		this.removeRow = function(index) {
			_debug("removeRow()", "count: " + count);
			if (!index) index = 0;
			if (!validateIndex(index)) {
				throw "Index out of range: " + index;
				return;
			}
			var tblBody = document.getElementById(tbodyId);
			var row = tblBody.childNodes[index];
			tblBody.removeChild(row);
			
			count -= 1;
		}
	};
	
	/* Public static members
	 */
	TableClass.instances = [];
})();

这样的模式目前我还不知道是否有正式的名称。不过就目前我所知道的,网上各位作者仍然把类似这种基于类型的方法称为块模式。我认为,上面的模式就实现的功能来说是最优秀的。因为它能实现私有成员、私有静态、公有成员及公有静态。我将其称为类模式(Class Pattern)。

注意,公有成员函数 addRow() 和 removeRow() 在代码中使用了一个私有成员变量 tbodyId。而只有 TableClass 的构造函数带一个 tbodyId 的参数。就是这个 tbodyId 充当了私有成员变量。如果您觉得这样不好看,也可以在构造函数中加上这样一句话,
var _tbodyId = tbodyId;

然后把那两处引用 tbodyId 的地方改成 _tbodyId。看您的喜好了。

将第一个例子改造,添加一个结构相同的表。首先添加 HTML,
<table class="tbl">
	<thead>
		<tr>
			<td>Col A</td>
			<td>Col B</td>
		</tr>
	</thead>
	<tbody id="tblbody2">
	</tbody>
</table>

然后初始化两个表对象,
var tableObj = new TableClass("tblbody");
var table2Obj = new TableClass("tblbody2");

最后改造按钮的事件响应函数,
function insertARow() {
	tableObj.addRow("hi", "hello");
	table2Obj.addRow("hello", "hi"); // added this row.
}

function removeARow() {
	var index = document.getElementById("indexText").value;
	if (/^\d+$/.test(index)) {
		index = index / 1;
	} else {
		alert("Not a number");
		return;
	}
	try {
		tableObj.removeRow(index);
		table2Obj.removeRow(index); // added this row.
	} catch (err) {
		alert(err);
	}
}

OK。现在如果您点击“Insert A Row”应该可以看到两列内容相反但结构一致的表了。为了验证我们的公共静态成员 instances 是否有效,创建一个单独的按钮,
<input type="button" value="Count table instances" onclick="alert(TableClass.instances.length)" /><br />

点击“Count table instances”,结果会显示“2”。

题外话,命名空间

为了避免全局名字污染,通常我们写的控件都会放一个全局名称之下。像 YUI 的 YAHOO,jQuery 的 jQuery($),DWR 的 dwr。比如我在 GE,写的控件可能就会以 ge. 开头。命名空间实际上就是借助对象的嵌套来实现,比如
if (!dwr) var dwr = {};
if (!dwr.util) dwr.util = {};

dwr.util.escapeHtml = function(...) { ... };


题外话,选项

通常一个函数有可选参数的时候,大家会习惯性地将其放在函数签名的末尾。但如果可选参数比较多就不好看了。可以通过这样的方式来提供可选参数,
function fun(param1, optionalParams) {
	if (!optionalParams) optionalParams = {};
	...
	if (optionalPrams.timeout) {
		...
	}
}

fun 函数带一个 param1 参数和一个 optionalParams 可选参数。这个 optionalParams 实际上代表了一个可选参数的集合。比如我可以这样调用,
fun("param1", { timeout: 1000 });

DWR 使用类似这样的方式来实现功能丰富的回调,而 jQuery 无疑是这方面最强大的库。结合 jQuery 的变量继承,可选参数的实现变得非常简单。因为通过继承,可选参数的默认值可以很容易地指定。

参考

   发表时间:2008-10-17  
其实第二种方法的 addRow removeRow 方法,在每次调用的时候都是不同的function(作用域不同)。

不想使用不必要的this.xxx绑定私有变量,从而使用prototype来实现公有函数的话,把 addRow removeRow 放在闭包里、返回体外,就能让他们在每次调用的时候保持一致。

tableObj.addRow === tableObj2.addRow; //true

0 请登录后投票
   发表时间:2008-10-23  
不好意思,这阵子太忙,没有回。您能写一段代码演示一下吗?
0 请登录后投票
   发表时间:2008-10-27  
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-cn" lang="zh-cn">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>test</title>
<script type="text/javascript">
...
</script>
</head>
<body>
	<table width="200" cellspacing="0" cellpadding="0" border="1">
	<tbody id="tbody_1"/>
	</table>

	<table width="200" cellspacing="0" cellpadding="0" border="1">
	<tbody id="tbody_2"/>
	</table>
</body>
</html>

var getTableBody=function()
{
	function addRow(text)
	{
		var $tr=this.insertRow(-1);
		for(var i=text.length;i;i--)
		{
			var $td=$tr.insertCell(0);
			$td.innerHTML=text[i-1];
		}
	}
	function removeRow(index)
	{
		this.deleteRow(index);
	}
	return function(id)
	{
		var $table=document.getElementById(id);
		$table.addRow=addRow;
		$table.removeRow=removeRow;
		return $table;
	};
}();

window.onload=function()
{
	var t1=getTableBody("tbody_1");
	var t2=getTableBody("tbody_2");

	t1.addRow([1,2]);
	t2.addRow([3,4,5]);

	alert(t1.addRow===t2.addRow);
}


这样相当于给每个tBody添加了两个方法。
如果不喜欢这样做,也可以返回仅包含这两个方法的对象:
return {addRow:function(text){addRow.call($table,text)},removeRow:function(index){removeRow.call($table,index)}}
但这样就t1.addRow!==t2.addRow了。
0 请登录后投票
   发表时间:2008-10-27  
您这样谢确实可以解决 t1.addRow === t2.addRow。好处是可以节省内存吗?我注意到,您的想法似乎是 hack 这个表的 DOM 对象。但我认为有充分的理由应该把自己的对象和 DOM 对象区分开,控件越复杂越是如此。

还有一个对于我来说最大的问题,就是实例方法里,我希望 this 指向我们自己编写的 table 脚本对象,而不是 DOM 对象。因为自己写的控件里会涉及自定义事件的触发,this 如果指向自身的话会十分方便。this 指向自身在我看来正是区分实例方法与静态方法的意义所在。也正是因为这个原因,我没有使用我参考里所列文章里的方法。使用他们的方法在调私有方法时必须用 addRow.call(this, ...)来保证 this 的语义。代码看起来非常乱,就我个人的经验来说,很容易出错,而且急难调试。如果忘记用 call 来调用,只会在被调函数内部出错,调用方不会有问题。

或者,您有方法既可以保证我所说的 this 的语义,同时还能使 t1.addRow === t2.addRow?

欢迎探讨!
0 请登录后投票
   发表时间:2008-10-28  
又要保证this的语义,又要t1.addRow===t2.addRow的话,就必须暴露私有变量,并使用原型的方式来定义对象了。比如:
function tableBody(id)
{
	this.table=document.getElementById(id);
}
tableBody.prototype=
{
	addRow:function(){...},
	removeRow:function(){...}
};

这样的话,var table=new tableBody('xxx');得到的对象的table属性是私有属性,但为了传递给其公有方法,必须绑定在该对象上。私有属性也能被外部访问了,不能隐藏。

因为每次执行该函数的时候,tbody都不尽相同,这时要么把这个函数作为一个“工具函数”使用,如TableManager.addRow(HTMLElement tbody,Array text),要么使用this关联到tbody,如上面的prototype里边的两个函数,this.table就是这个tbody。


t1.addRow === t2.addRow 的好处应该是能节省内存,多次定义一个内容相同的函数,和只有一个函数,肯定后者比较好。


另外,给DOM添加自定方法,在mootools里被发挥的淋漓尽致,除了IE的其他浏览器如Firefox2,3/Opera9/Safari3/Chrome都有这两个对象,扩展Element和Event的prototype,就相当于给所有DOM对象和Event对象添加方法。但IE系列对HTMLElement和Event没有抽象的“类”对象,mootools就把扩展的方法逐一直接以赋值的方式赋给每个他用$获取的DOM对象。

IE8里提供了Element和Event这两个对象,也可以直接扩展了。这样对于OO方式的编程来说方便了很多。就不用“工具函数”的方法传参了。
0 请登录后投票
   发表时间:2008-10-29  
看来目前还没有十分完美的方法了。没想到给 DOM 添加自定义方法会有正式的用处。不过就目前所说,我倾向于我自己的方法。浪费点内存,但能保证信息隐藏。
0 请登录后投票
   发表时间:2008-11-12  
1。在js里面this始终是指向调用者。
2。如果对象化js,应该保持多实例调用的是同一个对象的方法。
3。私有变量尽量用_开始定义例如 : var _xxx;
0 请登录后投票
   发表时间:2009-01-16  
 <SCRIPT LANGUAGE="JavaScript">
  <!--
	var TableClass;
	(function() {
		/* Private static members
		 */
		var _debug = function(src, msg) {
			if (window.console) {
				window.console.log("[TableClass] " + src + ": " + msg);
			}
		};

		// Constructor
		TableClass = function(tbodyId) {
			TableClass.instances.push(this);
			/* Private member fields
			 */
			var count = 0;
			
			/* Private member functions
			 */
			var validateIndex = function(index) {
				_debug("validateIndex()", "count: " + count);
				if (index >= 0 && index <= count) return true;
				return false;
			};

			/* Initializer
			 */
			(function() {
				TableClass.instances.push(this);
			})();
			
			/* Public member funcitons
			 */	
			this._tbodyId = tbodyId;

			if(typeof TableClass.prototype.addRow == "undefined"){
				TableClass.prototype.addRow = function(a, b) {
					_debug("addRow()", "count: " + count);
					if (!a) a = "";
					if (!b) b = "";
					var tblBody = document.getElementById(this._tbodyId);
					var newRow = document.createElement("tr");
					var colA = document.createElement("td");
					colA.innerText = a;
					var colB = document.createElement("td");
					colB.innerText = b;
					newRow.appendChild(colA);
					newRow.appendChild(colB);
					tblBody.appendChild(newRow);
					
					count += 1;
				}
			};	

			if(typeof TableClass.prototype.removeRow == "undefined"){
				TableClass.prototype.removeRow = function(index) {
					_debug("removeRow()", "count: " + count);
					if (!index) index = 0;
					if (!validateIndex(index)) {
						throw "Index out of range: " + index;
						return;
					}
					var tblBody = document.getElementById(this._tbodyId);
					var row = tblBody.childNodes[index];
					tblBody.removeChild(row);
					
					count -= 1;
				}		
			};
		};
	
		/* Public static members
		 */
		TableClass.instances = [];
	})();

  //-->
  </SCRIPT>

這樣改應該就可以了....
0 请登录后投票
   发表时间:2009-01-17  
没错,我觉得这样应该就完美了。
0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics