javascript面向对象精华编程(转)

Javascript 面向对象编程(一):封装

学习Javascript,最难的地方是什么?

我觉得,Object(对象)最难。因为Javascript的Object模型很独特,和其他语言都不一样,初学者不容易掌握。

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。

Javascript 面向对象编程(一):封装

学习Javascript,最难的地方是什么?

我觉得,Object(对象)最难。因为Javascript的Object模型很独特,和其他语言都不一样,初学者不容易掌握。

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。

那么,如果我们要把"属性"(property)和"方法"(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?

1. 生成对象的原始模式

假定我们把猫看成一个对象,它有"名字"和"颜色"两个属性。

  varCat = {

    name: '',

    color: ''

  }

现在,我们需要根据这个原型对象,生成两个实例对象。

  varcat1 = {}; // 创建一个空对象

    cat1.name= "大毛"; // 按照原型对象的属性赋值

    cat1.color= "黄色";

  varcat2 = {};

    cat2.name= "二毛";

    cat2.color= "黑色";

好了,这就是最简单的封装了。但是,这样的写法有两个缺点,一是如果多生成几个实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出有什么联系。

2. 原始模式的改进

我们可以写一个函数,解决代码重复的问题。

  functionCat(name,color){

    return{

      name:name,

      color:color

    }

  }

然后生成实例对象,就等于是在调用函数:

  varcat1 = Cat("大毛","黄色");

  varcat2 = Cat("二毛","黑色");

这种方法的问题依然是,cat1和cat2之间没有内在的联系,不能反映出它们是同一个原型对象的实例。

3. 构造函数模式

为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。

所谓"构造函数",其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。

比如,猫的原型对象现在可以这样写,

  functionCat(name,color){

    this.name=name;

    this.color=color;

  }

我们现在就可以生成实例对象了。

  varcat1 = new Cat("大毛","黄色");

  varcat2 = new Cat("二毛","黑色");

  alert(cat1.name);// 大毛

  alert(cat1.color);// 黄色

这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。

  alert(cat1.constructor== Cat); //true

  alert(cat2.constructor== Cat); //true

Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。

  alert(cat1instanceof Cat); //true

  alert(cat2instanceof Cat); //true

4. 构造函数模式的问题

构造函数方法很好用,但是存在一个浪费内存的问题。

请看,我们现在为Cat对象添加一个不变的属性"type"(种类),再添加一个方法eat(吃老鼠)。那么,原型对象Cat就变成了下面这样:

  functionCat(name,color){

    this.name= name;

    this.color= color;

    this.type= "猫科动物";

    this.eat= function(){alert("吃老鼠");};

  }

还是采用同样的方法,生成实例:

  varcat1 = new Cat("大毛","黄色");

  varcat2 = new Cat ("二毛","黑色");

  alert(cat1.type);// 猫科动物

  cat1.eat();// 吃老鼠

表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。

  alert(cat1.eat== cat2.eat); //false

能不能让type属性和eat()方法在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。

5. Prototype模式

Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。

这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。

  functionCat(name,color){

    this.name= name;

    this.color= color;

  }

  Cat.prototype.type= "猫科动物";

  Cat.prototype.eat= function(){alert("吃老鼠")};

然后,生成实例。

  varcat1 = new Cat("大毛","黄色");

  varcat2 = new Cat("二毛","黑色");

  alert(cat1.type);// 猫科动物

  cat1.eat();// 吃老鼠

这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。

  alert(cat1.eat== cat2.eat); //true

6. Prototype模式的验证方法

6.1 isPrototypeOf()

这个方法用来判断,某个proptotype对象和某个实例之间的关系。

  alert(Cat.prototype.isPrototypeOf(cat1));//true

  alert(Cat.prototype.isPrototypeOf(cat2));//true

6.2 hasOwnProperty()

每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。

  alert(cat1.hasOwnProperty("name"));// true

  alert(cat1.hasOwnProperty("type"));// false

6.3 in运算符

in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。

  alert("name"in cat1); // true

  alert("type"in cat1); // true

in运算符还可以用来遍历某个对象的所有属性。

  for(varprop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }

未完,请继续阅读这个系列的第二部分《构造函数的继承》和第三部分《非构造函数的继承》

Javascript面向对象编程(二):构造函数的继承

这个系列的第一部分,主要介绍了如何"封装"数据和方法,以及如何从原型对象生成实例。

今天要介绍的是,如何生成一个"继承"多个对象的实例。

比如,现在有一个"动物"对象的构造函数,


  functionAnimal(){

    this.species= "动物";

  }

还有一个"猫"对象的构造函数,


  functionCat(name,color){

    this.name= name;

    this.color= color;

  }

怎样才能使"猫"继承"动物"呢?

1. 构造函数绑定

最简单的方法,大概就是使用call或apply方法,将父对象的构造函数绑定在子对象上,也就是在子对象构造函数中加一行:

  functionCat(name,color){

    Animal.apply(this, arguments);

    this.name= name;

    this.color= color;

  }

  varcat1 = new Cat("大毛","黄色");

  alert(cat1.species);// 动物

2. prototype模式

更常见的做法,则是使用prototype属性。

如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。

  Cat.prototype= new Animal();

  Cat.prototype.constructor= Cat;

  varcat1 = new Cat("大毛","黄色");

  alert(cat1.species);// 动物

代码的第一行,我们将Cat的prototype对象指向一个Animal的实例。

  Cat.prototype= new Animal();

它相当于完全删除了prototype对象原先的值,然后赋予一个新值。但是,第二行又是什么意思呢?

  Cat.prototype.constructor= Cat;

原来,任何一个prototype对象都有一个constructor属性,指向它的构造函数。也就是说,Cat.prototype 这个对象的constructor属性,是指向Cat的。

我们在前一步已经删除了这个prototype对象原来的值,所以新的prototype对象没有constructor属性,所以我们必须手动加上去,否则后面的"继承链"会出问题。这就是第二行的意思。

总之,这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,

  o.prototype= {};

那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。

  o.prototype.constructor= o;

3. 直接继承prototype

由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。

现在,我们先将Animal对象改写:

  functionAnimal(){ }

  Animal.prototype.species= "动物";

然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。

  Cat.prototype= Animal.prototype;

  Cat.prototype.constructor= Cat;

  varcat1 = new Cat("大毛","黄色");

  alert(cat1.species);// 动物

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。

所以,上面这一段代码其实是有问题的。请看第二行

  Cat.prototype.constructor= Cat;

这一句实际上把Animal.prototype对象的constructor属性也改掉了!

  alert(Animal.prototype.constructor);// Cat

4. 利用空对象作为中介

由于"直接继承prototype"存在上述的缺点,所以可以利用一个空对象作为中介。

  varF = function(){};

  F.prototype= Animal.prototype;

  Cat.prototype= new F();

  Cat.prototype.constructor= Cat;

F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。

  alert(Animal.prototype.constructor);// Animal

5. prototype模式的封装函数

我们将上面的方法,封装成一个函数,便于使用。

  functionextend(Child, Parent) {

    varF = function(){};

    F.prototype= Parent.prototype;

    Child.prototype= new F();

    Child.prototype.constructor= Child;

    Child.uber= Parent.prototype;

  }

使用的时候,方法如下

  extend(Cat,Animal);

  varcat1 = new Cat("大毛","黄色");

  alert(cat1.species);// 动物

这个extend函数,就是YUI库如何实现继承的方法。

另外,说明一点。函数体最后一行

  Child.uber= Parent.prototype;

意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。这等于是在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

6. 拷贝继承

上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?

首先,还是把Animal的所有不变属性,都放到它的prototype对象上。

  functionAnimal(){}

  Animal.prototype.species= "动物";

然后,再写一个函数,实现属性拷贝的目的。

  functionextend2(Child, Parent) {

    varp = Parent.prototype;

    varc = Child.prototype;

    for(var i in p) {

      c[i]= p[i];

      }

    c.uber= p;

  }

这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。

使用的时候,这样写:

  extend2(Cat,Animal);

  varcat1 = new Cat("大毛","黄色");

  alert(cat1.species);// 动物

未完,请继续阅读第三部分《非构造函数的继承》

Javascript面向对象编程(三):非构造函数的继承

这个系列的第一部分介绍了"封装",第二部分介绍了使用构造函数实现"继承"。

今天是最后一个部分,介绍不使用构造函数实现"继承"。

一、什么是"非构造函数"的继承?

比如,现在有一个对象,叫做"中国人"。

  varChinese = {
    nation:'中国'
  };

还有一个对象,叫做"医生"。

  varDoctor ={
    career:'医生'
  }

请问怎样才能让"医生"去继承"中国人",也就是说,我怎样才能生成一个"中国医生"的对象?

这里要注意,这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现"继承"。

二、object()方法

json格式的发明人Douglas Crockford,提出了一个object()函数,可以做到这一点。

  functionobject(o) {

    functionF() {}

    F.prototype= o;

    returnnew F();

  }

这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。

使用的时候,第一步先在父对象的基础上,生成子对象:

  varDoctor = object(Chinese);

然后,再加上子对象本身的属性:

  Doctor.career= '医生';

这时,子对象已经继承了父对象的属性了。

  alert(Doctor.nation);//中国

三、浅拷贝

除了使用"prototype链"以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。

下面这个函数,就是在做拷贝:

  functionextendCopy(p) {

    varc = {};

    for(var i in p) { 
      c[i]= p[i];
    }

    c.uber= p;

    returnc;
  }

使用的时候,这样写:

  varDoctor = extendCopy(Chinese);

  Doctor.career= '医生';

  alert(Doctor.nation);// 中国

但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

请看,现在给Chinese添加一个"出生地"属性,它的值是一个数组。

  Chinese.birthPlaces= ['北京','上海','香港'];

通过extendCopy()函数,Doctor继承了Chinese。

  varDoctor = extendCopy(Chinese);

然后,我们为Doctor的"出生地"添加一个城市:

  Doctor.birthPlaces.push('厦门');

发生了什么事?Chinese的"出生地"也被改掉了!

  alert(Doctor.birthPlaces);//北京,上海,香港,厦门

  alert(Chinese.birthPlaces);//北京,上海,香港,厦门

所以,extendCopy()只是拷贝基本类型的数据,我们把这种拷贝叫做"浅拷贝"。这是早期jQuery实现继承的方式。

四、深拷贝

所谓"深拷贝",就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用"浅拷贝"就行了。

  functiondeepCopy(p, c) {

    varc = c || {};

    for(var i in p) {

      if(typeof p[i] === 'object') {

        c[i]= (p[i].constructor === Array) ? [] : {};

        deepCopy(p[i],c[i]);

      }else {

         c[i]= p[i];

      }
    }

    returnc;
  }

使用的时候这样写:

  varDoctor = deepCopy(Chinese);

现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:

  Chinese.birthPlaces= ['北京','上海','香港'];

  Doctor.birthPlaces.push('厦门');

这时,父对象就不会受到影响了。

  alert(Doctor.birthPlaces);//北京,上海,香港,厦门

  alert(Chinese.birthPlaces);//北京,上海,香港

目前,jQuery库使用的就是这种继承方法。

Javascript的this用法

this是Javascript语言的一个关键字。

它代表函数运行时,自动生成的一个内部对象,只能在函数内部使用。比如,

  functiontest(){

    this.x= 1;

  }

随着函数使用场合的不同,this的值会发生变化。但是有一个总的原则,那就是this指的是,调用函数的那个对象。

下面分四种情况,详细讨论this的用法。

情况一:纯粹的函数调用

这是函数的最通常用法,属于全局性调用,因此this就代表全局对象Global。

请看下面这段代码,它的运行结果是1。

  functiontest(){

    this.x= 1;

    alert(this.x);

  }

  test();// 1

为了证明this就是全局对象,我对代码做一些改变:

  varx = 1;

  functiontest(){

    alert(this.x);

  }

  test();// 1

运行结果还是1。再变一下:

  varx = 1;

  functiontest(){

    this.x= 0;

  }

  test();

  alert(x);//0

情况二:作为对象方法的调用

函数还可以作为某个对象的方法调用,这时this就指这个上级对象。

  functiontest(){

    alert(this.x);

  }

  varo = {};

  o.x= 1;

  o.m= test;

  o.m();// 1

情况三 作为构造函数调用

所谓构造函数,就是通过这个函数生成一个新对象(object)。这时,this就指这个新对象。

  functiontest(){

    this.x= 1;

  }

  varo = new test();

  alert(o.x);// 1

运行结果为1。为了表明这时this不是全局对象,我对代码做一些改变:

  varx = 2;

  functiontest(){

    this.x= 1;

  }

  varo = new test();

  alert(x);//2

运行结果为2,表明全局变量x的值根本没变。

情况四 apply调用

apply()是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。因此,this指的就是这第一个参数。

  varx = 0;

  functiontest(){

    alert(this.x);

  }

  varo={};

  o.x= 1;

  o.m= test;

  o.m.apply();//0

apply()的参数为空时,默认调用全局对象。因此,这时的运行结果为0,证明this指的是全局对象。

如果把最后一行代码修改为

  o.m.apply(o);//1

运行结果就变成了1,证明了这时this代表的是对象o。

学习Javascript闭包(Closure)

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

下面就是我的学习笔记,对于Javascript初学者应该是很有用的。

一、变量的作用域

要理解闭包,首先必须理解Javascript特殊的变量作用域。

变量的作用域无非就是两种:全局变量和局部变量。

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

  varn=999;

  functionf1(){
    alert(n);
  }

  f1();// 999

另一方面,在函数外部自然无法读取函数内的局部变量。

  functionf1(){
    varn=999;
  }

  alert(n);// error

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

  functionf1(){
    n=999;
  }

  f1();

  alert(n);// 999

二、如何从外部读取局部变量?

出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。

那就是在函数的内部,再定义一个函数。

  functionf1(){

    varn=999;

    functionf2(){
      alert(n);// 999
    }

  }

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

  functionf1(){

    varn=999;

    functionf2(){
      alert(n);
    }

    returnf2;

  }

  varresult=f1();

  result();// 999

三、闭包的概念

上一节代码中的f2函数,就是闭包。

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

四、闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

怎么来理解这句话呢?请看下面的代码。

  functionf1(){

    varn=999;

    nAdd=function(){n+=1}

    functionf2(){
      alert(n);
    }

    returnf2;

  }

  varresult=f1();

  result();// 999

  nAdd();

  result();// 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

五、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

六、思考题

如果你能理解下面两段代码的运行结果,应该就算理解闭包的运行机制了。

代码片段一。

  varname = "The Window";

  varobject = {
    name: "My Object",

    getNameFunc: function(){
      returnfunction(){
        returnthis.name;
      };

    }

  };

  alert(object.getNameFunc()());


代码片段二。

  varname = "The Window";

  varobject = {
    name: "My Object",

    getNameFunc: function(){
      varthat = this;
      returnfunction(){
        returnthat.name;
      };

    }

  };

  alert(object.getNameFunc()());

(完)

12种不宜使用的Javascript语法

这几天,我在读《Javascript语言精粹》。

这本书很薄,100多页,正好假日里翻翻。

该书的作者是DouglasCrockford,他是目前世界上最精通Javascript的人之一,也是Json格式的创造者。

他认为Javascript有很多糟粕。因为1995年BrendanEich设计这种语言的时候,只用了三个月,很多语言特性没有经过深思熟虑,就推向了市场。结果等到人们意识到这些问题的时候,已经有100万程序员在使用它了,不可能再大幅修改语言本身了。所以,Douglas Crockford决定,他要告诉大家,Javascript中哪些部分是精粹,哪些部分是糟粕和鸡肋。

这个想法非常好,但是我不得不说,这本书写得不够好,不适合新手阅读。原因如下:1)Douglas Crockford叙述得不清晰,更像与同行讨论问题,而不是由浅入深地讲解问题。这本书的重点不是解释,所以读完后,我觉得Javascript好像变得更复杂了。2)他固执地使用铁路图(railroad diagram)解释每一条语句。全世界似乎只有他一个人使用这种比Javascript更难看懂的图。3)该书基本上是一本简化的Javascript语法手册,缺乏足够的新内容。4)该书举例过少,而且在最难的函数和对象部分,使用的例子都是环环相套、层层递进的例子,导致阅读起来很吃力。

该书最有价值的内容不是正文,反而是附录。在附录B中,Douglas Crockford列出了12种应该避免使用的Javascript语法,我觉得非常值得推广。

==============================

1. ==

Javascript有两组相等运算符,一组是==和!=,另一组是===和!==。前者只比较值的相等,后者除了值以外,还比较类型是否相同。

请尽量不要使用前一组,永远只使用===和!==。因为==默认会进行类型转换,规则十分难记。如果你不相信的话,请回答下面五个判断式的值是true还是false:

  false== 'false'

  false== undefined

  false== null

  null== undefined

  0== ''

前三个是false,后两个是true。

2. with

with的本意是减少键盘输入。比如

  obj.a= obj.b;

  obj.c= obj.d;

可以简写成

  with(obj){
    a= b;
    c= d;
  }

但是,在实际运行时,解释器会首先判断obj.b和obj.d是否存在,如果不存在的话,再判断全局变量b和d是否存在。这样就导致了低效率,而且可能会导致意外,因此最好不要使用with语句。

3. eval

eval用来直接执行一个字符串。这条语句也是不应该使用的,因为它有性能和安全性的问题,并且使得代码更难阅读。

eval能够做到的事情,不用它也能做到。比如

  eval("myValue= myObject." + myKey + ";");

可以直接写成

  myValue= myObject[myKey];

至于ajax操作返回的json字符串,可以使用官方网站提供的解析器json_parse.js运行。

4. continue

这条命令的作用是返回到循环的头部,但是循环本来就会返回到头部。所以通过适当的构造,完全可以避免使用这条命令,使得效率得到改善。

5. switch 贯穿

switch结构中的case语句,默认是顺序执行,除非遇到break,return和throw。有的程序员喜欢利用这个特点,比如

  switch(n){
    case1:
    case2:
      break;
  }

这样写容易出错,而且难以发现。因此建议避免switch贯穿,凡是有case的地方,一律加上break。

  switch(n){
    case1:
      break;
    case2:
      break;
  }

6. 单行的块结构

if、while、do和for,都是块结构语句,但是也可以接受单行命令。比如

  if(ok) t = true;

甚至写成

  if(ok)
    t= true;

这样不利于阅读代码,而且将来添加语句时非常容易出错。建议不管是否只有一行命令,都一律加上大括号。

  if(ok){
    t= true;
  }

7. ++和--

递增运算符++和递减运算符--,直接来自C语言,表面上可以让代码变得很紧凑,但是实际上会让代码看上去更复杂和更晦涩。因此为了代码的整洁性和易读性,不用为好。

8. 位运算符

Javascript完全套用了Java的位运算符,包括按位与&、按位或|、按位异或^、按位非~、左移<<、带符号的右移>>和用0补足的右移>>>。

这套运算符针对的是整数,所以对Javascript完全无用,因为Javascript内部,所有数字都保存为双精度浮点数。如果使用它们的话,Javascript不得不将运算数先转为整数,然后再进行运算,这样就降低了速度。而且“按位与运算符”&同“逻辑与运算符”&&,很容易混淆。

9. function语句

在Javascript中定义一个函数,有两种写法:

  functionfoo() { }

  varfoo = function () { }

两种写法完全等价。但是在解析的时候,前一种写法会被解析器自动提升到代码的头部,因此违背了函数应该先定义后使用的要求,所以建议定义函数时,全部采用后一种写法。

10. 基本数据类型的包装对象

Javascript的基本数据类型包括字符串、数字、布尔值,它们都有对应的包装对象String、Number和Boolean。所以,有人会这样定义相关值:

  newString("Hello World");

  newNumber(2000);

  newBoolean(false);

这样写完全没有必要,而且非常费解,因此建议不要使用。

另外,newObject和new Array也不建议使用,可以用{}和[]代替。

11. new语句

Javascript是世界上第一个被大量使用的支持Lambda函数的语言,本质上属于与Lisp同类的函数式编程语言。但是当前世界,90%以上的程序员都是使用面向对象编程。为了靠近主流,Javascript做出了妥协,采纳了类的概念,允许根据类生成对象。

类是这样定义的:

  varCat = function (name) {
    this.name= name;
    this.saying= 'meow' ;
  }

然后,再生成一个对象

  varmyCat = new Cat('mimi');

这种利用函数生成类、利用new生成对象的语法,其实非常奇怪,一点都不符合直觉。而且,使用的时候,很容易忘记加上new,就会变成执行函数,然后莫名其妙多出几个全局变量。所以,建议不要这样创建对象,而采用一种变通方法。

Douglas Crockford给出了一个函数:

  Object.beget= function (o) {
    varF = function (o) {};
    F.prototype= o ;
    returnnew F;
  };

创建对象时就利用这个函数,对原型对象进行操作:

  varCat = {
    name:'',
    saying:'meow'
  };

  varmyCat = Object.beget(Cat);

对象生成后,可以自行对相关属性进行赋值:

  myCat.name= 'mimi';

12. void

在大多数语言中,void都是一种类型,表示没有值。但是在Javascript中,void是一个运算符,接受一个运算数,并返回undefined。

  void0; // undefined

这个命令没什么用,而且很令人困惑,建议避免使用。

(完)

快速排序(Quicksort)的Javascript实现

日本程序员norahiko,写了一个排序算法的动画演示,非常有趣。

这个周末,我就用它当做教材,好好学习了一下各种排序算法。

排序算法(Sortingalgorithm)是计算机科学最古老、最基本的课题之一。要想成为合格的程序员,就必须理解和掌握各种排序算法。

目前,最常见的排序算法大概有七八种,其中"快速排序"(Quicksort)使用得最广泛,速度也较快。它是图灵奖得主C. A. R. Hoare(1934--)于1960时提出来的。

"快速排序"的思想很简单,整个排序过程只需要三步:

  (1)在数据集之中,选择一个元素作为"基准"(pivot)。

  (2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。

  (3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

举例来说,现在有一个数据集{85, 24, 63, 45, 17, 31, 96, 50},怎么对其排序呢?

第一步,选择中间的元素45作为"基准"。(基准值可以任意选择,但是选择中间的值比较容易理解。)

第二步,按照顺序,将每个元素与"基准"进行比较,形成两个子集,一个"小于45",另一个"大于等于45"。

第三步,对两个子集不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

下面参照网上的资料(这里这里),用Javascript语言实现上面的算法。

首先,定义一个quickSort函数,它的参数是一个数组。

varquickSort = function(arr) {

};

然后,检查数组的元素个数,如果小于等于1,就返回。

varquickSort = function(arr) {

  if(arr.length <= 1) { return arr; }

};

接着,选择"基准"(pivot),并将其与原数组分离,再定义两个空数组,用来存放一左一右的两个子集。

varquickSort = function(arr) {

  if (arr.length <= 1) { return arr; }

  varpivotIndex = Math.floor(arr.length / 2) ;

  varpivot = arr.splice(pivotIndex, 1)[0];

  varleft = [];

  varright = [];

};

然后,开始遍历数组,小于"基准"的元素放入左边的子集,大于基准的元素放入右边的子集。

varquickSort = function(arr) {

  if (arr.length <= 1) { return arr; }

  var pivotIndex = Math.floor(arr.length / 2) ;

  var pivot = arr.splice(pivotIndex, 1)[0];

  var left = [];

  var right = [];

  for(var i = 0; i < arr.length; i++){

    if(arr[i] < pivot) {

      left.push(arr[i]);

    }else {

      right.push(arr[i]);

    }

  }

};

最后,使用递归不断重复这个过程,就可以得到排序后的数组。

varquickSort = function(arr) {

  if (arr.length <= 1) { return arr; }

  var pivotIndex = Math.floor(arr.length / 2);

  var pivot = arr.splice(pivotIndex, 1)[0];

  var left = [];

  var right = [];

  for (var i = 0; i < arr.length; i++){

    if (arr[i] < pivot) {

      left.push(arr[i]);

    } else {

      right.push(arr[i]);

    }

  }

  returnquickSort(left).concat([pivot], quickSort(right));

};

使用的时候,直接调用quickSort()就行了。

(完)

Javascript继承机制的设计思想

我一直很难理解Javascript语言的继承机制。

它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承。

我花了很多时间,学习这个部分,还做了很多笔记。但是都属于强行记忆,无法从根本上理解。

直到昨天,我读到法国程序员Vjeux的解释,才恍然大悟,完全明白了Javascript为什么这样设计。

下面,我尝试用自己的语言,来解释它的设计思想。彻底说明白prototype对象到底是怎么回事。其实根本就没那么复杂,真相非常简单。

一、从古代说起

要理解Javascript的设计思想,必须从它的诞生说起。

1994年,网景公司(Netscape)发布了Navigator浏览器0.9版。这是历史上第一个比较成熟的网络浏览器,轰动一时。但是,这个版本的浏览器只能用来浏览,不具备与访问者互动的能力。比如,如果网页上有一栏"用户名"要求填写,浏览器就无法判断访问者是否真的填写了,只有让服务器端判断。如果没有填写,服务器端就返回错误,要求用户重新填写,这太浪费时间和服务器资源了。

因此,网景公司急需一种网页脚本语言,使得浏览器可以与网页互动。工程师Brendan Eich负责开发这种新语言。他觉得,没必要设计得很复杂,这种语言只要能够完成一些简单操作就够了,比如判断用户有没有填写表单。

1994年正是面向对象编程(object-oriented programming)最兴盛的时期,C++是当时最流行的语言,而Java语言的1.0版即将于第二年推出,Sun公司正在大肆造势。

BrendanEich无疑受到了影响,Javascript里面所有的数据类型都是对象(object),这一点与Java非常相似。但是,他随即就遇到了一个难题,到底要不要设计"继承"机制呢?

二、Brendan Eich的选择

如果真的是一种简易的脚本语言,其实不需要有"继承"机制。但是,Javascript里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich最后还是设计了"继承"。

但是,他不打算引入"类"(class)的概念,因为一旦有了"类",Javascript就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度。

他考虑到,C++和Java语言都使用new命令,生成实例。

C++的写法是:

  ClassName *object = new ClassName(param);

Java的写法是:

  Foo foo = new Foo();

因此,他就把new命令引入了Javascript,用来从原型对象生成一个实例对象。但是,Javascript没有"类",怎么来表示原型对象呢?

这时,他想到C++和Java使用new命令时,都会调用"类"的构造函数(constructor)。他就做了一个简化的设计,在Javascript语言中,new命令后面跟的不是类,而是构造函数。

举例来说,现在有一个叫做DOG的构造函数,表示狗对象的原型。

  function DOG(name){

    this.name = name;

  }

对这个构造函数使用new,就会生成一个狗对象的实例。

  vardogA = new DOG('大毛');

  alert(dogA.name); // 大毛

注意构造函数中的this关键字,它就代表了新创建的实例对象。

三、new运算符的缺点

用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。

比如,在DOG对象的构造函数中,设置一个实例对象的共有属性species。

  function DOG(name){

    this.name = name;

    this.species= '犬科';

  }

然后,生成两个实例对象:

  var dogA = new DOG('大毛');

  var dogB = new DOG('二毛');

这两个对象的species属性是独立的,修改其中一个,不会影响到另一个。

  dogA.species= '猫科';

  alert(dogB.species); // 显示"犬科",不受dogA的影响

每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。

四、prototype属性的引入

考虑到这一点,Brendan Eich决定为构造函数设置一个prototype属性。

这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。

实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。

还是以DOG构造函数为例,现在用prototype属性进行改写:

  function DOG(name){

    this.name = name;

  }

  DOG.prototype= { species : '犬科' };


  vardogA = new DOG('大毛');

  var dogB = new DOG('二毛');


  alert(dogA.species);// 犬科

  alert(dogB.species); // 犬科

现在,species属性放在prototype对象里,是两个实例对象共享的。只要修改了prototype对象,就会同时影响到两个实例对象。

  DOG.prototype.species= '猫科';


  alert(dogA.species);// 猫科

  alert(dogB.species); // 猫科

五、总结

由于所有的实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象一样。

转载地址:http://blog.csdn.net/suncqujsj/article/details/7060720

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值