继续我们的 Javascript 面向对象之旅。上次已经提到过怎么去声明个 Javascript 类了,这篇主要是说明如何去继承 Javascript 类。
相对于其他语言而言,Javascript 类的机制显得尤其的宽松。这似乎是把双刃剑,使用不当就有可能割伤自己的手指。Javascript 没有严格意义上的抽象类的概念,这就意味着任何的类都可以被实例化。
在开始下面的文章前,我们首先重新认识下上篇我们已经定义好的类。
function Car(sColor) {
this.color = sColor;
this.showColor = function() {
alert(this.color);
}
}是的,看起来非常好理解。那么接下来就开始在 Car 上加装点东西使它变成更具体化。
对象冒充
对象冒充(object masquerading)是日常 coding 中最常见的一种类继承的手法。其原理就是,构造函数使用 this 关键字给所有的属性和方法赋值。
因为构造函数只是一个函数,所以可以使基类的构造函数成为继承类的一个方法。这样,继承类就能收到基类构造函数中定义的属性和方法。例如上述的 Car 基类就可以这样继承。
function Trunk(sColor, sName) {
this.Car = Car;
this.Car(sColor);
delete this.Car;
this.name = sName;
this.sayName = function () {
alert(this.name);
}
}在这里要需要注意一点,新属性和新的方法比必须在删除了新方法的代码后定义,否则可能会覆盖继承类的相关属性和方法。
深刻理解 Javascript 的 this 和函数机制是非常有用的。依次类推,我们就可以给让一个类继承多个基类(多重继承)。
call() 方法
了解对象冒充的机制以后使用 call() 方法就非常容易理解了。从代码量上理解,它仅仅是将上述方法的三条语句合并成一条(是的,没有任何的不同)。
function Trunk(sColor, sName) {
//this.Car = Car;
//this.Car(sColor);
//delete this.Car;
Car.call(this, sColor);
this.name = sName;
this.sayName = function () {
alert(this.name);
}
}有关如何使用 call() 的其他信息,可以访问这里获得。
apply() 方法
相对于 call() 方法,自然对于 apply() 方法也同样能够运行。只不过其参数是以数组的形式传递,请参考下面的代码。
function Trunk(sColor, sName) {
//this.Car = Car;
//this.Car(sColor);
//delete this.Car;
Car.apply(this, [sColor]);
this.name = sName;
this.sayName = function () {
alert(this.name);
}
}当然,继承类的参数顺序与基类的参数顺序完全一致才可以传递参数对象。即便只有一个参数,也需要使用数组传递(比如上述的例子)。
原型链
上篇文章已经介绍了使用原型方式定义类,而原型链扩展了这种方式,这种继承的方式非常的有趣。其实 prototype 对象是个模板,要实例化的对象都以这个模板为基础(我个人将这里的“模板”理解为指针)。
总而言之,prototype 对象的任何属性和方法都会被传递给那个类的所有实例,而原型链就是利用这种功能来实现继承机制,且看下面的代码实现。
function Car() {
}
Car.prototype.color = "red";
Car.prototype.showColor = function () {
alert(this.color);
}
Trunk.prototype = new Car;从代码中可以得知,调用 Car 的构造函数时,没有给它传递任何的参数。这在原型链中视标准的做法,要确保构造函数没有任何的参数。
原型链的弊端就是不支持多重继承,同时与艳星连会用另一类型的对象重写基类的 prototype 属性(就犹如上篇类的声明说所言的道理一样,所以我个人将其理解为指针)。
混合模式
同类的声明机制一样,两种方法(对象冒充和原型链)也可以结合在一起实现继承机制。比如下面是一个非常“完美的”类声明。
function Car(sColor) {
this.color = sColor;
if (typeof Car._initialized == "undefined") {
Car.prototype.showColor = function() {
alert(this.color);
};
Car._initialized = true;
}
}要继承这个类必须使用两种方式同时使用,应为类的声明中也使用了两种方式。
function Trunk(sColor, sName) {
Car.call(this, sColor);
this.name = sName;
}
Trunk.prototype = new Car();
Trunk.prototype.showName = function () {
alert(this.showName);
}由于混合方式使用了原型链,所以 instanceof 运算符仍然能够正确的运行。
理论方面的东西就到这里为止,接下来计划说点实际的东西。不过还是调下大家的胃口,我打算安排在下一篇文章中说明,敬请期待。
未完待续
有时间重新审视了遍 Javascript 的面向对象机制。与其他的语言不同,Javascript 可以说提供了更灵活的面向对象机制(比如 function 在完成其自身功能的同时也是对象)。
由于才疏学浅,我不得不将《Javascript 高级程序设计》中的部分内容摘抄过来,这些同时也算是我的读书笔记吧。由于 Javascript 面向对象机制及其的重要,而且内容非常的繁多,在这里就分篇章逐个介绍。
使用对象首先就是声明它(内置的对象当然就不需要了)。该死的 Javascript 总是会让我们死去很多的脑细胞,这篇文章主要说明下声明 Javascript 类的几种方法。
工厂模式
工厂模式可能是很多开发人员使用的一种模式,简单的说这种方法先定义“地基”,然后在往上面扔(绑定)各种功能和属性。下面的代码可能看起来会非常的熟悉:
var oCar = new Object;
oCar.color = "red";
oCar.showColor = function() {
alert(this.color);
}
oCar.showColor();当然,既然包装成一个类,就要重用它(上面的方法从语法上说仅仅是变量)。可以使用返回特定对象的工厂函数(factory function)将其封装起来:
function createCar() {
var oCar = new Object;
oCar.color = "red";
oCar.showColor = function() {
alert(this.color);
}
return oCar;
}
oCar = createCar();
oCar.showColor();当然,变通一下,可以在 createCar 函数上加入些参数,这样看起来已经非常地专业了:
function createCar(sColor) {
var oCar = new Object;
oCar.color = sColor;
oCar.showColor = function() {
alert(this.color);
}
return oCar;
}
oCar = createCar();
oCar.showColor();匿名函数总是让人感觉非常的高深,但是有时候也会迷惑了自己。如果不考虑篇幅,可以外部定义它:
function showColor() {
alert(this.color);
}
function createCar(sColor) {
var oCar = new Object;
oCar.color = sColor;
oCar.showColor = showColor;
return oCar;
}
oCar = createCar();
oCar.showColor();这样做还有一个好处,就是不用重复定义 oCar.showColor 了(高效率的程序每个人都喜欢)。
构造函数模式
构造函数其实和工厂方式差不多。从代码量上来说,就是省略了构造函数内部没有创建一个对象。
function Car(sColor) {
this.color = sColor;
this.showColor = function () {
alert(this.color);
}
}
oCar = new Car("red");
oCar.showColor();其实此隐含的对象已经在 new 以后就被实例化了。默认情况下,构造函数返回的就是其 this 的值(所以不必使用 return 返回)。但构造函数模式和工厂模式一样可能会重复定义方法,这点可以参考上述工厂模式的做法避免它(始终看起来不完美)。
原型模式
已经受够重复定义的问题了,那么有没有完美的解决办法呢?当然有。使用原型方法可以有效的避免此类的问题。
function Car() {}
Car.prototype.color = new Array("red", "green", "blue");
Car.prototype.showColor = function() {
alert(this.color);
}
oCar = new Car();
oCar.showColor();但是使用此模式需要注意的是类中的所有属性和方法都是共用的(其实就是指针)。这意味着虽然被实例化的两个变量,如果其中一处的值被更改,那么另外一个就也会被更改。
注:此段内容有更改,详细请参见这里和这里(感谢 fish 兄弟提出)。
混合模式
看起来越来越完美了,结合上述学到的方法就很容易解决原型模式的问题,这样看起来就更像是专业的程序员了。
function Car(sColor) {
this.color = sColor;
}
Car.prototype.showColor = function() {
alert(this.color);
}
oCar = new Car("red");
oCar.showColor();上述的方法声明的类, showColor 方法是原型(仅创建了一个实例),其他的都是构造(互不干扰)。
动态原型模式
把自己的方法仍在外面总不是件非常环保的事情,下面的方法就非常的“绿色”:
function Car() {
this.color = "red";
if (typeof Car._initialized == "undefined") {
Car.prototype.showColor = function() {
alert(this.color);
};
Car._initialized = true;
}
}
oCar = new Car("red");
oCar.showColor();此方法于上述的混合模式效果一致,即在构造函数内定义属性,而方法则使用原型模式。唯一的区别就是赋予对象方法的位置。
混合工厂模式
混合工厂模式可以认为是构造模式与混合模式的整合,因为 function 本身就是一个对象,所以可以使用 new 来实例化(请允许我这样描述)。
function Car() {
var oCar = new Object;
oCar.color = "red";
oCar.showColor = function() {
alert(this.color);
}
return oCar;
}
oCar = new Car();
oCar.showColor();不过建议避免使用此方法定义,因为于上述的工厂模式一样,它存在重复声明的问题。
选用何种模式?
其实通过上面的描述已经有所结果,通常使用的是 混合模式 与 动态原型模式 (我个人投动态原型模式一票)。不过不要单独使用 工厂模式 与 构造模式 (或者其两者的结合体),因为这样会造成不必要的浪费。
附,上述的代码打包下载。
未完待续
继续我们的 Javascript 优化计划,上期已经做到怎么尽可能的缩小 Javascript 脚本的文件体积便于传输。不过这样做仅仅是不够的,因为 Javascript 代码的速度被分割成两部分:下载时间(取决于文件的大小)和执行速度(取决于代码算法)。
当客户端载入 Javascript 脚本以后,真正的之行速度就取决于代码本身是否最优化了。这篇就是讲述如何优化代码本身的执行速度(听起来非常有技术的样子)。
关注作用域
浏览器中,Javascript 默认的变量范围是 window,也就是全局变量。在 window 中的变量只在页面从浏览器关闭以后才释放。而 Javascript 同时也有局部变量(私有变量)的概念,通常它在容器(比如 function)中执行完毕就会被释放。
所以很容易理解当调用某变量时,解释器就会自下(容器)由上(window)寻找变量,寻找的变量本身也是需要一点时间的。所以,解释器在作用树(《Javascript 高级程序设计》中称为“范围树”)中遍历的范围越短,那么脚本运行就会越快。
本人不擅长施教,下面的代码请自行理解
var country = "China";
function fn1() {
alert(country);
}
function fn2() {
var province = "Zhejiang";
fn1();
}
function fn3() {
var city = "Hangzhou";
fn2();
}
fn3();扩展阅读请点击这里和这里。
使用局部变量
理解了上述的细节以后,接下来就非常可以理解了。使用局部变量可以带来更快的执行速度,因为解释器无需因为搜索变量而离开当前执行范围。同时,局部变量让允许完毕就会被释放,所以它们不会一直占用内存。
这里要注意的是,使用闭包会打破这一规则,详细信息可以参看这里和以前我做的一道题目。
避免使用 with 语句
搜索变量范围越小,运行速度越快,所以就很很容易理解避免使用 with 语句的原因。比如
alert(document.title);
alert(document.body.tagName);
alert(document.location);
可以写成
with (document) {
alert(title);
alert(body.tagName);
alert(location);
}虽然代码缩减的程度,并且也非常的容易理解。但是使用 with 语句的同时,要强制解释器不仅在作用树(范围树)内查找局部变量,还强制检测每个变量及指定的对象,看其是否有此变量或者属性。
因此,最好避免使用 with 语句。最短的代码并不一定总是最高效的。
选择正确的算法
这似乎就是废话,所有的程序员都明白正确的算法对于之行效率是多么的重要。这里就不多解释,可以参考这篇和这篇。我始终相信,好的经验都是在实际 coding 中获得的。
循环的花招
Javascript 和大部分的程序语言一样,循环都会花费大量的执行时间,所以保持循环的高效可以减少执行时间。下面有几个花招,也是从那本书中获得的,照本宣科一下。
反转循环
有一个很有趣的例子,比如一个典型的循环会是这样写
for (var i = 0; i < element.length; i++) {
// ...
}但写成下面这个样子就有助于降低算法的复杂度,因为它用常数(O)作为条件循环以减少执行时间
for (var i = element.length - 1; i >= 0; i--) {
// ...
}书中的解释可能无法理解,那么我重新将其写成
var element_length = element.length;
for (var i = 0; i < element_length; i++) {
// ...
}可能会更好理解一些,因为它不会重复在循环中获取 element 的 length 属性,但书中的更改方法少了一个变量。
翻转循环
用 do...while 来替代 while 语句可以进一步的减少执行时间。比如
var i = 0;
while (i < element.length) {
// ...
i++;
}可以改写为 do...while 语句为这个样子
var i = 0;
do {
// ...
i++;
} while (i < element.length);当然,按照上一条的花招我们还可以优化成这个样子
var i = element.length - 1;
do {
// ...
} while (--i >= 0);这是因为 do...while 语句事先将循环体载入以后再做条件判断。不过本人认为还是保持程序的逻辑优先。
条件判断
优化 if 语句
用 if 和多个 else 语句时,将就有可能的情况放在最先,依次类推。同时尽量减少 else 和 if 的数量,将条件按照二叉树的方式进行排列。例如
if (i > 0 && i < 10) {
alert('between 0 and 10');
} else if (i > 9 && i < 20) {
alert('between 10 and 20');
} else if (i > 19 && i < 30) {
alert('between 19 and 30');
} else {
alert('out of range');
}可以将这段代码写成
if (i > 0) {
if (i < 10) {
alert('between 0 and 10');
} else {
if (i < 20) {
alert('between 10 and 20');
} else {
if (i < 30) {
alert('between 20 and 30');
} else {
alert('Greater than or equal 30');
}
}
}
} else {
alert('less than or equal 0');
}这个样子。虽然看上去非常的复杂,但是它已经考虑了很多代码潜在的条件判断情况,所以执行得更快。
switch 和 if
用 switch 还是 if 已经是老生常谈的问题了。一般来说,超过两个 if...else 判断的时候,最好是使用 switch 语句。这样做可以使代码更加清晰并且效率更高。同时,case 条件也可以使用任何类型的值。
语句瘦身
其实非常可以容易理解,脚本中的语句越少,执行所需的时间越短(听起来与上述观点有矛盾)。有很多方法可以将代码中的语句缩短,比如下面的一些花招。
定义多个变量
很明显,一条语句可以定义多个变量。这样做不仅可以缩小代码体积,还可以减少语句数量以减少执行时间。比如下面的代码
var webSite = "www.gracecode.com";
var haveLunch = function () {...}; 就可以精简为
var webSite = "www.gracecode.com", haveLunch = function () {...};相信这样的语句也不会给阅读带来多大的障碍。
迭代因子
使用迭代因子,尽可能的合并语句。比如
var girlFriend = girl[i];
i++;
这样的语句可以使用
var girlFriend = girl[i++];
替代。不过建议特别小心 i++ 和 ++i 的区别(该死的 C 语言后遗症)。
使用数组和对象字面量
这点其实在上一篇的时候就提到过,在这里就不复述。比如
var mySite = new Object;
mySite.author = "feelinglucky";
mySite.location = "http://www.gracecode.com";
就可以精简到
var mySite = {author:"feeinglucky", location:"http://www.gracecode.com"};这样子。
其他的花招
优先使用内置方法
比如
function power(number, n) {
var result = number;
for (var i = 1; i < n; i++) {
result *= number;
}
return result;
}这样的函数,完全就可以使用 Math.pow 来完成。Javascript 已经有很多现成的内置方法,只要允许最好使用它们。
存储常用的值
当多次用到同一个值的时候,可以先将其存储在局部变量中,以便快速访问。这个就不复述了,偷个懒不好意思。
DOM 操作
节约 DOM 操作
Javascript 对 DOM 的处理,可能是最耗费时间的操作之一。每次 Javascript 对 DOM 的操作,浏览器都会改变页面的表现、重新渲染页面,从而有明显的时间损耗。比较环保的做法就是尽可能不在 DOM 中进行 DOM 操作。
请看下面的例子,为 ul 添加 10 个条目
var oUl = document.getElementById("ulItem");
for (var i = 0; i < 10; i++) {
var oLi = document.createElement("li");
oUl.appendChild(oLi);
oLi.appendChild(document.createTextNode("Item " + i);
}乍看起来似乎无懈可击,但是这段代码的确有问题。首先是循环中的 oUl.appendChild(oLi); 的调用,每次执行过后浏览器就会重新渲染页面;其次,给列表添加添加文本节点(oLi.appendChind(document.createTextNode("Item " + i);),也这会造成页面重新渲染。每次运行都会造成两次页面重新渲染,总计 20 次。
要解决这个问题就如上面所言的,减少 DOM 操作,将列表项目在添加好文本节点以后再添加。下面的代码就可以与上述的代码完成同样的任务。
var oUl = document.getElementById("ulItem");
var oTemp = document.createDocumentFragment();
for (var i = 0; i < 10; i++) {
var oLi = document.createElement("li");
oLi.appendChild(document.createTextNode("Item " + i);
oTemp.appendChild(oLi);
}
oUl.appendChild(oTemp);遵循标准的 DOM
说点书中没有的(照本宣科完毕),Javascript 其实在寻找节点(Node)也会花上一段时间。对 Web 标准友好的 (x)html 文档相对杂乱文章的页面来说,Javascript 执行速度两者也会有所差别。
浏览器处理页面有模式之分,这也许也是为什么要编写遵循 Web 标准的页面的原因之一。具体信息可以参考这里和一些言论。
缓存 Ajax
Ajax 虽然提供了页面异步请求调用,但别忘记了它还是访问服务器的。Javascript 作为驱动层本身可以作为缓存使用,虽然在页面重新载入后就会被释放,但对于服务器而言这是一个好的消息。
结束语
不知不觉就写了那么多,很多东西都是书上照本宣科的。《Javascript 高级程序设计》的确是一本不可多得的好书,建议大家有机会都可以去看看。这本书不贵,59 RMB(可能在别的地方还有打折),对于烟民而言也就一条双精度红喜,不过它可比香烟所能带来的快感多得多。
全文完
- «
- 1
- ...
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- »