JavaScript OO不XX 学习总结

一、废话

    总觉得面向对象这东西,如果做的东西不是十分复杂的话,其实不太有场景能用上。最近重新学习了《JavaScript高级程序设计》中面向对象程序部分的知识,有一些收获,特此记录。

 

二、JavaScript创建对象最佳实践

    2.1 理论

    JavaScript是基于原型的语言,创建对象比较常用的方法是采用“构造函数+挂载原型”的方式。

    举个例子:

  var Engineer = function (name) {
    this.name = name;
  };
  Engineer.prototype.codeWith = function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  };
  Engineer.prototype.solve = function (problem) {
    return this.name + ' is solving ' + problem;
  };

  var a = new Engineer('kohpoll');
  var b = new Engineer('xp');

  console.log(a, b);
  console.log(a.codeWith(['vim']));
  console.log(a.solve('oo'));

    这段代码执行后,事实上的结构是这样的:

    这里总结2点:1)每创建一个函数,该函数默认都会拥有一个prototype属性,这个prototype是一个对象,默认会拥有一个constructor属性反过来指向该函数(比如:例子里的函数Engineer拥有的原型属性prototype,prototype里拥有constructor指向Engineer);2)每创建一个对象,该对象都会拥有一个内置属性__proto__,该属性指向构造了该对象的构造函数的prototype属性(比如:例子里的a对象的__proto__指向构造了它的构造函数Engineer的prototype)。

    那为什么Engineer.prototype拥有__proto__属性,且指向Object.prototype呢?这是因为原型对象prototype也是一个对象,那是谁构造了这个对象呢?当然是Object构造函数,所以Engineer.prototype的__proto__指向Object的原型(即:Object.prototype)。Object.prototype的__proto__已经到达顶端了,直接指向空。这就是所谓的原型链。

    当我们访问某个成员时,如:a.codeWith(['vim'])。会先从对象自身搜寻(上图的第一个方块),没有找到的话,就顺着__proto__来到Engineer.prototye,发现找到了这个方法,于是进行调用,若这里还没有找到,那就继续顺着Engineer.prototype的__proto__来到Object.prototype,如果这里还是找不到,若是访问属性就返回undefined,若是访问方法就报“Uncaught TypeError: Object [object Object] has no method"错误。

    所以,JavaScript中所有对象都继承自Object其实是说访问成员时,最终会在Object.prototype上结束。我们创建出一个对象后,toString、valueOf方法自动就可用,正是因为它们是挂载在Object.prototype上的。

    通过构造函数初始化属性,将方法挂载在原型上,我们实现了多个对象复用原型上的共有方法(不必在每个对象中都得定义一次),每个对象又分别拥有自己的属性。

    2.2 实践

    反过来,看上面的代码,总觉得每次都要这样编写(尤其是写一堆prototype的部分),是件比较烦人的事。我们可以将生成构造函数这个过程进行一个封装:

var construct = function () {// 构造器
  var Klass = function () {
    this.initialize.apply(this, arguments);
  };

// 添加实列成员(属性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; };

    那我们的示例代码可以这样来写:

var Engineer = construct().include({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var a = new Engineer('kohpoll');
var b = new Engineer('xp');

console.log(a, b);
console.log(a.codeWith(['vim']));
console.log(a.solve('oo'));

 

三、JavaScript继承最佳实践

    3.1 理论

    前面说过,JavaScript是基于原型的语言,其实从对原型链的说明中,我们已经大概能看出JavaScript实现继承的方法了,那就是构造原型链。如果,现在我们添加一个FrontEndEngineer子类继承Engineer,那我们想的效果应该是这样的结构:

    也就是说,如果我们能够打断默认情况下FronEndEngineer.prototype.__proto__的指向,让FrontEnginner.prototype.__proto__属性指向父类Engineer的原型prototype,根据前面说明过的原型链搜寻过程,我们就实现了FrontEnginner继承Engineer。即:FrontEndEngineer.prototype.__proto__=Engineer.prototype。

    可惜的是,__proto__是一个内部属性,各个浏览器的内部实现不一样,也许并不都叫__proto__,而且我们也不能直接修改。回想我们前面总结的2点中的第二点:每创建一个对象,该对象都会拥有一个内置属性__proto__,该属性指向构造了该对象的构造函数的prototype属性。对照我们的目的,让FrontEndEngineer.prototype.__proto__指向Enginner.prototype。于是,我们的方法就出现了:FroneEndEngineer.prototype = new Engineer()。

    说明如下:FrontEndEngineer.prototype是一个对象,拥有__proto__属性,默认情况下是指向Object.prototype(因为是Object构造函数构造了FrontEndEngineer.prototype),现在我们让FrontEndEngineer.prototype等于new Engineer(),等价于说FrontEndEngineer.prototype现在是由Engineer函数构造出的,根据上面提到的“每创建一个对象,该对象都会拥有一个内置属性__proto__,该属性指向构造了该对象的构造函数的prototype属性”,那此时FrontEndEngineer.prototype这个对象的__proto__应该指向构造了FrontEndEngineer.prototype这个对象的构造函数的prototype,即:Engineer.prototype。

    代码如下:

var Engineer = construct().include({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = function (name) {
  this.name = name;
};
FrontEndEngineer.prototype = new Engineer();
FrontEndEngineer.prototype.fuckIE6 = function () {
  return this.name + 'fuck ie6';
};
FrontEndEngineer.prototype.constructor = FrontEndEngineer;
var a = new FrontEndEngineer('kohpoll'); var b = new FrontEndEngineer('xp'); console.log(a, b); console.log(a.codeWith(['vim'])); console.log(a.solve('oo')); console.log(a.fuckIE6());
console.log(a instanceof Engineer, a instanceof FrontEndEngineer);

    于是,通过重写原型,我们实现了继承。通过这种方法需要注意的问题是:1)为子类FrontEndEngineer新添加的方法要在重写原型(即:FrontEndEngineer.prototype=new Engineer)后进行添加,否则,会被直接覆盖掉;2)由于我们重写了原型,原型的constructor属性也会改变,如果很在意,可以重新进行赋值;3)每次重写原型都调用了父类的构造函数,其实完全可以避免(下面说)。

    3.2 实践

    上面实现了继承,但是写起来也是有很多要注意的地方,比较麻烦。我们可以进行一个封装,代码如下:

var inherits = function (klass, supr, protoProps) {
  // 用于共享原型的空函数
  var F = function () {};

  // 重写原型实现继承
  F.prototype = supr.prototype;
  klass.prototype = new F();

  // 添加实列成员
  klass.include(protoProps);

  // 设置构造器的constructor(因为重写了原型)
  klass.prototype.constructor = klass;

  return klass;
};

    上面提到重写原型时会调用父类的构造函数,其实我们的目的仅仅是要让FrontEndEngineer.prototype的__proto__指向父类的prototype就好,根本不关心是不是真的是父类Engineer构造了FrontEndEngineer.prototype对象。于是,我们使用一个空函数F,将父类Engineer的prototype原型赋值给空函数F的prototype原型,然后让这个F来构造子类的prototype原型,就完成了原型的重写。

    于是,我们的示例代码可以这样来编写:

var Engineer = construct().include({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = construct();
inherits(FrontEndEngineer, Engineer, {
  fuckIE6: function () {
    return this.name + 'fuck ie6';
  }
});

var a = new FrontEndEngineer('kohpoll');
var b = new FrontEndEngineer('xp');

console.log(a, b);
console.log(a.codeWith(['vim']));
console.log(a.solve('oo'));
console.log(a.fuckIE6());
console.log(a instanceof Engineer, a instanceof FrontEndEngineer);

 

四、改进

    4.1 接口使用上改进

    上面实现的封装使用起来还是不太爽,我们参考下prototype(http://prototypejs.org/learn/class-inheritance),改进后得到如下代码:

var Class = {
  create: function () {
    var supr = Object;
    var protoProps = arguments[0] || {};
    var klass;

    if (typeof arguments[0] == 'function') {
      supr = arguments[0];
      protoProps = arguments[1] || {};
    }

    if (typeof protoProps.initialize != 'function') {
      protoProps.initialize = function () {};
    } 

    klass = this._construct();
    this._inherits(klass, supr, protoProps);

    return klass;
  },
  _construct: function () {//{{{// 构造器
    var Klass = function () {
      this.initialize.apply(this, arguments);
    };

// 添加实列成员(属性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; },//}}} _inherits: function (klass, supr, protoProps) {//{{{ // 用于共享原型的空函数 var F = function () {}; // 重写原型实现继承 F.prototype = supr.prototype; klass.prototype = new F(); // 添加实列成员 klass.include(protoProps); // 设置构造器的constructor(因为重写了原型) klass.prototype.constructor = klass; return klass; }//}}} };

    于是,现在我们的示例代码可以这样来写了:

var Engineer = Class.create({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = Class.create(Engineer, {
  initialize: function (name) {
    this.name = name;
  },
  fuckIE6: function () {
    return this.name + 'fuck ie6';
  }
});

var a = new FrontEndEngineer('kohpoll');
var b = new FrontEndEngineer('xp');

console.log(a, b);
console.log(a.codeWith(['vim']));
console.log(a.solve('oo'));
console.log(a.fuckIE6());
console.log(a instanceof Engineer, a instanceof FrontEndEngineer);

    4.2 继承使用上改进

    经过这样改进,代码看起来比较清晰了。但是,继承有一个很关键的问题没有解决,就是子类怎么调用父类的方法,从而实现代码复用?下面我们就来解决这个问题。

    step1  事实上,我们可以直接通过父类的prototype属性来访问父类的方法,为了方便,我们在生成构造器时添加一个$super属性。于是,代码变成如下(标红的是新增代码):

var Class = {
  create: function () {
    var supr = Object;
    var protoProps = arguments[0] || {};
    var klass;

    if (typeof arguments[0] == 'function') {
      supr = arguments[0];
      protoProps = arguments[1] || {};
    }

    if (typeof protoProps.initialize != 'function') {
      protoProps.initialize = function () {};
    } 

    klass = this._construct();
    this._inherits(klass, supr, protoProps);

    return klass;
  },
  _construct: function () {//{{{
    // 构造器
    var Klass = function () {
      // 访问父类成员快捷方式
      this.$super = Klass.$super;

      this.initialize.apply(this, arguments);
    };

    // 添加实列成员(属性,方法)
    Klass.include = function (obj) {
      for (var name in obj) {
        this.prototype[name] = obj[name];
      }

      return this;
    };

    return Klass;
  },//}}}
  _inherits: function (klass, supr, protoProps) {//{{{
    // 用于共享原型的空函数
    var F = function () {};

    // 重写原型实现继承
    F.prototype = supr.prototype;
    klass.prototype = new F();

    // 保存父类原型
    klass.$super = supr.prototype;

    // 添加实列成员、类成员
    klass.include(protoProps);

    // 设置构造器的constructor(因为重写了原型)
    klass.prototype.constructor = klass;

    return klass;
  }//}}}
};

    于是,示例代码可以这样使用:

var Engineer = Class.create({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = Class.create(Engineer, {
  initialize: function (name) {
    this.$super.initialize.call(this, name);
    // this.name = name;
  },
  codeWith: function (tools) {
    return 'fron end ' + this.$super.codeWith.call(this, tools);
  },
  fuckIE6: function () {
    return this.name + 'fuck ie6';
  }
});

var a = new FrontEndEngineer('kohpoll');
var b = new FrontEndEngineer('xp');

console.log(a, b);
console.log(a.codeWith(['vim']));

    step2  其实经过这样改进,已经比较不错了,只是每次调用父类方法时都得使用call方法来确保this正确的指向子类实例。如果不用call,那this会指向什么呢?答案是父类的prototye,在本例中,$super存的是Engineer.prototype,那当我们使用$super.codeWith()时,实际上,是指Engineer.prototype.codeWith(),this指向Engineer.prototype。

    我们想办法来改进这一点,得到代码如下(标红为新增):

var Class = {
  create: function () {
    var supr = Object;
    var protoProps = arguments[0] || {};
    var klass;

    if (typeof arguments[0] == 'function') {
      supr = arguments[0];
      protoProps = arguments[1] || {};
    }

    if (typeof protoProps.initialize != 'function') {
      protoProps.initialize = function () {};
    } 

    klass = this._construct();
    this._inherits(klass, supr, protoProps);

    return klass;
  },
  _construct: function () {//{{{
    var slice = Array.prototype.slice;

    // 构造器
    var Klass = function () {
      // 访问父类成员快捷方式
      this.$super = function (name) {
        var args = slice.call(arguments, 1) || [];
        var fn = Klass.$super[name];
        return typeof fn == 'function' ? fn.apply(this, args) : fn;
      };

      this.initialize.apply(this, arguments);
    };

    // 添加实列成员(属性,方法)
    Klass.include = function (obj) {
      for (var name in obj) {
        this.prototype[name] = obj[name];
      }

      return this;
    };

    return Klass;
  },//}}}
  _inherits: function (klass, supr, protoProps) {//{{{
    // 用于共享原型的空函数
    var F = function () {};

    // 重写原型实现继承
    F.prototype = supr.prototype;
    klass.prototype = new F();

    // 保存父类原型
    klass.$super = supr.prototype;

    // 添加实列成员、类成员
    klass.include(protoProps);

    // 设置构造器的constructor(因为重写了原型)
    klass.prototype.constructor = klass;

    return klass;
  }//}}}
};

    现在,我们将$super重写成函数,通过传入的函数名在父类的prototype里查找对应方法,若找到了就通过apply来调用,此时传入的this就指向了子类实例(因为$super现在是被子类的实例调用,$super函数内部this就指向子类实例)。

    于是,使用方法如下:

var Engineer = Class.create({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = Class.create(Engineer, {
  initialize: function (name) {
    this.$super('initialize', name);
    // this.name = name;
  },
  codeWith: function (tools) {
    return 'front end ' + this.$super('codeWith', tools);
  },
  solve: function (problem) {
    return 'front end ' + this.$super('codeWith', ['html', 'js', 'css']) + this.fuckIE6();
  },
  fuckIE6: function () {
    return ' and fuck ie6';
  }
});

var a = new FrontEndEngineer('kohpoll');
console.log(a.solve('work'));

    4.3 添加静态成员

    最后一点改进,是添加类似static的所有类共享的方法和属性。实现方法就是,直接将这些成员挂载到构造函数上面。这个就不多说了。于是得到最终代码如下:

  var Class = {
    create: function () {
      var supr = Object;
      var protoProps = arguments[0] || {}, staticProps = arguments[1] || {};
      var klass;

      if (typeof arguments[0] == 'function') {
        supr = arguments[0];
        protoProps = arguments[1] || {};
        staticProps = arguments[2] || {};
      }

      if (typeof protoProps.initialize != 'function') {
        protoProps.initialize = function () {};
      } 

      klass = this._construct();
      this._inherits(klass, supr, protoProps, staticProps);

      return klass;
    },
    _construct: function () {//{{{
      var slice = Array.prototype.slice;

      // 构造器
      var Klass = function () {
        // 访问类成员快捷方式 
        this.$self = Klass.$self;

        // 访问父类成员快捷方式
        this.$super = function (name) {
          var args = slice.call(arguments, 1) || [];
          var fn = Klass.$super[name];
          return typeof fn == 'function' ? fn.apply(this, args) : fn;
        };

        this.initialize.apply(this, arguments);
      };

      // 用于添加类成员(属性,方法)
      Klass.extend = function (obj) {
        for (var name in obj) {
          this[name] = obj[name];
        }

        return this;
      };

      // 添加实列成员(属性,方法)
      Klass.include = function (obj) {
        for (var name in obj) {
          this.prototype[name] = obj[name];
        }

        return this;
      };

      return Klass;
    },//}}}
    _inherits: function (klass, supr, protoProps, staticProps) {//{{{
      // 用于共享原型的空函数
      var F = function () {};

      // 重写原型实现继承
      F.prototype = supr.prototype;
      klass.prototype = new F();

      // 保存父类原型
      klass.$super = supr.prototype;
      // 保存类自身
      klass.$self = klass;

      // 添加实列成员、类成员
      klass.include(protoProps).extend(staticProps);

      // 设置构造器的constructor(因为重写了原型)
      klass.prototype.constructor = klass;

      return klass;
    }//}}}
  };

    

    最后的最后,可以在这里获取所有源码:https://github.com/KohPoll/zuki

posted on 2012-09-13 01:16  KohPoll  阅读(2258)  评论(7编辑  收藏  举报