前段时间对公司已有项目JavaScript代码进行优化,本文的是对优化工作的一个总结,拿出来与大家分享。当然我的优化方式可能并不是最优的,或者说有些不对的地方,请指教。
JavaScript优化总结分为以下几点
优化前后对比
优化前
优化后
代码混乱,同样功能的函数重复出现在多个地方。如果需要修改实现,需要找到所有的地方。牵一发而动全身 模块化,提取公共接口组织为库、结构清晰、方便代码重用、并且能够游戏防止变量污染问题。
JavaScript文件未压缩,size比较大加载消耗网络耗时,阻塞页面渲染 JavaScript公共库文件使用UglifyJS压缩:
● Size比较小优化了网络加载时间
● 压缩混淆了代码,在一定程度上保护代码
使用时需要加载多个单独的JavaScript文件,增加了http请求数降低性能 对公共库合并压缩在减少size的同时,减少http请求数
缺乏文档(让后面的开发者对已有功能不清楚,这在一定程度上造成前面说的,同样功能的函数重复出现在多个地方) 公共库中每个类、函数、属性都有说明文档
● 模块化(类编程):代码清晰、有效防止变量污染问题、代码重用方便扩展等;
● JavaScript压缩混淆:减少size优化加载时间,混淆保护代码;
● JavaScript文件合并:减少http请求优化网络耗时提升性能;
● 生成文档:方便公共库的使用,查找接口方便。
模块化(类编程)
对于静态类来说JavaScript实现比较简单,使用Object直接量就已经够用了;但是要创建实例化、可继承经典的类需要做一番工作。因为JavaScript是基于原型的(prototype-based)编程语言,并没有包含内置类的实现(它没有访问控制符,它没有定义类的关键字class,它没有支持继承的extend或冒号,它也没有用来支持虚函数的virtual等),但是我们通过JavaScript可以轻易地模拟出经典的类。
静态类
根据宝宝JS公共接口的特性,它们不需要实例化,所以优化使用了该方式。下面以PetConfigParser为例介绍下实现方式:
var PetConfigParser;
if (!PetConfigParser) {
PetConfigParser = {};
}
(function () {
//private 变量、函数
/**
* 宝宝所有配置字典,以【cate * 10000 + (lvl - 1) * 10 + dex - 1】为key
* @attribute petDic
* @type {Object}
* @private
*/
var petDic = null; //宝宝字典
/**
* 根据__pet_config构建一个Object字典,以cate、dex、lvl组合作为key
* @method buildPetDic
* @private
* @return {void}
*/
function buildPetDic() {
petDic = new Object();
for (var item in __pet_config) {
var lvl = parseInt(__pet_config[item]['lvl']);
var dex = parseInt(__pet_config[item]['dex']);
var cate = parseInt(__pet_config[item]['cate']);
var key = cate * 10000 + (lvl - 1) * 10 + dex;
petDic[key] = __pet_config[item];
}
}
//public 接口
/**
* 根据宝宝id,读取__pet_config中对应宝宝的信息
* @method getPetById
* @param {String/int} petId 宝宝id
* @return {Object} pet 宝宝的所有静态信息,如{id:"300003289", lvl:"1", dex:"2", price:"200", life:"2592000", cate:"3", name:"飞天小使等级1熟练2", intro:"", skill:"护身符", skill1_prob:"30", skill2_prob:"0"}
*/
if (typeof PetConfigParser.getPetById !== 'function') {
PetConfigParser.getPetById = function (petId) {
var pet = ("undefined" == typeof (__pet_config)) ? null : __pet_config["pet_" + petId];
return pet;
}
}
})();
这种方式利用了JavaScript匿名函数来创建私有作用域,这些私有作用域只能在内部访问。总结上述过程分为以下几个步骤:
1) 定义一个全局的变量(var PetConfigParser),注意变量首字母大写与普通变量区别;
2) 然后创建一个匿名函数并运行( (function () {/*xxxx*/ })(); ),在匿名函数内部创建局部变量和函数,它们只能在当前作用域中被访问到;
3) 全局变量(var PetConfigParser)可以在任何地方访问到,在匿名函数内部操作PetConfigParser添加静态函数。
使用实例:
$(function () {
DialogManager.init();
$('#showDialog').click(function () {
DialogManager.show("#msgBoxTest", "#closeId");
return false;
});
$('#cofirmBtn').click(function () {
DialogManager.hide();
return false;
});
})
实例类
JavaScript实现经典的类,总结有三种方法:
● 构造函数方式;
● 原型方式;
● 构造函数+原型的混合方式
构造函数方式
构造函数用来初始化实例对象的属性和值。任何JavaScript函数都可以用作构造函数,构造函数必须使用new运算符作为前缀来创建新的实例。
var Person = function (name) {
this.name = name;
this.sayName = function(){
alert(this.name);
};
}
//实例化
var tyler = new Person("tylerzhu");
var saylor = new Person("saylorzhu");
tyler.sayName();
saylor.sayName();
//检查实例
alert(tyler instanceof Person);
构造函数方式跟传统的面向对象语言是不是很相识!只不过是class关键字用function替换了。
注意:不要省略new否则Person(“tylerzhu”) //==>undefined。当使用new关键字来调用构造函数时,执行上下文(context)从全局对象(window)变成一个空的上下文,这个上下文代表了新生成的实例。因此,this关键子指向当前创建的实例。所以省略new时,没有进行上下文切换会在全局对象中查找name,没有找到而创建一个全局变量name返回undefined。
原型方式
构造函数方式简单,但是存在一个浪费内存的问题。如上面的例子中实例化了两个对象tyler、saylor,表面上好像没什么问题,但是实际上对于每一个实例对象,sayName()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容申请内容。
alert(tyler. sayName == saylor. sayName) 输出false!!!
Javascript中每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例共享。
var Person = function (name) {
Person.prototype = name;
Person.prototype.sayName = function(){
alert(this.name);
}
}
//实例化
var tyler = new Person("tylerzhu");
var saylor = new Person("saylorzhu");
tyler.sayName();
saylor.sayName();
//检查实例
alert(tyler instanceof Person);
这时tyler、saylor实例的sayName方法,都是同一个内存地址(指向prototype对象),因此原型方法更节省内存。
但是看tyler.sayName();saylor.sayName();两者输出,会看出问题 —— 它们都输出“saylorzhu”。因为原型所有属性都共享,只要一个实例改变其他的都会跟着改变,所以实例化对象saylor覆盖了tyler。
构造函数+原型的混合方式
构造函数方式可以为同一个类的每一个对象分配不同的内存,这很适合写类的时候设置属性;但是设置方法的时候我们就需要让同一个类的不同对象共享同一个内存了,写方法用原型的方式最好。所以写类的时候需要把构造方法和原型两种方式混合着用(很多类库提供的创建类的方法或框架的写类方式本质上都是:构造函数+原型)。
var Person = function (name) {
Person.prototype = name;
Person.prototype.sayName = function(){
alert(this.name);
}
}
//实例化
var tyler = new Person("tylerzhu");
var saylor = new Person("saylorzhu");
tyler.sayName();
saylor.sayName();
//检查实例
alert(tyler instanceof Person);
这样即可通过构造函数构造不同name的人,对象实例也都共享sayName方法,不会造成内存浪费。
JavaScript压缩/合并
JavaScript代码压缩混淆的意义:简单的说就是为了减小js文件大小,去掉多余的注释和换行缩进等,使得下载起来更快,提高用户体验。
JavaScript压缩工具有很多,我推荐使用jQuery现在使用的工具UglifyJS(jQuery以前也使用过多种压缩工具,如Packer),因为它压缩性能很好。
“jQuery 1.5 发布的时候 john resig 大神说所用的代码优化程序从Google Closure切换到UglifyJS,新工具的压缩效果非常令人满意”