跳至侧栏

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()也能很好地工作,因而是一种最佳 的引用类型继承模式。其缺点是,代码组织不方便。
posted @ 2012-12-31 17:55  JiayangShen  阅读(587)  评论(0编辑  收藏  举报
Top
推荐
收藏
关注
评论