面向对象的 Javascript (继承篇)February 20, 2008

继续我们的 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 (声明篇)February 16, 2008

有时间重新审视了遍 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 优化计划(程序优化篇)January 30, 2008

继续我们的 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. 1
  2. ...
  3. 18
  4. 19
  5. 20
  6. 21
  7. 22
  8. 23
  9. 24
  10. 25
Yahoo 统计