原文标题:《Functions as Namespaces, and How to Peek Inside》

原文地址:http://www.davidflanagan.com/2009/11/functions-as-na.html

很精彩的技巧,通过闭包和eval()的使用,达到标题中的功能。

var value = (function() {  // Wrapper function creates a local scope or namespace
	// your code goes here
	return value;  // Export a value from the namespace
})();  // Invoke the wrapper function to run your code

以上代码是我们经常用到的技巧:赋值时通过执行一个匿名函数来防止一些代码变量污染全局空间,并且函数内部可以写很复杂的实现而无需担心对外部的影响。

但是有些情况下我们得到的js代码是一串字符串——例如,用xhr读取到的js代码。倘若想要使用它,可以用eval(),还可以用更方便的Function()构造器。

var code = "alert(1);";  // A string of JS code to evaluate
var f = new Function(code);   // Wrap it in a function
f();    // And run the function

//army注:这段代码和下面是完全等同的:
function f() {
	alert(1);
}
f();

所以,通过这种技巧,哪怕是从一段js字符串源代码来赋值,也是可行的:

var code = "return 3;";
var f = new Function(code);
var i = f(); //i是3

但是这里却有一个问题。因为是由Function()构造函数而来的,相当于创造了一个密封的命名空间(所有代码都在一个匿名function内执行),我们无法从外部访问它。倘若里面有一些定义的类或者函数之类的东西,那就难办了。比如这样:

var code = "function Test() {};";
var f = new Function(code);
f();
//这相当于执行了以下方法:
function f() {
	function Test() {
	}
};
f();

内部定义了一个Test类,我们很难访问到它。不过这里有个技巧——这也是本篇要介绍的主角——可以通过闭包+eval()结合使用来绕过这种限制。

var code = "function Test() { alert('a test'); };";
var f = new Function(code + "return function(s) { return eval(s); };")(); //关键!还有后面的括号!
var Test = f("Test");
new Test();

如何?内部的Test类成功从外部创建了。关键就在于第2行,这里有个小小限制,第3行传入的参数需和要使用的内部变量名相等。外部的Test其实相当于密封函数内的Test的一个copy,没想明白的话根据代码倒着走一遍就ok了。

有人好奇JAse中的undo和redo是怎么做的,并且给出自己的设计做法也各有特色。我最初采用的是全文保存方法(这一方法也是最简单有效、使用最广泛的),后来被人痛批一顿换成了命令链。命令链其实是基于一种叫做“命令”的设计模式的,关键思想在于面向接口编程。这个东西随便搜搜有很多例子,我再重复制造一下轮子吧。

AS3中的命令链

as3对OOP的支持已经比较完善,所以可以据此写出很好的命令链,先从简单的例子来说起(以下代码均被简化)。假如我们要做一个Dog类,Dog的“说话”方式是bark(狗叫):

class Dog {
	public function bark():void {
		trace("汪汪!");
	}
}

这很简单,没有什么特殊之处。可是奇怪的事情发生了,中学英语课本中有个闻名的澳洲野狗Dingo,它的叫声和普通的狗不一样:

class Dingo extends Dog {
	public override function bark():void {
		trace("呜呜——");
	}
}

也很简单,Dingo毕竟还是一只狗,只需继承并覆盖即可。好了现在需求来了:我们想听听这些动物的叫声是什么样的,只需要调用下相应的方法即可。

public class Test {
	public function Test():void {
		var dog:Dog = new Dog();
		dog.bark();

		var dingo:Dog = new Dingo();
		dingo.bark();
	}
}

目前为止一切顺利,可惜未来总是不像我们想象的那样。动物中又多了只公鸡,它的叫法完全不一样,是打鸣。

class Cock {
	public function crow():void {
		trace("喔喔——");
	}
}

这下坏了,我们在听叫声的时候得记住,公鸡的叫法和狗不一样:

public class Test {
	public function Test():void {
		var dog:Dog = new Dog();
		dog.bark();

		var dingo:Dog = new Dingo();
		dingo.bark();

		var cock:Cock = new Cock();
		cock.crow();
	}
}

倘若把这些动物排成一列,让它们依次各自叫一下的话就更麻烦了:

public class Test {
	public function Test():void {
		var list:Array = [new Dog(), new Dingo(), new Cock()];
		for (var i:int = 0; i < list.length; i++) {
			var item = list[i];
			if (item is Dog) {
				item.bark();
			}
			else if (item is Cock) {
				item.crow();
			}
		}
	}
}

我们得对每种类型做单独判断,可所谓不胜其烦,动物的种类是不可预知的,倘若再来一只猫,还要继续增加判别吗?当然不能。

显然,关于叫法这里我们需要解耦。不管你是什么动物,这些叫声其实都可以概括为“说话”(动物们也有自己的语言和说话方式)。在主调程序中,我们并不想关心动物是“怎么说话”、“说些什么”的,我们只想调用一个命令,动物对象就能自动按照自己的方式来“说话”,甚至我们不用关心这个动物到底是什么。通过分离做什么和怎么做来实现这个目标的方式,就称为命令模式

好了,首先是命令接口:

public interface Command {
	function say():void;
}

所有动物只要实现了这个接口就行,各自具体的执行方式自己来定:

class Dog implements Commad {
	public function say():void {
		bark();
	}
	protected function bark():void {
		trace("汪汪!");
	}
}

class Dingo extends Dog {
	protected override function bark():void {
		trace("呜呜——"");
	}
}

class Cock implements Commad {
	public function say():void {
		crow();
	}
	private function crow():void {
		trace("喔喔——");
	}
}

这样的话,在主调程序中,管它排成一列的动物有哪些,统统视作Command接口的实现即可:

public class Test {
	public function Test():void {
		var list:Array = [new Dog(), new Dingo(), new Cock()];
		for (var i:int = 0; i < list.length; i++) {
			(list[i] as Command).say();
		}
	}
}

哪怕再多出来一只猫,我们只需要增加猫的这一类别,列表中多出只猫来即可,主调程序无需关心具体实现。甚至猫的方式更加复杂有感情:

class Cat implements Command {
	private var isHappy:Boolean = true;

	public function say():void {
		if (isHappy) {
			purr();
		}
		else {
			mew();
		}
	}

	private function purr():void {
		trace("咕噜……");
	}
	private function mew():void {
		trace("喵——");
	}
}

public class Test {
	public function Test():void {
		var list:Array = [new Dog(), new Dingo(), new Cock(), new Cat()];
		for (var i:int = 0; i < list.length; i++) {
			(list[i] as Command).say();
		}
	}
}

undo、redo的命令链

熟悉了这些后,undo和redo的做法同理:只需实现了命令接口,每个操作各是独自的命令,互不干涉,具体实现自己决定,最大程度上解耦。

定义命令接口(以下代码均被简化):

package command {

	public interface ICommand {
		function redo():void;
		function undo():void;
	}

}

输入命令——也就是在编辑器中敲入代码时:

package command {
	import flash.text.*;

	public class InputCommand implements ICommand {
		private var tf:TextField;
		private var index:int;
		private var text:String;

		public function InputCommand(tf:TextField, index:int, text:String):void {
			this.tf = tf;
			this.index = index;
			this.text = text;
		}

		public function redo():void {
			tf.setSelection(index + text.length, index + text.length);
		}
		public function undo():void {
			tf.replaceText(index, index + text.length, "");
		}
	}
}

删除命令——当按Delete键将编辑器里的内容删除时:

package command {
	import flash.text.*;

	public class DeleteCommand implements ICommand {
		private var tf:TextField;
		private var index:int;
		private var end:int;
		private var text:String;

		public function DeleteCommand(tf:TextField, index:int, end:int, text:String):void {
			this.tf = tf;
			this.index = index;
			this.end = end;
			this.text = text;
		}

		public function redo():void {
			tf.replaceText(index, index + text.length, "");
		}
		public function undo():void {
			tf.replaceText(index, index, text);
		}
	}
}

于是,每当我们输入一个字符时,向一个记录命令步骤的数组里存入一个输入命令;而在按下Delete键时,存入一个删除命令;其它多一个命令多建一个类实现,这样一个链连下来就是命令链

package command {
	import flash.text.*;
	import edit.*;

	public class CommandList {
		private var undoList:Array, redoList:Array;

		public function CommandList():void {
			clear();
		}

		public function addCommand(cmd:ICommand):void {
			//超过最大命令链长度需先出队列一个
			if (undoList.length > Editor.UNDO_SIZE) {
				undoList.shift();
			}
			//每添加一次命令,清空redoList
			if(redoList.length) {
				redoList = new Array();
			}
			undoList.push(cmd);
		}
		public function undo():Boolean {
			//undoList中有命令则执行,并将相应命令出栈存入redoList中
			if(undoList.length) {
				var cmd:ICommand = undoList.pop() as ICommand;
				cmd.undo();
				redoList.push(cmd);
				return true;
			}
			//为空返回false
			else {
				return false;
			}
		}
		public function redo():Boolean {
			//redoList中有命令则执行,并将相应命令出栈存入undoList中
			if (redoList.length) {
				var cmd:ICommand = redoList.pop() as ICommand;
				cmd.redo();
				undoList.push(cmd);
				return true;
			}
			//为空返回false
			else {
				return false;
			}
		}
		public function clear():void {
			undoList = new Array();
			redoList = new Array();
		}
	}

}

其实基于fp10中as3的新特性,有个叫Vector的类,它是存储命令链数组的更好的替代者,因为它规定数组里所有元素的类型必须是相同的。这样在编译期间便能防止出错,于是undoList和redoList更好的定义方式是这样:

undoList = new Vector.<ICommand>();
redoList = new Vector.<ICommand>();

熟悉Java的很容易理解:这就是Java5+中的泛型。

js中的命令链

扯了这么远才到js,而且篇幅也不会多长,真是愧对这标题。

js中并不存在接口,所以也就无从说起面向接口编程。然而以此就下定论说“不能使用命令链”尚为时过早。的确,js由于其本身特性原因,不能像传统OOP语言那样使用,但命令链模式其实就存在于日常生活当中:

function Dog() {
}
Dog.prototype.say = function() {
	this.bark();
}
Dog.prototype.bark = function() {
	alert("汪汪!");
}

function Dingo() {
}
Dingo.prototype = new Dog();
Dingo.prototype.bark = function() {
	alert("呜呜——");
}

function Cock() {
}
Cock.prototype.say = function() {
	this.crow();
}
Cock.prototype.crow = function() {
	alert("喔喔——");
}

function Cat() {
	this.bHappy = true;
}
Cat.prototype.say = function() {
	if(this.bHappy) {
		this.purr();
	}
	else {
		this.mew();
	}
}
Cat.prototype.purr = function() {
	alert("咕噜……");
}
Cat.prototype.mew = function() {
	alert("喵——");
}

var aList = [new Dog(), new Dingo(), new Cock(), new Cat()];
for(var i = 0; i < aList.length; i++) {
	aList[i].say();
}

基于弱类型,每个类都可以拥有一个同名方法,广义上说这也可以看作是实现了一个“通用接口”。js没有编译过程,因此无法在前期(编译期)实现检查,只能靠后期(运行时)来确定。网上也有很多例子模拟出js的接口和命令链,使得一个类在实现接口但却没有实现接口定义的方法时抛出异常。这样做的好处是在运行出错时有详尽的异常信息供使用者检查,但在目前firebug等工具的情况下所提供的能效有限,在大型的开发中才会有很好的帮助。因此我们目前大部分情况下——似乎最好的办法就是相信别人

一月 22nd, 2010

jssc 5.0 beta5

9 Comments, jssc, 前端开发, by army8735.

下载页面:http://code.google.com/p/jssc/downloads/list

源码地址:http://jssc.googlecode.com/svn/trunk/

预览效果:http://army8735.org/wp-content/uploads/jssc/

全部是细节方面的调整。

性能有所略微的提升,体积稍微减少一点,可能整体没啥感觉。因为性能瓶颈主要在两方面:as的分析阶段和js的显示阶段。分析阶段中主要是词法分析阶段和字符串拼接阶段,调整的是词法分析阶段,而这个部分占整体所耗时间不是最多的,所以提升不明显。

接口稍微改了下,具体的看源代码。php由原本的纯php代码变为了内嵌显示方式,像html那样。

这个可能是最后一个beta版了,rc如果放出基本都是针对bug的修补,另外会全面更新wiki的使用方法。

说下5.x系列的计划吧:

  1. 首先是增加语言:诸如jsp、ruby、csharp等等。语系的增加不会增加子版本号,如5.1,而是后缀的小版本号——5.0.1。
  2. 新特性增加将以子版本号形式出现,譬如5.1版本首先考虑的是缓存输出优化(针对代码行上万的高亮显示)。
  3. 也是以前考虑实现而没有实现的特性:自动格式化,这个可能难一点,在以后的计划之内吧。

暂且这么多,有想到新的或者别人的想法再列入计划里。

一月 7th, 2010

智能JPEG优化技术

8 Comments, 其它, 前端开发, 翻译, by army8735.

同事发来了一篇《Clever JPEG Optimization Techniques》,文中提到的部分技术很细致,分享下。

大部分人在考虑图像压缩优化的时候,还只停留在设置图像处理软件的保存选项上。另外我们也有一些常用的优化工具,诸如OptiPNG和jpegtran。但是还有一些鲜为人知的方法,比如即将介绍的“8像素格优化方法”,它的原理是基于图像数据存储的格式上的。

8像素格

众所周知,jpeg图像存储是以8像素格为基本单位的,看下图示例:

8pixel-1 32×32 pixels, Quality: 10 (in Photoshop), 396 bytes.

两个白色块大小均为8×8像素,图像保存质量为低。可以看出,左上角的方块很清晰,右下角却出现了杂色,这是为什么呢?让我们放大图片并画出参考线格:

8pixel-2

可以看出,左上角的方块恰好在8×8格子里面(占据4个),而右下角的却横跨了9个格子,除了中间部分占满一个格子外,周围8个都只占据一部分。

由于jpeg存储算法中,每8×8个像素格是单独进行优化的,算法会寻找这个基本单位格中的均色(jpeg是以颜色正弦波编码)。因此,图像处理时应该尽可能考虑到这点,使得元素的位置靠近每个8×8的像素格。

这个方法使用起来很简单,比如下面这个例子:

8grid-bad13.51 KB

8grid-good12.65 KB

第一张图片中,微波炉的位置是随意放的,而第二张却经过了细微的调整。两者存储的质量相同,都是55。让我们放大点看,红线是参考线:

8grid-zoom

可以看到,在略微移动了几个像素之后,图像减少了大约1 KB,并且也更清晰了一点。

颜色优化

这部分主要介绍不常用的图片存储格式,它主要应用在电视上面,暂不介绍。

常见的JPEG优化方法

这里介绍一些常用的优化方法。

JPEG算法很严格,唯一的压缩准则是图像软件设置里的质量选项。你可能在Photoshop中存质量为55~60的图片,但是在其它软件中存80质量才拥有同样的尺寸和外观。

一定不要用100的质量来保存图片!这其实并不是最高的质量值,因为这只是个数学理论上限,如果你非要质量很高的图片的话,一般存到95就够了,5点的质量丢失几乎没有区别。

注意Photoshop中低于50质量的图片保存。因为在50之下的时候,jpeg优化会启动一个附加的算法——color down-sampling——它将均衡相邻的8×8像素格的颜色。

q50 48×48 pixels, Quality: 50 (in Photoshop), 530 bytes.

q51 48×48 pixels, Quality: 51 (in Photoshop), 484 bytes.

高一个质量反而更好更小。所以,如果图片拥有小尺寸、高差别的情况,请至少在Photoshop中保存质量为51。

NCZ在他的同名博客《Feature detection is not browser detection》中,讲述了一直以来前端开发中的一个热门技术——检测用户的浏览器平台,并详细地叙说历史发展以及各种办法的优缺点。我大致翻译了部分文章,可能有理解错误的地方,敬请指正。值得一提的是,评论部分的争论亦值得一看。

特性检测

起初前端工程师们就极力反对浏览器检测,他们认为类似user-agent嗅探的方法是很不好的,理由是它并不是一种面向未来的代码,无法适应新版的浏览器。更好的做法是使用特性检测,就像这样:

if (navigator.userAgent.indexOf("MSIE 7") > -1){
    //do something
}

而更好的做法是这样:

if(document.all){
    //do something
}

这两种方式并不相同。前者是检测浏览器的特殊名称和版本;后者却是检测浏览器的特性。UA嗅探能够精确得到浏览器的类型和版本(至少能得知浏览器类型),而特性检测却是去确定浏览器是否拥有某个对象或者支持某个方法。注意这两者是完全不同的。

因为特性检测依赖于哪些浏览器支持,当出现新版本浏览器的时候需要繁琐的确认工作。例如DOM标准刚出现的时候,并不是所有浏览器都支持getElementById()方法,所以一开始代码可能是这样:

if(document.getElementById){  //DOM
    element = document.getElementById(id);
} else if (document.all) {  //IE
    element = document.all[id];
} else if (document.layers){  //Netscape < 6
    element = document.layers[id];
}

这是特性检测很好的一个例子,亮点在于当其它浏览器开始支持getElementById()方法时不必修改代码。

混合方式

后来前端工程师们考虑改进的写法,代码变化成这样:

//AVOID!!!
if (document.all) {  //IE
    id = document.uniqueID;
} else {
    id = Math.random();
}

这个代码的问题是通过检测document.all属性来确定是否是IE。当确定是IE后,假定使用私有的document.uniqueID属性也是安全的。然而,目前所作的只是确定是否支持document.all,并非是去辨识浏览器是否为IE。仅仅支持document.all的话也不意味着document.uniqueID是可用的。

后来人们开始这样写,用下面那行代替上面的:

var isIE = navigator.userAgent.indexOf("MSIE") > -1;
//下面这行代替上面那行
var isIE = !!document.all;

这些变化说明大家对“不要使用UA嗅探”存在误解——不再对浏览器的详细信息进行检测,取而代之的是通过特性的支持来推断。这种基于浏览器特性检测的方式非常不好。

后来前端们发现document.all并不可靠,更好的检测IE变为:

var isIE = !!document.all && document.uniqueID;

这种实现方式陷入歧途。不仅需要费时费事地去识别浏览器所增加的特性支持,另外也不能确定其它浏览器开始支持相同的特性。

如果你认为这样的代码并未被广泛使用,那么看看来自于老版本的Mootools代码片段吧:

//from MooTools 1.1.2
if (window.ActiveXObject) window.ie = window[window.XMLHttpRequest ? 'ie7' : 'ie6'] = true;
else if (document.childNodes && !document.all && !navigator.taintEnabled) window.webkit = window[window.xpath ? 'webkit420' : 'webkit419'] = true;
else if (document.getBoxObjectFor != null || window.mozInnerScreenX != null) window.gecko = true;

注意它是如何使用特性检测的。我可以指出它一系列的问题,比如通过检测window.ie会将ie8误认为ie7。

余波

随着浏览器的快速发展,使用特性检测变得越来越困难和不可靠。但是Mootools 1.2.4仍然使用这一方法,例如:getBoxObjectFor()

//from MooTools 1.2.4
var Browser = $merge({

	Engine: {name: 'unknown', version: 0},

	Platform: {name: (window.orientation != undefined) ? 'ipod' : (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase()},

	Features: {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)},

	Plugins: {},

	Engines: {

		presto: function(){
			return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925));
		},

		trident: function(){
			return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4);
		},

		webkit: function(){
			return (navigator.taintEnabled) ? false : ((Browser.Features.xpath) ? ((Browser.Features.query) ? 525 : 420) : 419);
		},

		gecko: function(){
			return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18);
		}

	}

}, Browser || {});

应该怎么做?

特性检测是个应该避免的方法,尽管直接进行特性检测是个很好的方法,并且大部分情况下能满足需求。一般只要在检测前知道这个特性是否被实现即可,而不会去考虑它们之间的关系。

我并非是说永远不使用浏览器特性检测而是基于UA嗅探,因为我相信它还是有很多用途的,然而我不相信它有很多合理的用途。如果你考虑UA嗅探的话,请先贯彻这一思想:唯一安全的方式是针对特定浏览器的特定版本,超出范围之外都是不可靠的——例如新出的浏览器版本。其实这样做也是个明智的办法,因为相较于向前兼容不确定的新版本而言,向后兼容老版本是最简单的做法。