Ch6 Object-Oriented Programming(JavaScript中的面向对象编程)

    本文为书籍《Professional JavaScript for Web Developers, 3rd Edition》英文版第 6 章:“Object-Oriented Programming” 个人学习总结,主要介绍 JavaScript 中自定义类型的产生和类型继承实现的各种模式及其优缺点。

 

. 类和对象的产生
本节介绍在 10 种在 JavaScript 中产生对象的模式及其优缺点。
1.Object构造函数模式
var person = new Object (); person . name = "Nicholas" ; person . age = 29 ;

var person = {}; person . name = "Nicholas" ; person . age = 29 ;
2.对象字面表达(object literal) 模式
// 这是定义对象较好的一种方式,书写方便,简洁易读 var person = { // 属性和属性的值用冒号分开 name : "Nicholas" , // 用逗号分开属性定义 age : 29 // 最后定义的属性不需要逗号 }; // 结尾最好带分号 可以使用字符串和数字作为属性名称,如:
var person = { "name" : "Nicholas" , "age" : 29 , 5 : true // 为数字的属性名称将自动转为字符串 };
3.工厂模式
前两种创建对象的方式简单,但如果要创建具有相同类型的多个对象, 只能重复相同的代码。也就是说,无法创建一种类型的多个对象实例 。所以这两种方法只能产生一个对象,它们的类型始终是Object,没有 具体的类型。 即:var person1 = new person () 或var person1 = new person是无 效的。 工厂模式可以解决使用一种方式创建多个相同类型对象的问题,如:
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不能被判断是否为Person类型。
4.构造函数模式
function Person ( name , age , job ) { this . name = name ; //this关键字使得类型 this . age = age ; //的成员能够被外界访问 this . job = job ; this . sayName = function () { alert ( this . name ); }; } var person1 = new Person ( "Nicholas" , 29 , "Software Engineer" ); var person2 = new Person ( "Greg" , 27 , "Doctor" );
由于JavaScript中函数的性质,在产生类实例时可以不用传递全部 参数,甚至不传递参数,如:
var person1 = new Person () var person2 = new Person ( "Jim" ); var person3 = new Person ( "Tom" , 29 ); person1 . sayName (); //undefined person1 . name = "Tim" ; person1 . sayName (); //"Tim" person2 . sayName (); //"Jim" person3 . sayName (); //"Tom" 构造函数模式能够很好地产生一种类型,并创建这种类型的多个 对象实例。比如,我们根据面向对象的概念,可以认为 Person 一个类,而 person1 person2 Person 的两个实例对象。 可以判断构造函数产生的对象实例的类型,有两种方法:
// constructor 属性是每个引用类型对象都具有的属性 // 指向定义对象的构造函数 alert ( person1 . constructor == Person ); //true alert ( person2 . constructor == Person ); //true
alert ( person1 instanceof Object ); //true alert ( person1 instanceof Person ); //true alert ( person2 instanceof Object ); //true alert ( person2 instanceof Person ); //true ( 1 ) 是类,也是函数
构造函数模式建立的类是使用函数方式定义的,因此这种类型 既是类也是函数。下面的例子说明了这一点
//use as a constructor var person = new Person ( "Nicholas" , 29 , "Software Engineer" ); person . sayName (); //"Nicholas" //call as a function Person ( "Greg" , 27 , "Doctor" ); //adds to window window . sayName (); //"Greg" //call in the scope of another object var o = new Object (); Person . call ( o , "Kristen" , 25 , "Nurse" ); o . sayName (); //"Kristen" ( 2 ) 构造函数模式的问题
在JavaScript中,函数即对象,所以在类内部定义的方法,当实 例化类创建多个对象时,每个对象内的方法虽然具有相同的名称, 但却是不同的实例。所以构造函数模式产生的对象实例不能重用方法 功能,只能各自产生新的方法实例,造成代码冗余。可以将方法的定 义放到类的外部来解决这个问题。如:
function Person ( name , age , job ) { this . name = name ; this . age = age ; this . job = job ; this . sayName = sayName ; } function sayName () { alert ( this . name ); }
这样,sayName () 的功能便可以重用。但这样不利于代码组织,容 易产生混乱的代码。sayName () 函数本来只应属于类Person,却在全局 作用域中定义,可以在其它类、对象和函数等作用域内调用。这些问 题可以使用原型 ( Prototype ) 模式解决。
5.原型(Prototype) 模式
所有的函数都具有一个 prototype 属性,该属性对于引用类型的 实例都可用,是一个包含属性与方法的对象。每当通过构造函数产生 对象时,将以 prototype 作为原型产生对象。 prototype 的所有属性 和方法将在所有实例对象间共享。属性默认值在各实例对应属性中是 相等的,每个对象可以重新定义属性的值,但重新定义的属性 ( prototype 中属性名称相同 ) 仅位于每个对象 ( 实例 ) 上, prototype 的对应属性被屏蔽,但仍可以通过 prototype 属性访问。对象的信息可 以在构造函数中定义,也可以在 prototype 对象上定义。例如:
function Person () { } Person . prototype . name = "Nicholas" ; Person . prototype . age = 29 ; Person . prototype . job = "Software Engineer" ; Person . prototype . sayName = function () { alert ( this . name ); }; var person1 = new Person (); //person1.sayName(); //"Nicholas" var person2 = new Person (); //person2.sayName(); //"Nicholas" //alert(person1.sayName == person2.sayName); //true //alert(person1 instanceof Object); //true //alert(person1 instanceof Person); //true //alert(person2 instanceof Object); //true //alert(person2 instanceof Person); //true //name属性被重新定义在person1上, //prototype中的name属性被屏蔽 person1 . name = "Jim" ; person2 . name = "Tom" ; //person1.sayName(); //"Jim" -- 来自实例 //person2.sayName(); //"Tom" alert ( person1 . name ); //"Jim" -- 来自实例 //"Nicholas" -- 来自 prototype alert ( person1 . constructor . prototype . name ); var person3 = new Person (); person3 . sayName () //"Nicholas" -- 来自 prototype 也可以这样使用原型模式:
function Person () { } //使用对象方式定义原型,默认的prototype对象属性被覆写 Person . prototype = { name : "Nicholas" , age : 29 , job : "Software Engineer" , sayName : function () { alert ( this . name ); } };
这种原型模式与上一个例子中的大致相同,但是所产生实例的 constructor 属性不再指向初始的构造函数 ( 本例中为Person ) ,每个 实例的 constructor prototype 都会重新产生,覆写默认的 constructor prototype instanceof 运算符作用于每个实例 仍可以可靠地工作,constructor 却不可靠,如:
var friend = new Person (); alert ( friend instanceof Object ); //true alert ( friend instanceof Person ); //true alert ( friend . constructor == Person ); //false alert ( friend . constructor == Object ); //true 原因是, prototype 对象也具有自己的 constructor 属性, 默认指向 prototype 所在的构造函数,但是采用上面的原型模式 所产生的对象会覆写 prototype constructor 属性所指向的构 造函数。 这个问题可以通过以下方法解决:
function Person () { } Person . prototype = { constructor : Person , //设置 prototype 的构造函数 name : "Nicholas" , age : 29 , job : "Software Engineer" , sayName : function () { alert ( this . name ); } };
需要注意的是,使用对象覆写默认原型时,实例化应在定义原型 之后,否则可能得不到预期效果。如:
function Person () { } var friend = new Person (); Person . prototype = { constructor : Person , name : "Nicholas" , age : 29 , job : "Software Engineer" , sayName : function () { alert ( this . name ); } }; friend . sayName (); //error 这里,Person的实例 friend 产生在 prototype 对象被覆写之前, friend仍指向默认的 prototype ,但默认的 prototype 中没有 sayName () 方法,因此产生错误。如果 friend 产生在 prototype 对象 被覆写之后,则 friend 将指向新的 prototype ,不会出错。 可以通过 prototype 扩展或覆写像 window , string , math等预定义 对象的属性和方法。
( 1 ) 原型模式的问题
原型模式创建对象无法通过构造函数传递初始化参数,并且,若在 原型上定义的属性是引用类型,则每个实例相同名称的引用类型的属性 总是具有相同的值,这可以实现静态成员,但若每个实例想拥有各自的 引用类型的属性值,这就成了问题。因此,纯粹的原型模式也很少用。
6.构造函数/ 原型模式
这种模式属于构造函数模式和原型模式的混合。实例属性在构造函数 中定义,方法和共享属性在原型中定义。这种模式很好地综合了构造 函数模式和原型模式的优点,同时解决了各自的问题。如:
function Person ( name , age , job ) { this . name = name ; this . age = age ; this . job = job ; this . friends = [ "Shelby" , "Court" ]; } Person . prototype = { constructor : Person , sayName : function () { alert ( this . name ); } }; var person1 = new Person ( "Nicholas" , 29 , "Software Engineer" ); var person2 = new Person ( "Greg" , 27 , "Doctor" ); person1 . friends . push ( "Van" ); alert ( person1 . friends ); //"Shelby,Court,Van" alert ( person2 . friends ); //"Shelby,Court" alert ( person1 . friends === person2 . friends ); //false alert ( person1 . sayName === person2 . sayName ); //true 7.动态原型模式
这种模式将原型定义放在构造函数内部,必要时可以检查方法是否 可用以决定是否需要初始化原型。如:
function Person ( name , age , job ) { //properties this . name = name ; this . age = age ; this . job = job ; //methods //if (typeof this.sayName != "function") //{ Person . prototype . sayName = function () { alert ( this . name ); }; //} } var friend = new Person ( "Nicholas" , 29 , "Software Engineer" ); friend . sayName ();
可以通过 if 语句检查任何属性或方法,但没有必要使用多个 if 句,检查任何一个属性或方法即可。这种模式具有以上各种模式的优点 ,同时便于代码组织。
8.寄生(Parasitic) 构造函数模式
这种模式类似于工厂模式的定义,但创建对象时,使用 new 关键字。 例如:
function Person ( 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 friend = new Person ( "Nicholas" , 29 , "Software Engineer" ); friend . sayName (); //"Nicholas" alert ( friend instanceof Person ); //false 这种模式产生的对象与构造函数无关,也不能依赖 instanceof 运算符 判断对象的类型,因此,仅在特殊情况下使用该模式。
9.持久(Durable) 构造函数模式
这种模式创建的对象没有公共属性,方法也不引用 this 对象。此模式 常应用于安全环境和混搭模式,不允许使用 this new。如:
function Person ( name , age , job ) { //create the object to return var o = new Object (); //optional: define private variables/functions here //attach methods o . sayName = function () { alert ( name ); }; //return the object return o ; } var friend = Person ( "Nicholas" , 29 , "Software Engineer" ); friend . sayName (); //"Nicholas" 类似于寄生函数模式,不能依赖 instansof 运算符判断对象类型。
10.ECMAScript 6 模式
ECMAScript 6 中引入了许多面向对象语言的特性,在实现了 ECMAScript 6 JavaScript中,可以像许多 C 语言风格的面向对象 编程语言一样定义类。例如,一个常规的 JavaScript 自定义类型如下:
function Person ( name , age ) { this . name = name ; this . age = age ; } Person . prototype . sayName = function () { alert ( this . name ); }; Person . prototype . getOlder = function ( years ) { this . age += years ; };
如果使用实现了 ECMAScript 6 JavaScript来定义与上例相同的 类型,可以这样:
class Person { constructor ( name , age ) { public name = name ; public age = age ; } sayName () { alert ( this . name ); } getOlder ( years ) { this . age += years ; } }
但是由于目前浏览器的兼容性原因,ECMAScript 6 定义类型的模式 也不常用。所以这里不做详细介绍。 为了使 JavaScript 更好地以 ECMAScript 6 方式工作,同时解决浏 览器兼容性问题,微软推出了开源的 TypeScript,其官方网址是: http : //www.typescriptlang.org/ . 继承
JavaScript中,主要是通过原型链 ( Prototype Chaining ) 来实现继承, 并且只有实现继承,没有接口继承。
1.原型链继承
JavaScript中对象的 prototype 属性一个很重要的方面就是用于实现 类型的继承。所有引用类型的对象默认都继承了 Object 对象的 prototype ,因而都有 toString () valueOf () 等方法。 原型链继承的模式如下:
// 父类 function SuperType () { this . property = true ; } SuperType . prototype . getSuperValue = function () { return this . property ; }; // 子类 function SubType () { this . subproperty = false ; } // 使 SubType 从 SuperType 继承 // 这是原型链继承的关键 SubType . prototype = new SuperType (); // 在子类的原型上定义新的子类的方法 SubType . prototype . getSubValue = function () { return this . subproperty ; }; var instance = new SubType (); alert ( instance . getSuperValue ()); //true 有两种方法用于判定实例与类型的关系,例如,对于上面的代码:
//使用 instanceof 运算符 alert ( instance instanceof Object ); //true alert ( instance instanceof SuperType ); //true alert ( instance instanceof SubType ); //true //使用类型prototype属性的isPrototypeOf方法 alert ( Object . prototype . isPrototypeOf ( instance )); //true alert ( SuperType . prototype . isPrototypeOf ( instance )); //true alert ( SubType . prototype . isPrototypeOf ( instance )); //true 如果子类要重写父类的方法,可以在将父类的实例赋值给子类的 原型后,在子类的原型上定义相同名称的方法,如:
// ... // 实现继承 SubType . prototype = new SuperType (); //重写继承的方法 SubType . prototype . getSuperValue = function () { return false ; }; var instance = new SubType (); alert ( instance . getSuperValue ()); //false 需要注意的是,在原型链继承模式中,不可以在原型 ( prototype ) 用对象字面表示 ( object literal ) 方式定义方法,这样会覆写已继承的 原型链。如:
function SuperType () { this . property = true ; } SuperType . prototype . getSuperValue = function () { return this . property ; }; function SubType () { this . subproperty = false ; } //实现继承 SubType . prototype = new SuperType (); //试图用对象字面表示方式在原型上定义方法, //这会覆写已经继承的原型链,即上一行的代码所继承的原型链 SubType . prototype = { getSubValue : function () { return this . subproperty ; }, someOtherMethod : function () { return false ; } }; var instance = new SubType (); alert ( instance . getSuperValue ()); // 错误! 原型链继承模式的问题 因为原型链继承模式是通过将父类的实例赋予子类,因而具有与原型 模式所产生的对象的同样的问题。如果父类中的某个属性是引用类型,则 子类的所有实例的这个属性的值都相同,修改子类一个实例的该属性的值 将会同时改变其他实例该属性的值。如:
// 父类 function SuperType () { this . colors = [ "red" , "blue" , "green" ]; } // 子类 function SubType () { } //从父类继承 SubType . prototype = new SuperType (); var instance1 = new SubType (); instance1 . colors . push ( "black" ); alert ( instance1 . colors ); //"red,blue,green,black" var instance2 = new SubType (); alert ( instance2 . colors ); //"red,blue,green,black" 上例中,子类 SubType 从父类 SuperType 继承了一个数组类型 ( 引用类型 ) 的属性 colors。在向 SubType 的实例 instance1 colors 属性中添加一个新项 black 后,SubType 的实例 instance2 中的 colors 属性也自动地拥有这个新项。这在很多情况下是不希望 看到的。 原型链继承模式的另一个问题是,当产生子类的新实例时,无法 向父类的构造函数传递参数。 由于以上两个原因,在实际中,很少单纯地使用原型链模式进行 继承。
2.构造函数窃取(Constructor Stealing) 模式继承
可以使用构造函数窃取继承模式解决原型 ( prototypes ) 上引用值 的继承问题,其基本思想是:在子类的构造函数内部调用父类的 apply () call () 方法并传递 this 参数执行父类的构造函数,这 样,每个子类的实例从父类中继承的引用属性都是各自独立的,不受 其它实例的影响。如:
function SuperType () { this . colors = [ "red" , "blue" , "green" ]; } function SubType () { //从父类 SuperType 继承 SuperType . call ( this ); } var instance1 = new SubType (); instance1 . colors . push ( "black" ); alert ( instance1 . colors ); //"red,blue,green,black" var instance2 = new SubType (); alert ( instance2 . colors ); //"red,blue,green" 传递参数 构造函数窃取继承模式可以在子类的构造函数内向父类的构造函 数传递参数,这正好克服了原型链继承模式的相应缺点。例如:
function SuperType ( name ) { this . name = name ; } function SubType () { //从父类 SuperType 继承并传递参数 SuperType . call ( this , "Nicholas" ); //实例属性 this . age = 29 ; } var instance = new SubType (); alert ( instance . name ); //"Nicholas"; alert ( instance . age ); //29 构造函数继承模式的问题 构造函数继承模式的问题具有与使用构造函数模式产生自定义类型 相似的问题:由于方法定义在构造函数内,不能进行功能重用。而且, 定义在父类原型上的方法不能被子类访问。因此,实际中,很少纯粹使 用构造函数窃取继承模式。
3.混合继承(Combination inheritance )
这种继承方式综合了原型链 ( Prototype Chaining ) 和构造函数窃取 ( Constructor Stealing ) 各自的优点,并克服了相应的缺点。其基本思 想是:使用原型链继承在原型上的成员,使用构造函数窃取继承实例 属性。这允许重用定义在原型上的功能并允许各个实例拥有自己的属 性。这是JavaScript中最常用的继承方式,并能通过 instanceof isPrototypeOf () 区分对象的类型和组成。例如:
function SuperType ( name ) { this . name = name ; this . colors = [ "red" , "blue" , "green" ]; } SuperType . prototype . sayName = function () { alert ( this . name ); }; function SubType ( name , age ) { SuperType . call ( this , name ); //第一次调用SuperType this . age = age ; } SubType . prototype = new SuperType (); //第二次调用SuperType SubType . prototype . constructor = SubType ; SubType . prototype . sayAge = function () { alert ( this . age ); }; var instance1 = new SubType ( "Nicholas" , 29 ); instance1 . colors . push ( "black" ); alert ( instance1 . colors ); //"red,blue,green,black" instance1 . sayName (); //"Nicholas"; instance1 . sayAge (); //29 alert ( instance1 instanceof SuperType ); //true alert ( instance1 instanceof SubType ); //true //alert(instance1.constructor); var instance2 = new SubType ( "Greg" , 27 ); alert ( instance2 . colors ); //"red,blue,green" instance2 . sayName (); //"Greg"; instance2 . sayAge (); //27 //alert(instance2 instanceof SuperType); //alert(instance2.constructor); 上例中,SuperType的原型成员可以在SuperType函数内部定义,但 SubType的原型和原型成员却不可以在SubType内定义。如果要在SubType 内定义原型和原型上的成员,在instance2之前必须要有一个实例 ( 例如 instance1 ) ,instance2的所有成员以及继承成员都可以访问,但instance1 原型成员以及继承的原型成员都不可以访问。 混合模式因为调用两次SuperType,所以效率不是很高。
4.寄生混合继承
寄生混合继承使用构造函数窃取继承属性,使用原型链混合形式继承 方法。实质上是,使用寄生继承模式继承父类的原型然后将结果分配给 子类的原型。例如:
function object ( o ) { function F () { } F . prototype = o ; return new F (); } function inheritPrototype ( subType , superType ) { var prototype = object ( superType . prototype ); //create object prototype . constructor = subType ; //augment object subType . prototype = prototype ; //assign object } function SuperType ( name ) { this . name = name ; this . colors = [ "red" , "blue" , "green" ]; } SuperType . prototype . sayName = function () { alert ( this . name ); }; function SubType ( name , age ) { SuperType . call ( this , name ); this . age = age ; } inheritPrototype ( SubType , SuperType ); SubType . prototype . sayAge = function () { alert ( this . age ); }; var instance1 = new SubType ( "Nicholas" , 29 ); instance1 . colors . push ( "black" ); alert ( instance1 . colors ); //"red,blue,green,black" instance1 . sayName (); //"Nicholas"; instance1 . sayAge (); //29 alert ( instance1 instanceof SuperType ); var instance2 = new SubType ( "Greg" , 27 ); alert ( instance2 . colors ); //"red,blue,green" instance2 . sayName (); //"Greg"; instance2 . sayAge (); //27 寄生混合继承比混合继承效率高,原型链也保持了完整,而且, instanceof isPrototypeOf () 也能很好地工作,因而是一种最佳 的引用类型继承模式。其缺点是,代码组织不方便。

转载于:https://www.cnblogs.com/shn11160/archive/2012/12/31/2840865.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值