JavaScript面向对象程序设计

1.创建对象

JavaScript 把对象定义为 : 无序属性的集合, 其属性可以包括基本值, 对象或者 函数.

每个对象都是基于一个引用类型创建的.(  引用类型可以是原生的, 也可以是开发人员创建的 )

创建自定义对象的最简单方式就是创建一个 object 的实力, 再添加属性和方法. 例如 :

var person = new Object() ;

person.name = "Nicholas" ;

person.age = 29;

person.job = "Software Engineer" ;

person.sayName = function () {

   alert( this.name );    --> this.name 被解析成 person.name

}


 

早期的JavaScript开发人员经常使用这个模式创建新对象,  但这种方式有个明显的缺点, 使用同一个接口创建很多对象, 会产生大量的重复代码, 为解决这个问题, 人们开始使用工厂模式的一种变体。

工厂模式 ( 利用函数封装创建对象 )

这种模式抽象了创建具体对象的过程, 在 ECMAScript 中无法创建类, 开发人员发明了一种函数, 用函数来封装以特定接口创建对象的细节, 例如 :

function createPerson (name, age, job){

    var o = new Object ();

    o.name = name;

    o.age = age;

    o.job = job;

    o.sayName = function(){

      alert(this.name) ;

    } ;

    return o ;

}

var person1 = createPerson( "Nicholas", 29, "Software Engineer") ;

var person2 = createPerson( "Greg" , 27 , "Doctor" ) ;

person1.sayName() ;

person2.sayName();


 

工厂模式很好, 但却没有解决对象识别的问题 ( 即怎么样知道一个对象的类型) , 进尔出现了构造函数模式.

构造函数模式

function Person (name, age, job) {

  this.name = name ;

  this.age = age ;

  this.job = job ;

  this.sayName = function(){

    alert( this.name); 

  };

}

var person1 = new Person("Nicholoas", 29, "Software Engineer" ) ;         // 构造函数有个 new


 

与上边的CreatePerson()区别 : 没有显示的创建对象, 直接将属性和方法赋给了 this 对象. 没有 return 语句

 要创建一个 Person 的新实例, 必须使用 new 操作符,  以这种方式调用构造函数. 会有4个动作;

1)  创建一个新对象;

2)将构造函数的作用域赋给新对象( 因此 this 就指向了这个新对象)

3)执行构造函数中的代码( 为这个新对象添加属性);

4)返回新对象

以这种方式定义的构造函数是定义Global对象( 在浏览器中是 window对象)

任何函数, 只要通过 new 操作符来调用, 那么它就可以作为构造函数. 而任何函数, 如果不通过new 操作符号来调用, 那它跟普通函数没什么两样.

var pserson = new Person ( "Nicholas", 29 ,"Software" ) ;        // 当作构造函数

Pserson( "Greg" , 27, "Doctor" ) ;  // 作为普通函数, 添加到 windows  

var o = new Object() ;                         // 在另一个对象的作用域中调用

Person.call( o, "Kristen" , 25, "Nurse" ) ;   // call 是每一个函数都有的方法

o.sayName() ;           // Kristen

alert( person1.sayName == person2.sayName)      // false


 

将方法从构造函数中移出

因为构造函数中的方法实例并不是来自一个类, 所以它们是不同的方法,

function Person ( name, age, job ){

  this.name = name ;

  this.age = age ;

  this.job = job ;

  this.sayName = sayName ;   // 注意此时的 sayName右边没有括号, 表示将 sayName作为一个指针变量进行赋值, 而并非一个函数, 需要执行.

}

function sayName() {

  alert( this.name) ;

}

var person1 = new Person("Nicholas" , 29, "Software Engineer") ;

alert( person1.sayName == person2.sayName )   // true


 

这样, 方法来自于一个类, 但是... 此时 sayName实际上是个全局函数, 但是只是该类来调用, 所以就失去了"全局函数"的实质. 即, 在全局作用域中定义的函数实际上只能被某个对象调用, 这让全局作用域有点名不副实.更让人无法接受的是: 如果对象需要定义很多方法, 那么就要定义很多个全局函数, 于是我们这个自定义的引用类型就丝毫没有封装性可言.这样就引如了原型模式....

原型模式

prototype  原型属性 ( 前边介绍过) 这个属性是一个对象, 它的用途是包含可以由特定类型的所有实例共享的属性和方法.那么prototype就是通过调用构造函数而创建的那个对象的原型对象.

当调用构造函数创建一个新实例后, 该实例的内部将包含一个指针( 内部属性 ) , 指向构造函数的原型属性, 在很多实现中, 这个内部属性的名字是 _proto_ ,  就是这个连接存在于实例与构造函数的原型属性之间, 而不是存在与实例与构造函数之间.


Person.prototype.constructor = Person .

function Person(){

  Person.prototype.name = "nich" ;                 

  Person.prototype.sayName = function(){

    alert( this.name );

  }

}

var person1 = new Person () ;

var person2 = new Person ();


 


更简单的原型语法

function Person(){

}

Person.prototype = {                      // prototype 是一个对象, 这样子定义, 相当于重写了 prototype 对象

    constructor : Person ,              // 特殊声明

     name : "Nicholas" ,

     age : 29 ,

     job : "Software Engineer" ,

     sayName : function(){

           alert( this.name );

      }

} ;


 

最终结果与上边的原型 相同, 但有一个例外 : constructor 属性不再指向 Person了, ( 每创建一个函数, 就会同时创建它的 prototype 对象, 这个对象自动获得 constructor 属性, 而我们 在这里使用的语法, 本质上完全重写了默认的prototype对象, 因此 constructor 属性也就变成了新对象的 constructor 属性( 指向 Object构造函数), 不再指向Person函数.

如果 constructor 的值真的很重要, 可以进行特殊声明

由于在原型中查找值是一次搜索, 因此我们对原型对象所做的任何修改都能立刻从实例上反映出来-即使是先创建了实例后修改原型也是一样. ( 看图就知道, 是共享的内容, 所以肯定是一改变就全部都改变, 即便是先创建的对象, 因为指针指的位置没有变, 单个增加的函数也是一样 )

当我们调用实例的一个方法时, 比如 person.sayHi() , 首先会在实例中搜索名为 sayHi的属性, 在没找到的情况下, 会继续搜索原型 .因实例与原型之间的连接只不过是一个指针, 而非一个副本, 因此就可以在原型中找到新的sayHi属性并返回保存在那里的函数.

实例与原型之间是松散连接

尽管可以随时为原型添加属性和方法, 并且修改能够立即在所有对象实例中反映出来, 但是如果重写整个原型对象, 那么情况就不一样了, 我们知道, 构造函数会为实例添加一个指向最初原型的 _proto_ 指针, 请记注: 实例中的指针仅指向原型, 而不指向构造函数

1) 创建实例, 构造函数会为这个实例的指针 _proto_ 分配原型函数地址.

2) 原型函数被重写, 原型函数地址变更. ( 此时构造函数中的prototype指针变更为新的原型对象的指针, 因为在重写时语句是 Person.prototype = {}; 这说明什么, 就是将新的原型对象的指针赋值给 Person.prototype 指针啊. )

3) 已经创建的实例的 _proto_ 指针还指向原来的( 未重写之前) 的原型对象.

4) 所以当再调用时, 就会出错. ( 新创建的实例就不会有问题, 因为新创建的实例的_proto_ 属性是新分配的, 是正确的)

5) 所以, 已经有创建实例的原型就不能在用字面量方法, 即完全重写原型.


原型对象的问题, 它省略了为构造函数传递初始化参数这一环节, 结果所有实例在默认情况下都将取得相同的属性值, 尤其是引用类型, 比如数组会有问题.所以就有构造+原型

构造模式与原型模式相结合 ( 比较好的方法 )   -- 先使用这种吧

function Person(name, age, job){      // 不想要所有实例都共享的内容, 就写在构造函数中

   this.name = name ;

   this.age = age ;

   this.job = job; 

   this.firends = [ "Shelby" , "court"] ;    // 尤其是这种引用类型

}

Person.prototype = {                               // 使用这种字面量时, prototype会被重写, 所以尽量在定义时使用这种方法, 定义之后, 当需要新添加方法时, 不要使用这种方法

  consturctor : Person ,                           // 接上,而是直接添加方法 Person.prototype.addMethod = function(){} ;  如果不指定 constructor : Person , 默认的就会是Object.

  sayName : function() {                          // 想要所有实例都共享的内容, 就把它写在 Person.prototype中.

    alert( this.name ) ;

  }

} ;


 

这样, 不需要共享的就保存在构造函数中, 需要共享的就保存在 prototype 中.

alert( person1.firends === person2.friends)  //false

alert( person1.sayName === person2.sayName ) ; // true


 

动态原型模

以上, 已经做很好了。只是有其他OO语言经验的开发人员在看到嘟噜的构造函数和原型时, 会感到困惑(应该是在一起的 ) , 所以才用动态原型模型, 说白了就是将判断增加内容写在构造函数中.

function  Person( name, age, job ) {

    this.name = name;

    this.age = age;

    this.job = job;

    if ( typeof this.sayName != "function" ) {

      Person.prototype.sayName = function(){

             alert( this.name ) ;

      };

    }

}

var person = new Person ( "Nicholas" , 29, "Software engineer" );

person.sayName() ;


 

2. 继承 

每个构造函数都有一个原型对象, 原型对象包含一个指向构造函数的指针, 而实例都包含一个指向原型对象的内部指针.

Javascript中只有实现继承, ( 即没有那种, 只定义一个函数接口名称, 没有函数内容实现的继承) , 而且是通过原型链来实现的继承.


new SupperType() 构造函数创建了supperType的实例, 并将此实例赋给了 subType.prototype . 实现的本质是重写原型对象( 即 subType.prototype)代之以一个新类型的实例( new SupperType() 这样, 原来存在于 superType中的实例的所有属性和方法现在也都存在于 subType.prototype中. 假如supper.prototype 又是另外一个对象的实例, 那么, 就会出现层层递近的现象, 所有引用类型都是通过这种原型链的机制实现的, 所有引用类型默认都继承了Object,  )


可以重写父类的代码, 但是没有重载 ( 子类型会重写父类型的方法, 方法名相同的情况下, 如果出现重写, 通过SubType的实例调用该方法就是重写后的方法, SupperType实例调用该方法,则还是没有重写之前的方法,详情可以看上图 )

这里要注意 : 想要重写的方法和新增加的方法必须写在 SubType.prototype = new SuperType(); 这条语句之后. 还有就是不能使用 SubType.prototype = {}; 这种能够重写原型链的字面量形式.

确认原型和实例的关系, instanceof , Object.prototype.isPrototypeOf(instance) , 继承关系都会返回 true .

原型链的问题

原型链在继承时, subType.prototype = new SuperType() ; 此时 原型链由于继承的关系, 会将 subType当作时 SuperType的一个实例, 这样, superType中的属性,例如数组也会被继承, 这样就存在着之前说的问题, 引用类型的问题...跟前面一样...例如 :

function SuperType(){

  this.color = [ "red", "blue", "green"] ;        // 注意, 此处特意将引用类型写在构造函数中, 而不是 prototype( 原型中 )

}

function SubType(){

}

// 继承了 SuperType

SubType.prototype = new SuperType() ;

var instance1 = new SubType() ;

instance1.colors.push("black") ;      // "red","blue","green","black"

alert( instance1.colors );

var instance2 = new SubType() ;     // 一个另外的独立的对象

alert ( instance2.colors );                 // "red","blue","green","black"


 

当SubType通过原型链继承了SuperType之后, SubType.prototype就变成了SuperType的一个实例,因此就有了color属性.

在父类构造函数中的属性, 例如 color数组, 在继承时, 因为是通过父类的实例, 所以这个属性也顺理成章的被继承.进而成为子类的prototype( 注意color不是在父类的prototype中,但是由于继承,跑到了子类的prototype中,这样,所有的子类再创建实例时就会共享这个数组color, 这是不可以的.

另一个问题 : 在创建子类型的实例时, 不能向超类型的构造函数中传递参数,

借用构造函数技术

在子类型构造函数的内部调用超类型的构造函数,

function SubType(){

  SuperType.call(this , "参数") ;   // 调用父类构造函数

  this.age = 29 ;                             // 子类的成员

}


 

这样做, 虽然在继承是, 原型链将数组等内容改变, 但是通过调用父类的构造函数, 又重新初始化了一般数组类型, 进而得到正确的结果.

例如 color[] 这个数组, 开始颜色是 "red" ,"blue","green" ,  当在父类的一个实例中增加一个颜色 "black" , 而恰巧该实例被赋予给子类的原型, subType.prototype = new SupperType() ;  这时由于继承关系, subType中的数组默认就是 "red" , "blue" , "green" , "black" , 这是错的, 此时可以重新调用父类的构造函数来初始化这个数组为 "red","blue","green"

经典继承 ( 最常用 )又叫组合继承  会调用2次父类的构造函数   

将原型链和借用构造函数技术组合 ( 使用原型链实现对原型属性和方法的继承, 而借用构造函数来实现对实例属性的继承 )

( 方法全部写在 prototyoe 中, 属性全部写在 构造函数中 ) , 只是会调用2次父类的构造函数.

这种调用2次父类构造函数的情况, 实际上是一个屏蔽的过程,即子类实例中的color数组屏蔽了子类prototype中的属性color

寄生组合式继承 ( 开发人员普遍认为这种继承是最理想的 )   -->个人推荐


其实, 原来的思想是将 SubType.prototype = new SupperType() ; 即将父类的一个实例赋予子类的prototype, 但是, 父类的实例并不是"干净" 的, 有可能这个实例有自己的引用类型color, 而此时会一同继承, 所以, 现在的这种方法说就是 使用 subType.prototype = superType.prototype. 并且 subType.prototype.constructor = subType;

别的都一样, 只是将 var subType.prototype = new SuperType() 替换成 inheritPrototype( subType, SuperType );

function inheritPrototype(subType, superType){      // subType, superType 是2个构造函数

  var portotype = object(superType.prototype) ;         // 注意: 此时创建 superType.prototype的副本, 没有再开辟内存,是一个东西,没有 new

  prototype.constructor = subType ;                             // 因为重新定义了, 所以要重新定义 prototype.constructor

  subType.prototype = prototype ;                                // 最后将有继承信息的prototype 赋予给subType.prototype

}

这样, 就只调用了一次构造函数 .

前面方式 : 前面方式使用 SubType.prototype = new SupperType() , 此时, 如果向后, 父类又增加新方法时, 子类可以直接自动继承. 同时, 如果子类增加新的方法, 父类是不能够使用的, 所以个人这种方法更合乎逻辑, 只是调用了2次父类构造函数.

后面方式 : 后面父类的 SupperType.prototype 和 SubType.prototype 是一个东西... 所以现在父类增加新方法, 子类当然也可以直接使用. 并且此时, 如果子类增加一个新的方法,父类也可以直接使用.

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值