柯里化(currying),维基百科的解释是“在计算机科学中,柯里化(Currying),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 哈斯凯尔·加里 命名的,尽管它是 Moses Schönfinkel 和 Gottlob Frege 发明的。”(http://zh.wikipedia.org/wiki/Currying)

微博上看到一群大牛们在说currying的事情,所以自己实现了一个把给定函数给柯里化的方法:

function curry(fn) {
    var args = [];
    var argLen = fn.length;
    var currying = function(){
    	for (var i  = 0; i < arguments.length; i++) {
            args.push(arguments[i]);
        }
        if (args.length >= argLen) {
        	return fn.apply(this, args);
        } else {
        	return currying;
        }
    }
    return currying;
}

/*以下测试*/
function fn(arg1, arg2, arg3, arg4) {
	return arg1 + arg2 + arg3 + arg4;
}
f = curry(fn);
f(1,2)(3)(4);

结果如下:

搜索了一下,发现在2007年,大牛winter就有currying的实现(http://bbs.51js.com/forum.php?mod=viewthread&tid=74706),代码如下:

function currying(func)
{
    return function()
    {
        if(arguments.length<func.length)
        {
            var args=arguments;
            var retarg=new Array(func.length-arguments.length);
            for(var i=0;i<retarg.length;i++)retarg="_"+i;
            eval("var ret=function("+retarg+"){return args.callee.apply(this,Array.prototype.slice.call(args).concat(Array.prototype.slice.call(arguments)));}");
            return currying(ret);
        }
        else return func.apply(this,arguments);
    }
}
/////////////////////////////////////////////////////////////////////////////////////////
function f(a,b,c)
{
    alert([a,b,c]);
}
var cuf=currying(f);
cuf(1)(2)(3);

har文件是一种记录页面加载过程的标准格式,最近需要将这个文件可视化的展现出来。网上也有一些能够可视化展现har的工具,其中harviewer算是其中比较优秀的一个,项目主页:http://code.google.com/p/harviewer/。
但是试用了一段时间之后发现了一些问题,其中最严重的一个问题就是在IE8及更早版本中使用官方文档中说的如下方式调用的时候会无效:

	$("#content").bind("onViewerPreInit", function(event){
		    // Get application object
		    var viewer = event.target.repObject;
		    viewer.loadHar("z.har");
	});

原因是在代码中作者使用了document.createEvent来实现fireEvent函数,但是这个函数在低版本的IE中是不受支持的。我修改了原来的fireEvent函数,使用jquery来实现,可以达到很好的浏览器兼容性。不解的是,作者在项目中使用了jquery,但是这里却要自己去实现,这么写不混乱么?

修改后的代码我上传到了github,同时还进行了部分汉化工作,地址:https://github.com/zhujianfeng/harviewer

作为一个菜鸟,最近学习了一下Jquery插件开发,写完一个练手的插件之后发现在Jquery上开发插件实在是很令人愉悦。Jquery插件分为两种,一种是类级别的,另一种是对象级别的。由于这篇文章里我学习的是对象级别的,所以本文主要记录对象级别插件开发。
首先插件开发需要对Jquery的fn进行扩展,一般采取下述几种方式之一

//方式一
jQuery.fn.myPlugin = function() {
    //我们的代码
};

//方式二
$.fn.myPlugin = function() {
    //我们的代码
};

//方式三
jQuery.fn.extend({     
    myPlugin:function(){     
        //我们的代码       
    }     
});

//方式四
$.fn.extend({     
    myPlugin:function(){     
        //我们的代码       
    }     
})

这里面的$符号其实就是变量jQuery的一个别名而已,另外,我们可以使用extend方法来扩展fn,也可以直接在fn上定义新的对象。除此之外,我们还需要把插件封装起来,防止插件的代码污染其他逻辑代码,所以使用一个匿名函数来做这件事情

(function( $ ) {
  $.fn.myPlugin = function() {
    // 我们的代码
  };
})( jQuery );

这样我们就可以使用$(“somediv”).myPlugin()来使用这个插件了。为了能够让插件有多个方法可用,那么可以使用如下方式来组织代码

(function( $ ){

  var methods = {
    init : function( options ) { 
      // 代码
    },
    show : function( ) {
      // 代码
    },
    hide : function( ) { 
      // 代码
    },
    update : function( content ) { 
      // 代码 
    }
  };

  $.fn.myPlugin = function( method ) {
    
    if ( methods[method] ) {
      return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Method ' +  method + ' does not exist' );
    }    
  
  };

})( jQuery );

//可以使用如下方式来调用
$("somediv").myPlugin({...}); //调用了init方法
$("somediv").myPlugin("update",{...});//调用了update方法

代码会首先判断调用者传入的第一个参数所对应的函数名在不在定义的函数哈希里,在的话使用apply方法调用该函数,并把被调用函数的this设置为当前this,同时把剩余的参数传给被调用的函数。如果第一个参数是一个对象或者为空,那么同样使用apply调用init初始化方法。
在插件运行过程中,如果有数据需要存储,比如上例中的options需要存储起来,供插件运行时其他方法使用,那么我们最好使用jquery的data方法来管理。

(function( $ ){

  var methods = {
    init : function( options ) { 
      // 这里存储数据
      $(this).data("mydata", options);
      // 其他代码
    },
    show : function( ) {
      // 这里使用数据
      var myData = $(this).data("mydata");
      //其他代码
    },
    hide : function( ) { 
      // 代码
    },
    update : function( content ) { 
      // 代码 
    }
  };

  $.fn.myPlugin = function( method ) {
    
    if ( methods[method] ) {
      return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Method ' +  method + ' does not exist' );
    }    
  
  };

})( jQuery );

最后放上练手的例子,一个可以点击表头排序的表格插件

(function($) {
	var arrowUp = "▲", arrowDown = "▼";
	var sortArray = function(arr, sortBy, order) {
		var sortFunc = function(a, b) {
			if (order === "desc") {
				console.log(typeof a[sortBy]);
				if (typeof a[sortBy] === "string" && typeof b[sortBy] === "string") {
					return a[sortBy].localeCompare(b[sortBy]);
				} else {
					return b[sortBy] - a[sortBy];
				}
			} else {
				if (typeof a[sortBy] === "string" && typeof b[sortBy] === "string") {
						return b[sortBy].localeCompare(a[sortBy]);
				} else {
					return a[sortBy] - b[sortBy];
				}
			}
		};
		return arr.sort(sortFunc);
	};
	var drawBody = function(options){
		var dCount = 0, ddCount = 0;
		dCount = options["data"].length;
		ddCount = options["data"][0].length;
		var htm = [];
		for (var i = 0; i < dCount; i++) {
			htm.push("<tr>");
			for (var j = 0; j < ddCount; j++) {
				htm.push("<td>");
				htm.push(options["data"][i][j]);
				htm.push("</td>");
			}
			htm.push("</tr>");
		}
		var htmStr = htm.join("");
		$(this).find("div > table > tbody").html(htmStr);
	};
	var methods = {
		"init" : function(opts) {
			var options = $.extend({
				"thead" : ["c1", "c2"],
				"data" : [[1, 2], [3, 4],[5, 3]],
				"sortBy": 1,
				"order" : "desc"
			},opts);
			var headStr = "";
			cCount = options.thead.length;
			var colWidthAll = 100 / cCount;
			for (var i = 0; i < cCount; i++) {
				var arrow = "";
				if (options.sortBy === i) {
					if (options.order === "desc") {
						arrow = arrowDown;
					} else {
						arrow = arrowUp;
					}
				}
				var thWidth = "";
				if (options.colWidth) {
					if (options.colWidth[i]) {
						thWidth = "width='" + options.colWidth[i] + "'";
					}
				} else {
					thWidth = "width='" + colWidthAll + "%'"
				}
				headStr += "<th " + thWidth + "><span rel='" + i + "'>" + options.thead[i] 
						+ " <span>" + arrow + "</span></span></th>";
			}
			var tableStr = "<div class='sGrid_wrap'><table class='sGrid'>"
								+"<thead><tr>"
								+ headStr
								+"</tr></thead>"
								+"<tbody></tbody>"
							+"</table></div>";
			$(this).html(tableStr);
			
			options["data"] = sortArray(options["data"], options["sortBy"], options["order"]);
			drawBody.apply(this, [options]);
			var me = $(this);
			$(this).find("div > table > thead th span").click(function(){
				var c = $(this).attr("rel");
				if (c == options.sortBy) {
					if (options.order === "desc"){
						options.order = "asc";
						$(this).find("span").html(arrowUp);
					} else {
						options.order = "desc";
						$(this).find("span").html(arrowDown);
					}
				} else {
					me.find("div > table > thead th span span").html("");
					options.sortBy = c;
					options.order = "desc";
					$(this).find("span").html(arrowDown);
				}
				options["data"] = sortArray(options["data"], options["sortBy"], options["order"]);
				drawBody.apply(me, [options]);
			});
		}
	};
	$.fn.rankTable = function(method) {
		if ( methods[method] ) {
      		return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    	} else if ( typeof method === 'object' || ! method ) {
      		return methods.init.apply( this, arguments );
    	} else {
      		$.error( 'Method ' +  method + ' does not exist' );
    	}
	};
})(jQuery);
//调用方式
<html xmlns="http://www.w3.org/1999/xhtml">  
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <title>rankTable</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js" type="text/javascript"></script> 
    <script src="jquery.ranktable.js" type="text/javascript"></script>
    <link href="style.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript">
        $(function () {
            $("#rank_table").rankTable({
                "thead" : ["姓名", "年龄", "成绩"],
                "data" : [["张三", 16, 98], ["李四", 15, 88], ["王五", 18, 100], ["孙六", 17, 60]],
                "sortBy": 2,
                "order" : "desc",
                "colWidth" : ["33%","33%"]
            });

        });  
    </script>  
</head>  
<body> 
    <div id="rank_table">  
    </div>
</body>  
</html> 

今天贝贝同学问了我一个问题,怎样在可编辑的div中的光标处插入图片。网上search了一把,果然如贝贝所说,一堆都是往光标处插入文本的。
最后发现使用document.execCommand()方法即可简单的往光标处插入图片。在chrome和firefox下只需要执行document.execCommand(‘InsertImage’, false, _imgUrl);就能达到目的,可是在IE下就有bug:当光标停留在某个地方,不选中任何东西的情况下,该方法无效。当选中编辑区内的一部分内容时,该方法有效。经过一番摸索,终于找到解决方案。原来在不选中任何东西的情况下,当我去点击插入按钮时,编辑区就失去了焦点,这个时候插入会失败。所以需要先保存焦点状态,插入之前恢复状态即可。具体内容看代码

<html>
<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <title>edit</title>
</head>
<body>
    <div id="editor" 
        style="width:400px;margin:100px auto;height:300px;border:1px solid #DFDFDF;" 
        contenteditable="true">
        这里插入一个图片!
    </div>
    <a href="javascript:void(0);" onclick="edit()">强力插入!</a>
    <script>
    var editor = document.getElementById("editor");
    var range, bookmark;
    var saveFocus = function(){//保存焦点状态
        if (document.selection) { //只有坑爹的IE才执行下面的代码
            range = document.selection.createRange();
            bookmark=range.getBookmark();
        }
    }
    editor.onclick = saveFocus;//在鼠标点击编辑区时保存焦点
    editor.onkeydown = saveFocus;//在输入内容时也保存焦点
    function edit() {
        insertImg("http://www.baidu.com/img/baidu_sylogo1.gif");
    }
    function insertImg(_img) {
        if (range) { //同样,坑爹IE专用代码
            range.moveToBookmark(bookmark);
            range.select();
        }
        document.execCommand('InsertImage', false, _img);

    }
    </script>
</body>
</html>

将代码中方法document.execCommand(‘InsertImage’, false, _img)更改参数,应该可以插入html代码等其他元素。

工作中有个功能想用js的eval来实现,上网搜了一把,一堆说eval不好的,总结一下,记在这里。

Eval函数是js中用来执行一段字符串的,当然前提是这段字符串的内容的是js代码。但是为什么是evil的呢,原因如下:

1、使用eval的代码不好调试。一般使用eval的地方,代码都是动态生成的,虽然动态生成具有很大的灵活性,但是一旦出错,很难找出bug在哪里,毕竟你要调试的代码没有写在静态文件里。
2、使用eval的代码不好维护。想一下,一个程序员写的代码,几个月之后回去再看,可能需要看一阵子才能明白写的是什么,如果你用eval,那么就意味着你写了一段生成代码的代码,维护起来代价就更高了。
3、性能差。eval在执行的时候意味着浏览器又要开一个解释器(实际也许不是这样,但是总需要做点什么才能让这段文本执行起来),开解释器的这个过程代价是很高的,导致性能会急剧下降。例如下面的两段代码,片段1中使用了eval,在我的chrome上执行,平均耗时在2300ms左右,片段2没有使用eval,平均耗时只有150ms左右。可见,性能差距巨大。

//片段1
var count = 1;
var t1 = new Date();
for (var i = 0; i < 10000000; i++){
	eval("count = count + 1;");
}
var t2 = new Date();
alert(t2 - t1);

//片段2
var count = 1;
var t1 = new Date();
for (var i = 0; i < 10000000; i++){
	count = count + 1;
}
var t2 = new Date();
alert(t2 - t1);

4、可能会有注入危险。设想这么一个情况,你从用户那里接收一段js代码字符串,然后在服务端使用类似于Node之类的技术来使用eval执行用户输入的代码,那么用户就完全可以写一段代码来获得你的机器控制权。

5、影响优化。现代的js解释器一般都会对js代码进行优化,但是当你的代码里有eval的时候,就意味着在执行之前这段代码是不固定的,解释器就不知道该怎么优化,所以当解释器碰到eval的时候基本就放弃优化了。

但是一直没弄明白的是,既然这玩意这么eval,那么为什么当时把他设计出来了呢?

已经把这事搁置很久了,前一段时间主要是毕业的事情比较多,现在终于能有一点时间继续写了。好了,进入正题,本节的重点在于WebSocket。
由于各大浏览器对WebSocket的支持各不相同,所以直接使用浏览器提供的API会比较复杂。Socket.IO对这些API做了封装,并且还使用其他方式对不支持WebSocket的浏览器做了支持,使用Socket.IO会减轻很多工作量。
使用Socket.IO只需要在express里安装Socket.IO模块,同时在页面中加入/socket.io/socket.io.js的引用,服务端包含socket.io包即可。服务端代码如下:

var express = require('express')
  , routes = require('./routes');

var app = module.exports = express.createServer();
var io = require('socket.io').listen(app);
// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.set('view options',{layout:false});
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

// Routes

app.get('/', routes.index);
var conns = {};
var userNames = {};
var sids = {};
var onLineList = [];
io.sockets.on('connection',function(socket){
	var sid=socket.id;
	conns[sid]=socket;
    socket.on('submitUserName',function(userName, fn) {
        if (userNames[userName]) {
            fn("have");
        } else {
            for (var i = onLineList.length - 1; i >= 0; i--) {
                var tmpSid = userNames[onLineList[i]]["sid"];
                conns[tmpSid].emit('addNewUserToList', userName);
            }
            var userInfo = {};
            userInfo["sid"] = sid;
            userNames[userName] = userInfo;
            sids[sid] = userName;
            onLineList.push(userName);
            fn("not_have");

        }
    });
    socket.on('getOnlineList', function(userName, fn){
        fn(onLineList);
    });
    socket.on('setCompetitor', function(competitor, fn){
        var myName = sids[socket.id];
        for (var i = onLineList.length -1; i >= 0; i--) {
            if (onLineList[i] == myName) {
                onLineList.splice(i, 1);
            }
        }
        for (var i = onLineList.length -1; i >= 0; i--) {
            if (onLineList[i] == competitor) {
                onLineList.splice(i, 1);
            }
        }
        var deletedUser = [myName, competitor];
        for (var i = onLineList.length -1; i >= 0; i--) {
            var tmpSid = userNames[onLineList[i]]["sid"];
            conns[tmpSid].emit('deleteUserFromList', deletedUser);
        }
        if (!userNames[myName]["competitor"] && !userNames[competitor]["competitor"]) {
            userNames[myName]["competitor"] = competitor;
            userNames[competitor]["competitor"] = myName;
            var comSid = userNames[competitor]["sid"];
            conns[comSid].emit('beginChess', 'begin');
            fn("success");
            socket.on('goChess', function(data){
				conns[comSid].emit('comGoChess', data);
            });
            conns[comSid].on('goChess', function(data){
                socket.emit('comGoChess', data);
            });
        } else {
            fn("fail");
        }
    });
});
app.listen(3000, function(){
  console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});

代码前半部分都是Express框架的设置部分,然后io.sockets.on();部分开始监听来自浏览器端的WebSocket连接。连接建立之后,会将该连接存起来,并在该连接上设置一些可能的监听事件。

socket.on('submitUserName',function(userName, fn){});
//用户提交自己的名字时触发的事件

socket.on('getOnlineList', function(userName, fn){});
//用户获取在线等待配对用户列表时触发的事件

socket.on('setCompetitor', function(competitor, fn){});
//用户选中一个对手之后设置对手的事件

浏览器端在触发事件时会发送一些数据给服务端,比如socket.on(‘submitUserName’,function(userName, fn){})中,userName是浏览器端发送过来的用户名,而fn则是浏览器端定义的一个函数回调函数,当服务端完成某些操作后,会调用fn函数,相当于浏览器端得到了服务端的响应。当然,这里也可以不设置fn,只发送数据。关于socket.io的具体情况可以查看官方文档,地址:http://socket.io/#how-to-use
浏览器端会根据用户的一些操作触发一些服务端事件,部分代码如下:

$(function(){
    $("#chessCanvas").hide();
    $("#PiecesCanvas").hide();
    $("#onlinelist").hide();
    socket = io.connect();
    window.submitUserName = function(){
        var userName = $("#username").val();
        if (userName) {
            socket.emit('submitUserName', userName, function(data){
                if (data == "have") {
                    alert("用户名已经被使用,请重新取一个更酷的名字吧!");
                    $("#username").val("");
                    $("#username").focus();
                } else {
                    socket.emit('getOnlineList', userName, function(list){
                        $.each(list, function(i, item){
                            if (item == userName){
                                return true;
                            }
                            var str = '<div id="' + item + '"><input type="radio" name="onlineuser" value="'
                                + item + '" /><span>' + item + '</span></div>';
                            $("#namelist").append(str);
                        });
                        $("#login").hide();
                        $("#onlinelist").show();
                    });
                }
            });
            socket.on('addNewUserToList', function(data){
                var str = '<div id="' + data + '"><input type="radio" name="onlineuser" value="'
                    + data + '" /><span>' + data + '</span></div>';
                $("#namelist").append(str);
            });
            socket.on('deleteUserFromList', function(data) {
                $.each(data, function(i, item) {
                    $("#namelist > #" + item).remove();
                });
            });
            socket.on('beginChess', function(data) {
                showChess('red', 'wait', socket);
            });
        } else {
            alert("请输入用户名!");
        }

    };
    window.beginChess = function(){
        var competitor = $("input[name='onlineuser']:checked").val();
        if (competitor) {
            socket.emit('setCompetitor', competitor, function(data){
                if (data == "success") {
                    showChess('black', 'idle', socket);
                } else {
                    //未配对成功的情况
                }
            });
        } else {
            alert("请选择一个对手!");
        }
    };
    function showChess(ourColor, status, sock) {
        $("#onlinelist").hide();
        $("#chessCanvas").show();
        $("#PiecesCanvas").show();
        window.chss = chess("chessCanvas", "PiecesCanvas", sock);
        window.chss.ourColor = ourColor;
        window.chss.status = status;
        window.chss.initPieces();
    }

});

当用户输入完用户名之后,点击登录,就会使用socket.emit(‘submitUserName’, userName, function(data){})触发服务端响应。同时,浏览器端也会监听一些服务端事件,例如socket.on(‘beginChess’, function(data) {})就监听了服务端发起的棋局开始事件。这就是WebSocket的强大之处,浏览器可以被动接收服务端发送的消息,换个角度说,这一刻的浏览器就是C/S结构中的服务端,而服务器则是C/S结构中的客户端。当棋局开始后,浏览器使用如下代码监听服务器发送过来的对方走子情况,并在本地执行。

//监听服务端发送过来的对方棋子移动的事件
me.socket.on('comGoChess', function(movement) {
    var lineX = 10 - movement.toLineX;
    var lineY = 11 - movement.toLineY;
    var comPiece = movement.piece;
    me.pieceMoveTo('opposite', comPiece, lineX, lineY);
    me.status = 'idle';
});

当然,在本地走子之后,也会将走子信息发送给服务器,服务器再发送到对方。本子走子信息通过如下代码向服务端发送。

var movement = {};
movement["piece"] = me.activePiece;
movement["toLineX"] = lineX;
movement["toLineY"] = lineY;
me.socket.emit('goChess', movement);

到这里,仅仅实现了配对,同步走子的功能。判断输赢和结束棋局重来棋局等功能还未实现。最后,再放截图一张。

互联网产品开发过程中,想了解用户体验一直是一件比较困难的事情。之前很难使用js获得用户访问网站的连接建立时间、dns时间等信息,想得到这些信息一般是建立固定的监测点或者使用专门的测试客户端软件。不过,现在情况有所改变,IE9和chrome6以上的版本都支持了一个新的api:window.performance( ie9中为window.msPerformance,chrome6-9为window.webkitPerformance,chrome10中是window.performance)(2012/6/19注:今天在本博客的测速日志里发现了firefox13和360浏览器,这说明现在这个api至少又多了两个浏览器支持)。该api目前在W3C上的状态是“Candidate Recommendation”,相信会有越来越多的浏览器支持。废话不多说,我们来看一下这个api能干哪些事情。

首先,该api的的接口定义为:


interface PerformanceTiming {
 readonly attribute unsigned long long navigationStart;
 readonly attribute unsigned long long unloadEventStart;
 readonly attribute unsigned long long unloadEventEnd;
 readonly attribute unsigned long long redirectStart;
 readonly attribute unsigned long long redirectEnd;
 readonly attribute unsigned long long fetchStart;
 readonly attribute unsigned long long domainLookupStart;
 readonly attribute unsigned long long domainLookupEnd;
 readonly attribute unsigned long long connectStart;
 readonly attribute unsigned long long connectEnd;
 readonly attribute unsigned long long secureConnectionStart;
 readonly attribute unsigned long long requestStart;
 readonly attribute unsigned long long responseStart;
 readonly attribute unsigned long long responseEnd;
 readonly attribute unsigned long long domLoading;
 readonly attribute unsigned long long domInteractive;
 readonly attribute unsigned long long domContentLoadedEventStart;
 readonly attribute unsigned long long domContentLoadedEventEnd;
 readonly attribute unsigned long long domComplete;
 readonly attribute unsigned long long loadEventStart;
 readonly attribute unsigned long long loadEventEnd;
};

这些变量都表示神马意思呢?莫急,看看浏览器的一般加载顺序,下图所示:
浏览器页面加载顺序图从上图就可以看出这些变量的具体含义了,这时想得到用户浏览器加载过程的各项指标就简单了。比如想拿到用户的dns时间,只需要拿domainLookupEnd减去domainLookupStart即可。

不过,在chrome下,这里有一个坑,按照W3C的标准,navigationStart应该是整个过程的开始,也就是时间点应该最早。但是chrome下很多时候不是这样,会有domainLookupStart时间早于navigationStart的情况,初步认为应该是chrome的各种优化机制和预渲染功能打乱了上图的顺序。IE9相对守规矩一点。

使用该api时需要在页面完全加载完成之后才能使用,最简单的办法是在window.onload事件中读取各种数据,因为很多值必须在页面完全加载之后才能得出。