`
oham_一1一
  • 浏览: 51311 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Javascript 面向对象化写法——语法篇

阅读更多

本编参考:http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html 系列,总结如下

 

一.几个重要关键字

 

1.this —— 指当前的对象,若在全局范围,则指当前页面对象window,如果是函数中使用this,则指的是调用这个函数的对象。firebug一下demo:

function sayHi() {
  
  console.debug(this);
}


var obj = {
  sayHi: sayHi,
  sayHi2: function() {
    return function() {
      console.debug(this);
    }
    
  }
};


sayHi(); // 结果为window对象

obj.sayHi(); // 结果为obj对象

obj.sayHi2()(); //  结果为window对象

 解析:在javascript当中,一切皆为对象,function也是,所以可以看到全局函数sayHi可以像变量一般pass给obj的sayHi属性。三个函数调用的,前两个不用多解析,看第三个,其实等价于下面写法:

 

var f = obj.sayHi2();
f();

 sayHi2函数返回一个函数对象,赋予变量f,f就是个函数了,然后 f 被window调用执行,如此就这么回事了。

 

再看一个demo:

function TestThis () {
 
 this.oham = 'oham';
  console.debug(this);
 
}

TestThis();   // window对象
new TestThis();  // TestThis 对象实例本身

 对于TestThis();的结果不奇特,而new TestThis();之神奇在于new,其内部机制在下就不懂了...只可以看出,通过new XXX();这样调用,XXX方法中的this就指向XXX构造出的对象实例本身了。

 

 

2.apply与call

接着上面的例子,介绍apply 与 call,它们均是全局函数,归Function所有(Function属于javascript内部对象,不是Function的对象是没有这两个方法的)。 接上文的demo:

 

...
obj.sayHi2()();  //结果为window对象

obj.sayHi2().apply(obj);   // 结果为obj对象
obj.sayHi2().call(obj);   // 结果为obj对象

 apply 与 call 的 作用是改变函数中this的指向,即使函数看起来好像被谁调用一样,其作用域也是那个谁的。 

 

apply 与 call 的区别,仅仅是参数定义不同,请看如下demo:

 

function ml(me, mm) {
  
  console.log(this);
  
  if( this === window ) {
    console.log(me + ',' + mm + ' not possible in public');
  }else {
    console.log(me + ',' + mm + ' so happy');
  }
  
}

var hotel = {};


ml('me', 'mm');  

ml.apply(hotel, ['me', 'mm']);

ml.call(hotel, 'me', 'mm');

 请实践一下,代码,便知知道,apply的定义的参数列表是个数组,call是参数列(估计是function call(obj, params...))。

 

**至此深觉有必要介绍javascript中闭包的概念。闭包,指的是语法上表示包含不被计算的变量的函数,也就是说,函数可以使用函数之外定义的变量。

demo(以下摘自w3cschool):

 

var sMessage = "hello world";

function sayHelloWorld() {
  alert(sMessage);
}

sayHelloWorld();

 在上面这段代码中,脚本被载入内存后,并没有为函数 sayHelloWorld() 计算变量 sMessage 的值。该函数捕获 sMessage 的值只是为了以后的使用,也就是说,解释程序知道在调用该函数时要检查 sMessage 的值。sMessage 将在函数调用 sayHelloWorld() 时(最后一行)被赋值,显示消息 "hello world"。

 

 

再看看自己的一个demo:

 

var outSide = 'madom';

var room = {
  mm : 'mm',
  watch: function(me) {
    
    return function () {
      
      if(this.outSide) {  //尝试
        console.debug(me + ' can watch ' + this.outSide);
      }else {
        console.debug(me + ' can\'t watch  outSide, but never mind.');
      }
       
      if(this.mm) {   //尝试
          console.debug(me + ' can watch ' + this.mm);
      }else {
          console.debug(me + ' can\'t watch mm, not happy');
      }
    } 
  },
  
  touch: function (me) {
  
      if(this.outSide) { //尝试
        console.debug(me + ' can touch ' + this.outSide + ', but won\'t do that');
      }else {
        console.debug(me + ' can\'t touch outSide, but never mind.');
      }
       
      if(this.mm) { //尝试
          console.debug(me + ' can touch ' + this.mm + ', so cool...');
      }else {
          console.debug(me + ' can\'t touch mm, not happy');
      }
  }
};

var watchFunc = room.watch('I');

//全局调用
watchFunc();   
console.log('-------------------------------------');
//room范围调用
watchFunc.apply(room);

console.log('============================================');

//room范围调用
room.touch('I');
console.log('----------------------------------------');
//全局调用
room.touch.apply(this, ['I']);


 结果为:

 

"I can watch madom"
"I can't watch mm, not happy"
"-------------------------------------"
"I can't watch  outSide, but never mind."
"I can watch mm"
"============================================"
"I can't touch outSide, but never mind."
"I can touch mm, so cool..."
"----------------------------------------"
"I can touch madom, but won't do that"
"I can't touch mm, not happy"

 这个结果好解析,this为谁调用就是谁,接着请尝试把demo中标有//尝试的if 语句中的"this."删除,

情况会如下:

"I can watch madom"
"Uncaught ReferenceError: mm is not defined (line 15)"

 执行到watch函数的if(mm) 这句出错了,无论apply部apply结果都一样,原因具体我就不清楚了,这里只是证实了,对于一个变量如name, name 并不是等价于this.name(跟java大不同) ,JS的解释程序不会为你做这个,当函数被调用的时候,它只会先从当前函数范围去找,若无命中变量,则直接到全局范围去找。可以把mm部分的代码去掉,只留outSide的,结果是无论apply与否,结果都能访问的变量outSide。

 

 3.prototype——对该对象的对象原型的引用。对于所有的对象,它默认返回 Object 对象的一个实例(w3c定义)。

prototype本质上还是一个JavaScript对象,个人觉得上述定义不太妥当,实际上prototype感觉更像对象的一个模板,而非实例, 并且每个函数都有一个默认的prototype属性。 以demo为证:

 

var room = {
  mm: 'mm',
  watch: function () {
  }
};

console.log(room.prototype);    // undefined
console.log(room.valueOf());    //  room object
 
现在修改demo如下:
var outSide = 'madom';

function Room () {
  this.mm = 'mm';
}

console.log(Room.prototype);   //Room{}
console.log(Room.mm); // undefined

console.log(new Room().prototype);    //undefined
console.log(new Room().mm);    // mm

console.log(new Room().valueOf());   //  Room {mm: "mm"} 
console.log(Room.valueOf());    // function Room(){this.mm = 'mm';}
 解析,当声明 function Room的时候,Room便是javascript的固有对象Function,Function 有自己的一个空的prototype,同时也说明Room.mm为什么是undefined,因为Room此时仅仅是个Function对象,javascript的Function对象当中必定没有mm的定义。
对于后new Room()的两条语句的结果,因为new Room()之后,javascript解析器会执行Room函数本身,返回一个具体的对象实例,该对象包含了Room() 方法体内定义的所有东西。这里new Room() 返回的对象实例跟var room = {...}的区别,请看demo:
function Room () {
  this.mm = 'mm';
}

var room = {
  mm : 'mm'
  
};

console.log(new Room().valueOf()); // Room {mm: "mm"} 
console.log(room.valueOf());  // Object {mm: "mm"} 
 可见,Room 是一个类型为Room的实例, 而room是一个Object的实例。当然此时两者的prototype都为空,以上的demo我拿mm属性来测试,是为了说明prototype本质也是个对象,可以像个普通的对象属性那般玩,只是javascript会对名为prototype的属性特别对待,那prototype到底为何用?demo:
function Room () {
  this.mm = 'mm';
}

Room.prototype.watch = function(){};   // Room 本身为Function,有个空的prototype属性,js解析器执行new 操作的时候会特别对待prototype属性

var room = {
  mm : 'mm'
  
};

room.prototype.watch = function() {};  //报错 room 本身为Object 类型对象,不具备prototype属性的定义

console.log(new Room());  //  Room {mm: "mm", watch: function}
 从上面的结果可以看出,prototype 对象是个模板,要实例化的对象都以这个模板为基础。总而言之,prototype 对象的任何属性和方法都被传递给那个类的所有实例;对于Object,充其量只是个普通的属性。
这样的意义是,此时Room相当于一个构造函数(constructor),我们在构造函数内定义属性与方法,当某天我们需要构建一种对象BabyRoom,包含Room的所有特性,此时,我们只需把Room的prototype 赋予 BabyRoom Function 就可以了,而不必重新另行定义(这是本篇后话)于是使用BabyRoom构造出的对象除了有自身特性外,还包含Room对象的prototype中的一切。
 
4. constructor——指创建当前对象的构造函数
上述Room就是一个constructor,demo:
function Room () {
  this.mm = 'mm';
}

Room.prototype.watch = function(){};

var room = {
  mm : 'mm'
  
};

console.log(new Room().constructor == Room);   // true
console.log(room.constructor == Object);   //  true
 当constructor 与 prototype 相遇, demo:
function Room () {
  this.mm = 'mm';
}

Room.prototype.watch = function(){};

console.log(new Room().constructor === Room);   //   Room对象的实例的构造为Room无疑
console.log(Room.prototype.constructor === Room);//  构造函数Room的prototype的构造函数是自己本身
console.log(new Room().constructor.prototype.constructor === Room);  /合并上两句得此结论

 现在修改demo如下:
function Room () {
  this.mm = 'mm';
}

Room.prototype = {   //改了这里
  watch: function() {}
};

console.log(new Room().constructor === Room);  //false , Room的对象实例的构造不再是Room了
console.log(Room.prototype.constructor === Room);    // 不再是Room
console.log(new Room().constructor.prototype.constructor === Room);  // false
console.log(new Room().constructor === Object);   //true
console.log(Room.prototype.constructor === Object);  //true
console.log(new Room().constructor.prototype.constructor === Object);   //true
  原因是修改之后的Room的prototype的声明形式等同于:
Room.prototype = new Object({
   watch: function() {}
});
 如此看来,prototype的构造当然是Object了,这就造成用Room构造的对象实例的构造也成了Object了。这样引起的问题倒没什么,只是prototype作为一个对象模板,应用在实现继承的时候,其构造却不对应类构造函数本身,这有点说不过去。
修正的办法,网上如是说:
Room.prototype.constructor = Room;  // 个人猜测是子类对象调用instanceOf 时能够有正确的结果
 demo:
function Room () {
  this.mm = 'mm';
}

Room.prototype = {
  watch: function() {}
};

Room.prototype.constructor = Room;   //  重新覆盖Room.prototype.constructor

console.log(new Room().constructor === Room);  //true
console.log(Room.prototype.constructor === Room);   //true
console.log(new Room().constructor.prototype.constructor === Room);   //true

console.log(new Room().constructor === Object);   //false
console.log(Room.prototype.constructor === Object);    //false
console.log(new Room().constructor.prototype.constructor === Object);   //false

console.log(new Room() instanceof Room );   //无论覆盖Room.prototype.constructor与否, 结果都为true
 
二、javascript的继承实现
原始版:按照上述关键字的陈述,估计不难给出下面实现:
//定义一个父类 Huamn
function Human(name, gender) {
  this.name = name;
  this.gender = gender;
  this.skills = new Array('talk', 'walk', 'sleep');
}

Human.prototype = {
  ml: function () {
    if(this.gender == 'male'){
      console.log(this.name + ' is feeling hign');
    }else if(this.gender == 'female' ){
      console.log(this.name + ' is screaming');
    }else{
      console.log('-_-!...');
    }
    
  }
};

//继承 父类Huamn,定义一个Superman类
function Superman(name, gender) {
  this.name = name;
  this.gender = gender;
}

 //为了把prototype中的一切以及Huamn的一些属性都继承过来(如skills),因而不是Superman.prototype = Human.prototype
 // 那为什么不都在prototype把属性定义完?这里做个mark,回头给你看看
Superman.prototype = new Human();  

Superman.prototype.constructor = Superman;

Superman.prototype.fly = function () {
  console.log(this.name + ' is flying');
}

var sMM = new Superman('mm', 'female');

sMM.ml();  //调用了Human的ml方法
sMM.fly();  //调用子类Superman的方法
 先说明为什么不都在prototype把属性定义完,看看下面修改后的demo:
//定义一个父类 Huamn
function Human(name, gender) {
  this.name = name;
  this.gender = gender;
  //this.skills = new Array('talk', 'walk', 'sleep');
}

Human.prototype = {
  ml: function () {
    if(this.gender == 'male'){
      console.log(this.name + ' is feeling high');
    }else if(this.gender == 'female' ){
      console.log(this.name + ' is screaming');
    }else{
      console.log('-_-!...');
    }
  },
  
  skills:  new Array('talk', 'walk', 'sleep') // 在prototype中定义skills
};


//继承 父类Huamn,定义一个Superman类
function Superman(name, gender) {
  this.name = name;
  this.gender = gender;
}

Superman.prototype = new Human();  

Superman.prototype.constructor = Superman;

Superman.prototype.fly = function () {
  console.log(this.name + ' is flying');
}

var sMe = new Superman('me', 'male');
var hMM = new Human('mm', 'female');

console.log(sMe.skills);   // "talk", "walk", "sleep"
console.log(hMM.skills);   // "talk", "walk", "sleep"

sMe.skills.push('fly');

console.log(sMe.skills);  // "talk", "walk", "sleep", "fly"
console.log(hMM.skills);   //"talk", "walk", "sleep", "fly"   ,这就不对了
 这里只对sMe实例的skills push了一个'fly',但hMM的skills却同时多了'fly',这样是不对的。所谓对象模板,即所有的实例都共享这一模板,而且本身又是个对象,于是就所以然了。
这里的原始版的继承实现不得不指出:
  1. Superman.prototype = new Human();  实属不妥,我构建Superman的类,却先实例化一个Huamn对象,如此而然,当new Superman()的时候,实例sMe里会有两套this.name ,this.gender——因为Superman.prototype = new Human(),之后,等价于:
    function Superman(name,  gender){
      this.name = name;
      this.gender = gender;
      prototype = function Human() {
          this.name = name;
          this.gender = gender;
          this.skills = new Array('talk', 'walk', 'sleep');
          ...
      }
    }
      所以可以预言,即便Human的skills不放在prototype中,对于两个不同的Superman实例仍会出现demo2 的问题,demo:
    //定义一个父类 Huamn
    function Human(name, gender) {
      this.name = name;
      this.gender = gender;
      this.skills = new Array('talk', 'walk', 'sleep');
    }
    
    Human.prototype = {
      ml: function () {
        if(this.gender == 'male'){
          console.log(this.name + ' is feeling hign');
        }else if(this.gender == 'female' ){
          console.log(this.name + ' is screaming');
        }else{
          console.log('-_-!...');
        }
      }
      
    };
    
    //继承 父类Huamn,定义一个Superman类
    function Superman(name, gender) {
      this.name = name;
      this.gender = gender;
    }
    
    Superman.prototype = new Human();  
    
    Superman.prototype.constructor = Superman;
    
    Superman.prototype.fly = function () {
      console.log(this.name + ' is flying');
    }
    
    var sMe = new Superman('me', 'male');
    
    var hMM1 = new Human('mm1', 'female');
    
    var hMM2 = new Superman('mm2', 'female');
    
    console.log(sMe);   // "talk", "walk", "sleep"
    console.log(hMM1.skills);   // "talk", "walk", "sleep"
    console.log(hMM2.skills);   // "talk", "walk", "sleep"
    
    sMe.skills.push('fly');  //sMe超人的skills才包含fly
    
    console.log(sMe.skills);  // "talk", "walk", "sleep", "fly"
    console.log(hMM1.skills);   //"talk", "walk", "sleep"
    console.log(hMM2.skills);   //"talk", "walk", "sleep", "fly"   ,这就不对了
    
     
  2. 感觉很挫,说是继承,但Superman需要知道太多Human的细节,感觉还不如重新单独搞个Superman,然后拿Human的copy 来的优雅。

改良版:必须解决原始版的Superman.prototype = new Human();问题,然后封装继承的细节:

 

// 将继承的细节封装到一个全局自定义函数Class
// 用法是: var Human = Class({inti:function(name, gender){...}, ml:function(){...}, ...});
// 即inti是必须要得,初始化属性用的
function Class(parentCls, props) {
  
  if( (typeof parentCls) === 'object' ) {  // 创建类构造函数时的情形,而非继承一个类
    props = parentCls;
    parentCls = null;   //  把parentCls为null做成一个flag,使得下面的处理为创建类,而非继承类
  }
  
  
  this._initCls = false;
  
  //无论是构建类还是继承类,Class终究要返回一个类的构造函数
  //cFunc将作为最终的返回值  
  function cFunc() {
    if( !_initCls && this.init ) {
      
      if(parentCls)
        this.baseprototype = parentCls.prototype;  //拿着父类的prototype,有方无便
      
      // _initCls用作继承时的情形,避免init分别被父子类调用两次
      // 因为实现继承需要cFunc.prototype = new parentCls();
      // 当new YourClass()去构建类对象实例时,无疑会造成上述原始版中提到的属性重复以及prototype定义属性问题,这里设定为当真正new cFunc()的时候才调用init方法
      // 注意 无论parentCls 还是YourCls,本质都是这里的有Class返回的cFunc
      this.init.apply(this, arguments);   
    }
  }
  
  
  if( parentCls ){
    //从parentCls 继承一切, 所以设_initCls 为false避免下面new parentCls()时执行parentCls的init函数
    this._initCls = true;
    cFunc.prototype = new parentCls();
    cFunc.constructor = cFunc;
    //把_initCls设置false,当真正调用new cFunc();的时候才调用init函数
    //因为只用那时候才能执行YourCls自己的init函数
    this._initCls = false;
  }
  
  
  //配置cFunc函数,把props的一切传入cFunc
  for( var name in props ) {
    if( props.hasOwnProperty(name) ) {
      
      //覆盖父类parentCls的同名函数
      if( parentCls && (typeof props[name]) === 'function'
            && (typeof cFunc.prototype[name]) === 'function' ) {
        
        cFunc.prototype[name] = (function(name, fn) {
          //这是闭包用法,返回一个函数
          return function() {
              this.base = parentCls.prototype[name]; // 存储父类的同名函数,相当于java的super(),供子类调用父类方法用
              return fn.apply(this, arguments);  // 调用子类自己定义的方法, 这里用return是考虑到有些方法是有返回值得
          }
        })(name, props[name]);
      } else {
         cFunc.prototype[name] = props[name]; 
      }
    }
  }
  
  return cFunc;
}

// ----------------测试:创建一个类Huamn,一个子类Superman--------------------
var Human = Class({
  init: function(name, gender){
    this.name = name;
    this.gender = gender;
    this.skills = new Array('talk', 'walk', 'sleep');
  },
  ml: function() {
     if(this.gender == 'male'){
      console.log(this.name + ' is feeling high');
    }else if(this.gender == 'female' ){
      console.log(this.name + ' is screaming');
    }else{
      console.log('-_-!...');
    }
  }
});


var Superman = Class(Human, {
  pangzi: 'red', //定义一个变量胖次(内裤),默认为红色,次变量将放入prototype中
  // 此处没定义init方法,当new Superman()的时候会调用父类的,把name跟gender设置好
  ml: function() {
    this.base();
    console.log('Power up');
  },
  fly: function() {
    console.log('fly up high');
  }
});

var sMe = new Superman('me', 'male');
var hMM = new Human('mm', 'female');
var sMe2 = new Superman('me2', 'male');

console.log(sMe);
console.log(hMM);

sMe.skills.push('fly');
 
console.log(sMe.skills); //  ["talk", "walk", "sleep", "fly"] 
console.log(sMe2.skills); // ["talk", "walk", "sleep"] 
console.log(hMM.skills); // ["talk", "walk", "sleep"] 


sMe.pangzi = 'none';  //脱掉
console.log(sMe.pangzi);   //  none
console.log(sMe2.pangzi);  // red

sMe.ml();
hMM.ml();

 整理一下思路,1.根据传入的参数判断是创建类还是继承创建类。

                          2. 定义初始的类构造函数cFunc —— 可以理解为一个未成型的胚胎,里面主要是配置使得真正被调用是调用传进的init方法去初始化属性值

                          3. 若是继承操作,通过形如Subxxx.prototype = new Superxxx(); 将父类的一切传给子类

                          4. 配置子类自己定义的属性以及方法。

 

注意一点,看看上面的内裤(pangzi),变量,没有将其放入init方法去初始化,那么它将藏入prototype中,上文提到,prototype的非方法属性问题,(好比java的一个静态变量),但上面测试的我把它脱了,按道理sMe2的也会脱了才对。。。若现在把pangzi的定义改为如下:

 

pangzi: {color:'red'},
...

//测试代码改为
sMe.pangzi.color = 'none';
console.log(sMe.pangzi);   //none
console.log(sMe2.pangzi);   //none

 这里我估计原因是javascript当中对于基本数据类型的处理方式,回顾上文的skills数组,也是个Object,也是出问题。具体的在下是不懂了。

 

改良版有两个问题:

  1. 引入了全局变量_initCls。
  2. 请看上面一段代码:
cFunc.prototype[name] = (function(name, fn) {
          //这是闭包用法,返回一个函数
          return function() {
              this.base = parentCls.prototype[name]; // 存储父类的同名函数,相当于java的super(),供子类调用父类方法用
              return fn.apply(this, arguments);  // 调用子类自己定义的方法, 这里用return是考虑到有些方法是有返回值得
          }
        })(name, props[name]);

注意this.base,当子类调用父类的同名方法的时候,this指的是具体的子类对象,即,每调用一下父类同名方法,this.base就指向父类的同名方法了,在下不清楚javascript有无并发机制,或者某些js框架有无,若有,则这种写法就不行了,若无,则就调用结果而言,并无大碍,但总觉有点扯蛋。

 

现在介绍优雅版,解决改良版的两个问题,先看看是如何调用的:

var Human = Class.extends({
   init: function(name, gender){
    this.name = name;
    this.gender = gender;
    this.skills = new Array('talk', 'walk', 'sleep');
  },
  ml: function() {
     if(this.gender == 'male'){
      console.log(this.name + ' is feeling high');
    }else if(this.gender == 'female' ){
      console.log(this.name + ' is screaming');
    }else{
      console.log('-_-!...');
    }
  }
});

var Superman = Human.extends({
  pangzi: 'red', 
  ml: function() {
    this._super();
    console.log('Power up');
  },
  fly: function() {
    console.log('fly up high');
  }
});

 

优雅版的实现(John Resig关于JavaScript继承的一个实现,John Resig,jQuery的创始人)

(function() {// js 一加载便执行该方法,即方法中的this引用的是window对象,这样做主要是运用闭包把改良版中的全局变量消除
   
  var _initCls = false;
  
  // fnTest为一正则表达式,用于判断子类中有无对父类的引用
  var fnTest;
  if( /xyz/.test(function(){ xyz; }) ) {  //先看看浏览器对test的支持如何
     fnTest = /\b_super\b/;
  }else {
    alert('Sorry, Web Browser not support.'); 
  }
  
 
  this.Class = function() {};  //此实现优雅之所在
 
 //这里看看上文的调用demo便知Class指的是父类,所以在调用的extends方法中,this指的是Class,即父类
  Class.extends = function (props){
    var _super = this.prototype;
   
    _initCls = true;
    var prototype = new this();
    _initCls = false;
   
   
    for( var name in props ) {
    
    // 判断当前方法是否对父类的同名方法有覆盖
      if ( (typeof props[name]) === 'function' 
        && (typeof _super[name]) === 'function' 
        && fnTest.test(props[name]) )   {
      
        prototype[name] = (function(name, fn) {
          return function() {
          //此处先把_super缓存起来,因为这里要执行子类的父类同名方法,
          //于是在fn.apply之前,把this._super先指向父类的同名方法,
          //如此就可以在子类的方法体内使用this._super()去调用父类的方法
            var tmp = this._super;
            this._super = _super[name];
            var ret = fn.apply(this, arguments);
          //当方法执行完毕,马上把this._super设置回原始值
          // 但这样并无完全解决改良版中的问题2,要是js将来引进并发机制,问题依旧,
          //只是,比起改良版中的做法,这里就不扯蛋了  
            this._super = tmp;
            return ret;  
          }
        })(name, props[name]);
      } else {
        prototype[name] = props[name];
      }
    }  
  
   //类的构造函数
    function ClsFunc() {
      if( !_initCls && this.init ) 
        this.init.apply(this, arguments);
    }
   
    ClsFunc.prototype = prototype;
    ClsFunc.constructor = ClsFunc;
    // 子类自动获取extends方法,arguments.callee指向当前正在执行的函数,即Class.extends
    ClsFunc.extends = arguments.callee;
    return ClsFunc;
 };
})();


//------------------------------测试demo----------------------------------------

var Human = Class.extends({
   init: function(name, gender){
    this.name = name;
    this.gender = gender;
    this.skills = new Array('talk', 'walk', 'sleep');
  },
  ml: function() {
     if(this.gender == 'male'){
      console.log(this.name + ' is feeling high');
    }else if(this.gender == 'female' ){
      console.log(this.name + ' is screaming');
    }else{
      console.log('-_-!...');
    }
  }
});

var Superman = Human.extends({
  pangzi: 'red', 
  ml: function() {
    this._super();
    console.log('Power up');
  },
  fly: function() {
    console.log('fly up high');
  }
});

var sMe = new Superman('me', 'male');
var hMM = new Human('mm', 'female');
var sMe2 = new Superman('me2', 'male');

console.log(sMe);
console.log(hMM);

sMe.skills.push('fly');
 
console.log(sMe.skills); //  ["talk", "walk", "sleep", "fly"] 
console.log(sMe2.skills); // ["talk", "walk", "sleep"] 
console.log(hMM.skills); // ["talk", "walk", "sleep"] 


sMe.pangzi = 'none';  //脱掉
console.log(sMe.pangzi);   //  none
console.log(sMe2.pangzi);  // red

sMe.ml();
hMM.ml();

 

关于优雅版如何除掉改良版中的全局变量在此作简述,首先看看下面一段demo:

//这里的test方法的局部变量_init,作用范围为test方法体,包括retFunc,执行retFunc方法的时候,javascript解析器会先从retFunc方法体找_init,找不到,再从初始化retFunc的作用域范围找(即,test(),而非function test(){...})
function test(p) {
  var _init = false;
  
  if(p) {  //若有参数传入,设_init为true
    _init = true;
  }
 
  function retFunc() {  //运用闭包,使之可以使用_init变量
    if( _init == true ) {
      console.log('log me true');
    }else {
      console.log('log me false');
    }
  }
  
  return retFunc;
}

function objFunc() {
  var _inti = false;
  
  function ownRet() {
    if( _inti == true ) {
      console.log('log me true');
    }else {
      console.log('log me false');
    }
  }
  
  return ownRet;
};

var ret = test();
ret();  // log me false, 此时ret的初始化作用域为test(), 域中_inti为false

var ret2 = test('3');  //log me true, 此时ret的初始化作用域为test('3'), 域中_inti为true
ret2();

objFunc.ret = ret2;  //log me true,方法亦对象,ret2致死其初始化作用域都为test('3'),干爹可以变,生爹无法变
objFunc.ret();  

objFunc()(); //log me false, 此处执行的是objFunc的ownRet,初始化作用域为objFunc()

现在可以回头理解优雅版如何解决改良版的问题1了。

 

 其实可以根据优雅版的实现,对改良版进行稍稍的修改,推出改良版2:

(function () {   //js加载即启动,参照优雅版的
  
  this.Class = function(){};   //照搬优雅的核心
  
  var _initCls = false;   // 不再是全局变量
  
  Class.extends = function(props){    //将原来的类函数构造逻辑移入extends中
    
    var parentCls = null;
    if( this !== Class ) {
      parentCls = this;
    }
    
    function cFunc() {
     
      if( !_initCls && this.init ) {
      
        if( parentCls )  
          this.baseprototype = parentCls.prototype;  
      
        this.init.apply(this, arguments);   
      }
    }
    
    cFunc.extends = arguments.callee;
    
    if( parentCls ) {
      _initCls = true;
      cFunc.prototype = new parentCls();
      cFunc.prototype.constructor = cFunc;
      _initCls = false;
    }
    
    for( var name in props ) {
      
      if( props.hasOwnProperty(name) ) {
           
        if( parentCls && (typeof props[name]) === 'function'
               && (typeof cFunc.prototype[name]) === 'function'
               &&  /\b_super\b/.test(props[name]) ) {   //参照优雅版的逻辑
       
          cFunc.prototype[name] = (function(name, fn) {
        
            return function() {
              this._super = parentCls.prototype[name];  //参照优雅版的逻辑
              var ret = fn.apply(this, arguments);  
              this._super = null;
              return ret;
            }
          })(name, props[name]);
        } else {
          cFunc.prototype[name] = props[name];
        }
      }
    }
    
    return cFunc;
  };
  
})();



//------------------------------测试demo----------------------------------------

var Human = Class.extends({
   init: function(name, gender){
    this.name = name;
    this.gender = gender;
    this.skills = new Array('talk', 'walk', 'sleep');
  },
  ml: function() {
     if(this.gender == 'male'){
      console.log(this.name + ' is feeling high');
    }else if(this.gender == 'female' ){
      console.log(this.name + ' is screaming');
    }else{
      console.log('-_-!...');
    }
  }
});

var Superman = Human.extends({
  pangzi: 'red', 
  ml: function() {
    this._super();
    console.log('Power up');
  },
  fly: function() {
    console.log('fly up high');
  }
});

var sMe = new Superman('me', 'male');
var hMM = new Human('mm', 'female');
var sMe2 = new Superman('me2', 'male');

console.log(sMe);
console.log(hMM);

sMe.skills.push('fly');
 
console.log(sMe.skills); //  ["talk", "walk", "sleep", "fly"] 
console.log(sMe2.skills); // ["talk", "walk", "sleep"] 
console.log(hMM.skills); // ["talk", "walk", "sleep"] 


sMe.pangzi = 'none';  //脱掉
console.log(sMe.pangzi);   //  none
console.log(sMe2.pangzi);  // red

sMe.ml();
hMM.ml();

 

 

 最后,尝试理解库类级源码对继承的实现——源自Prototypejs

 

先看调用方式:

 

var Human = Class.create({
   initialize: function(name, gender){
    this.name = name;
    this.gender = gender;
    this.skills = new Array('talk', 'walk', 'sleep');
  },
  ml: function() {
    if(this.gender == 'male'){
      console.log(this.name + ' is feeling high');
    }else if(this.gender == 'female' ){
      console.log(this.name + ' is screaming');
    }else{
      console.log('-_-!...');
    }
  }
});

var Superman = Class.create(Human, {
  pangzi: 'red', 
  ml: function($super) {  //此处将$super作为父类方法对象引用,然后当成参数传入子类同名方法体内
    $super();  // 于是就可以这样调用父类的方法了
    console.log('Power up');
  },
  fly: function() {
    console.log('fly up high');
  }
});

var sMe = new Superman('me', 'male');
var hMM = new Human('mm', 'female');
var sMe2 = new Superman('me2', 'male');

console.log(sMe);
console.log(hMM);

sMe.skills.push('fly');
 
console.log(sMe.skills); //  ["talk", "walk", "sleep", "fly"] 
console.log(sMe2.skills); // ["talk", "walk", "sleep"] 
console.log(hMM.skills); // ["talk", "walk", "sleep"] 


sMe.pangzi = 'none';  //脱掉
console.log(sMe.pangzi);   //  none
console.log(sMe2.pangzi);  // red

sMe.ml();
hMM.ml();
 

 

看看其继承的实现方式:

var Prototype = { 
    emptyFunction: function() { }
};

// 用于把参数转换为数组形式
function $A(iterable) {
    if( !iterable ) return [];
    if( iterable.toArray ) return iterable.toArray();
    var length = iterable.length || 0;
    var results = new Array(length);
    
    while ( length-- ) 
        results[length] = iterable[length];
    
    return results;
}

// 直接将source的own properties复制给destination
Object.extend = function(destination, source) {
    for( var property in source ) 
        destination[property] = source[property];
    return destination;
};

// 定义一组通用方法,类比java的Object类的equals,hashCode 等方法
Object.extend(Object, {
    //获取对象中的属性
    keys: function(object) { 
        var keys = [];
        for (var property in object)
            keys.push(property);
         
        return keys;
    },
    
     //判断对象是否为方法对象
    isFunction: function(object) {
         return typeof object == "function";
    },
     
    // 判断对象undifined
    isUndefined: function(object) {
        return typeof object == "undefined";
    }
});

Object.extend(Function.prototype, {      
    //此方法运用正则表达式抽取function的参数名,具体:
    //  对于regexp:  /^[\s\(]*function[^(]*\(([^\)]*)\)/,match出来一个数组,
    // 下标为1的就是function(param1, param2, ...) 中的"param1, param2, ...“字符串
    // .replace(/\s+/g, ''), 将字符串的空白字符除掉,最后根据","split成数组
    argumentNames: function() {
        var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(',');     
        return names.length == 1 && !names[0] ? [] : names; 
    },
            
    // 此方法用于,举例,将方法meFunc通过 meFunc.bind(mmSession); 
    // 邦定到mmSession去准备执行,与apply类似,不过这里只拿第一个参数作邦定
    // 与apply不一样的是,这里将方法对象返回,而不会执行_method
    bind: function() {
        if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
        var _method = this;
        var args = $A(arguments);
        var object = args.shift();
               
        //返回一个方法对象
        return function() {      
            // 这里为何 args.concat($A(arguments))? 只因此时此处是处于一个方法对象的闭包当中,
            // 所以此$A(arguments) 不同于上面的$A(arguments), 这里是当方法对象被执行时的参数
            return _method.apply(object, args.concat($A(arguments)));
        }
    },
            
    // 首先要明确这个方法是约定由function调用,作用是把调用wrap的那个方法
    // 经过邦定后将其作为参数传给wrapper方法,其意图是用于重写父类的同名方法时,
    // 子类的方法中拥有了父类方法的引用了
    wrap: function(wrapper) {
        var _method = this;
               
        return function() {
            return wrapper.apply(this, [_method.bind(this)].concat($A(arguments)));
        } 
    }
});

// 扩展Array,加入一个first方法,返回第一个元素
Object.extend(Array.prototype, {
    first: function() {
        return this[0];
    }
    /*
    ,...其他方法定义
    */
});

// 定义全局变量Class
var Class = {
    // 基本跟前面讲的继承实现一致
    create: function() {
        var parent = null;
        var properties = $A(arguments);
        
        // 判断是否为继承操作
        if( Object.isFunction(properties[0] ) ) 
            parent = properties.shift();
        
        function klass() {
            this.initialize.apply(this, arguments);
        }
        
        Object.extend(klass, Class.Methods);
        klass.superclass = parent;
        klass.subclasses = [];
        
         // 这里为创建类时不调用父类的构造函数提供了一种新的途径
        // 使用一个中间过渡类, 与前面用一个boolean的做法较之,这里优雅多了
        if( parent ) {
            var subclass = function(){};
            subclass.prototype = parent.prototype;
            klass.prototype = new subclass();
            parent.subclasses.push(klass);
        }
       
        for( var i = 0; i < properties.length; i++ ) 
            // 注意此处的addMethods
            klass.addMethods(properties[i]);
        
        if( !klass.prototype.initialize ) 
            klass.prototype.initialize = Prototype.emptyFunction;
        
        klass.prototype.constructor = klass;
        
        return klass;
    }
};



Class.Methods = {
    // 此方法用于将source中的方法取出,然后放入子类的prototype中,
    // 另外,实现父类的同名方法重写
    addMethods: function (source) {
        var ancestor = this.superclass && this.superclass.prototype;
        var properties = Object.keys(source);
        
        // 对于IE8 ... for ( var property in source ) 是遍历不出toString的,
        // 所以此code是针对IE8的
        if( !Object.keys({toString: true}).length ) 
            properties.push('toString', 'valueOf');
        
        //遍历属性
        for( var i=0, length=properties.length; i<length; i++ ) {
            var property = properties[i];  // 属性名
            var value = source[property];  // 属性值
            
            // 判断若有对父类方法的重写, 则使得参数中的 $super 指向父类的同名方法
            if( ancestor && Object.isFunction(value) && value.argumentNames().first() == '$super' ) {
                // 这是子类的方法
                var method = value;
                // 分析此句:运用闭包,获取父类同名方法对象ancestor[m], 将其包裹与一个方法对象返回,
                // 目的是使得父类同名方法对象ancestor[m]在子类范围“this”中执行
                // 返回的方法对象调用wrap,目的是将经过包裹的父类方法对象传给子类方法method
                // 按上分析,这里用 value =  ancestor[property].wrap(method);
                value = (function(m) {
                    return function() { return ancestor[m].apply(this, arguments) };                    
                })(property).wrap(method);
                
                // 此二句code目的是将vauleOf和toString还原,
                // 因为上面调用了wrap,看看wrap方法实现,其返回一个包裹方法对象,
                // 所以此时的valueOf与toString是描述包裹方法的,而不是method,
                // 此时绑定一下就是使其变回描述method
                value.valueOf = method.valueOf.bind(method);
                value.toString = method.toString.bind(method);
            }
            
            this.prototype[property] = value;
        }
        return this;
    }
    
     /*
        ,...还有其他的方法定义
      */
};

 

 

 若理解上述,试着参照prototypeJS的继承实现方式重写我们自己的实现方式(使用改良版进行改造):

// 这个方法是盗用Prototypejs中的定义
function argumentNames(fn) {
    var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(',');
    return names.length == 1 && !names[0] ? [] : names;
}

function Class(parentCls, props) {
  
  if( (typeof parentCls) === 'object' ) { 
    props = parentCls;
    parentCls = null;   
  }
  
 
  function cFunc() {
     if(parentCls)
        this.baseprototype = parentCls.prototype;
      this.initialize.apply(this, arguments);      
  }
    
   // 盗用prototypejs的优雅写法,以消去全局变量
  if( parentCls ) {
    var subclass = function(){};
    subclass.prototype = parentCls.prototype;
    cFunc.prototype = new subclass();
    cFunc.prototype.constructor = cFunc;
  }
  
  
  //配置cFunc函数,把props的一切传入cFunc
  for( var name in props ) {
    if( props.hasOwnProperty(name) ) {
      
      //覆盖父类parentCls的同名函数
      if( parentCls && (typeof props[name]) === 'function'
            &&  argumentNames(props[name])[0] === "$super" ) {
        
        cFunc.prototype[name] = (function(name, fn) {
          return function() {
              var method = this;
              $super = function() {
                  return parentCls.prototype[name].apply(method, arguments);
              }
              // 此处无深意,只是技巧地,像Java中调用静态方法一样,调用concat将$super
              // 整合如方法参数中
              return fn.apply(this, Array.prototype.concat.apply($super, arguments));  
          }
        })(name, props[name]);
      } else {
         cFunc.prototype[name] = props[name]; 
      }
    }
  }
  
  return cFunc;
}

 测试demo就不写了,不能再让mm scream 了。。。

 在下承认确实费了吃奶的劲去弄懂上述,总结出把握js的闭包运用是关键。玩弄透彻javascript的继承,对一些js框架如dojo,extJs,以及正在学的Angular,即使当中很多跟本篇不搭干,想必也不会觉其太诡异,太晦涩。

 

 最后介绍一个很帅的在线前端测试工具:http://jsfiddle.net/

分享到:
评论

相关推荐

    Javascript面向对象编程.

    4. **类(Class)**:ES6引入了类语法糖,使得JavaScript的面向对象更加符合传统面向对象语言的写法,但实际上,这些“类”仍然基于原型实现。 ```javascript class Person { constructor(name) { this.name = ...

    JavaScript面向对象编程指南(第2版).rar

    5. 类(Class)语法:ES6引入的类语法糖,提供更接近传统面向对象语言的写法。 6. 封装与模块化:如何使用闭包和立即执行函数表达式(IIFE)来实现封装,以及AMD、CommonJS和ES6模块系统。 7. 继承模式:浅谈原型...

    《JavaScript内核系列》和《JavaScript面向对象基础》

    《JavaScript内核系列》和《JavaScript面向对象基础》这两本书籍是深入理解JavaScript语言核心机制和面向对象编程的重要参考资料。JavaScript作为一种广泛应用于Web开发的脚本语言,其内核和面向对象特性对于开发者...

    JavaScript面向对象程序程序设计PPT与代码

    ES6引入了类的概念,虽然JavaScript的类本质上仍然是函数,但它提供了一种更符合传统面向对象语法的写法。例如: ```javascript class Person { constructor(name) { this.name = name; } sayName() { ...

    javascript 面向对象程序设计博客文章

    每个JavaScript对象都有一个`__proto__`属性,指向其构造函数的原型对象。原型对象又有一个`__proto__`属性,形成一个链,这就是原型链。通过原型链,对象可以访问到构造函数原型上的属性和方法。例如: ```...

    javascript对象参考手册

    10. **类与继承**:ES6引入的类是基于原型的语法糖,它提供了更接近传统面向对象编程的写法。类支持继承,通过`extends`关键字,一个类可以继承另一个类的属性和方法。 以上只是JavaScript对象涉及的一部分知识点,...

    javascript面向对象之访问对象属性的两种方式分析

    总的来说,通过点表示法和方括号表示法,我们可以方便地读取和设置JavaScript对象的属性。在实际编程中,根据需求选择合适的方式,是提高代码效率和可维护性的关键。希望本文的分析能帮助你深化对JavaScript面向对象...

    js面向对象游戏开发,飞机大战

    4. 类与继承:在ES6中,引入了`class`语法糖,虽然在JavaScript引擎底层仍然是基于原型的实现,但提供了更接近传统面向对象语言的写法。例如,我们可以创建一个`EnemyPlane`类继承自`Plane`,添加敌机特有的行为。 ...

    js面向对象

    2. **原型**:每个JavaScript对象都有一个内部原型(__proto__),它链接到另一个对象,这个对象可以提供继承的属性和方法。`Person.prototype`可以添加共享的方法: ```javascript Person.prototype.greet = ...

    怎么理解js的面向对象编程共9页.pdf.zip

    ES6引入了类的语法糖,提供了一种更接近传统面向对象语言的写法。实际上,JavaScript的类仍然是基于原型的,类的实例化过程与使用构造函数和`prototype`相同。 9. **静态方法和属性**: 类可以拥有静态方法和属性...

    JavaScript语法手册&JavaScript程序范例

    这份"JavaScript语法手册&JavaScript程序范例"旨在帮助你深入理解JavaScript的核心概念,并通过实例加深对语法的理解。 一、JavaScript基础语法 1. 变量声明:JavaScript使用`var`、`let`和`const`进行变量声明。`...

    Javascript中的对象.pdf

    JavaScript对象的属性分为数据属性和访问器属性两种: 1. 数据属性由以下特性组成:[[Writable]](可写)、[[Value]](值)、[[Enumerable]](可枚举)、[[Configurable]](可配置)。 2. 访问器属性由以下特性组成...

    javascript对象的创建和访问_.docx

    尽管JavaScript没有真正的类,但ES6引入了Class语法,提供了更接近传统面向对象语言的写法,其实质仍然是基于原型的: ```javascript class Person { constructor(name, age) { this.name = name; this.age = ...

    js面向对象(部分)

    ES6引入了类语法,使得JavaScript的面向对象编程更加接近传统类语言。类实际上是一个特殊的函数,但它们的写法更加符合面向对象的思维。类有构造函数(`constructor`)、方法等。例如: ```javascript class ...

    javascript 2015语法

    JavaScript 2015语法,也被称为ECMAScript 6(ES6),是JavaScript的一个重大更新版本。这一版本在2015年获得正式采纳,引入了一系列新特性和语法改进,以帮助开发者编写更清晰、更模块化、更高效的代码。 ### ...

    JavaScript 语言用法指南,语法详细介绍,中文版的哦

    3. 类(ES6):JavaScript的类语法是基于原型的,但提供了更接近传统面向对象语言的语法糖。 五、异步编程 1. 回调函数:处理异步操作的常见方式,但可能导致回调地狱问题。 2. Promise:ES6引入的Promise对象...

    非常好的javascript原理资源,分享出来.zip

    javascript 的基础语法 面向对象的实现 设计模式实现 模块化开 javascript 常见的疑问 jQuery NodeJs html5 Javascript based 1.对象 JavaScript 引用 2.JavaScript this 3.JavaScript 闭包 4.JavaScript 事件 ...

    Javascript 程序设计基础教程(第2版)_习题答案

    理解原型和构造函数是掌握JavaScript面向对象编程的关键。 数组是另一种重要的数据结构,JavaScript提供了丰富的数组方法,如push、pop、shift、unshift、slice、splice等,用于操作和管理数组元素。此外,ES6引入...

    JavaScript第十章完整案例

    构造函数用于创建具有特定属性和方法的对象,原型链则允许对象共享属性和方法,而ES6引入的类语法则提供了一种更接近传统面向对象语言的写法。 在"完整的项目案例"中,我们可能会看到一个基于当当网的模拟电商系统...

    JavaScript for Absolute Beginners

    ES6引入的Class语法提供了更面向对象的写法,但实质上仍然是基于原型的。 在学习过程中,了解并实践JavaScript的调试技巧也很重要,如使用浏览器的开发者工具,设置断点、查看变量值、跟踪调用栈等。 总的来说,...

Global site tag (gtag.js) - Google Analytics