《JavaScript》高级程序设计 Chapter 6 面向对象的程序设计
关键的一章。
- 理解ES实现面向对象的“类”及“对象”的模式的方法。go
- 理解对象的两种属性以及属性特性、以及相关的设置和获取的方法。go
- 创建对象。8种创建方法的总结。从原型模式初步了解原型链。go
- 继承对象。go
“类”及“对象”
- 一般OO中,类及对象的定义,以及二者关系:
-
类的概念 : 类是具有相同属性和服务的一组对象的集合。为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和服务两个主要部分。在面向对象的编程语言中,类是一个独立的程序单位,应该有一个类名并包括属性说明和服务说明两个主要部分。
-
对象的概念:对象是系统中用来描述客观事物的一个实体,是构成系统的一个基本单位。一个对象由一组属性和对这组属性进行操作的一组服务组成。从更抽象的角度来说,对象是问题域或实现域中某些事物的一个抽象,它反映该事物在系统中需要保存的信息和发挥的作用;它是一组属性和有权对这些属性进行操作的一组服务的封装体。客观世界是由对象和对象之间的联系组成的。
-
类与对象的关系就如模具和铸件的关系,类的实例化结果就是对象,而对一类对象的抽象就是类。类描述了一组有相同特性( 属性 ) 和相同行为 ( 方法 ) 的对象。
-
总而言之,类是对象的抽象,对象是类的实例
-
- ES中没有类的概念,通过原型链(实例->原型->原型的原型->...)实现。
理解对象(数据属性/访问器属性、属性特性、特性的设置方式、特性的获取方式)
- 对象有两种属性:数据属性和访问器属性,可以用内部属性([[...]])来描述属性的特性(ES5)。
- 数据属性(存储数据的值,可以进行相关的读取和修改),有如下特性:
- [[Configurable]]:可否用delete删除属性,可否修改属性,可否修改属性的其他特性,可否将属性类型改成访问器属性。用构造函数或者字面量创建的属性默认值为true。
- [[Enumerable]]:可否用for-in进行枚举。默认值同上。
- [[Writable]]:可否修改属性的值,默认值同上。
- [[Value]]:存储数据属性的值,一般为传入的属性值。
- 访问器属性:不包含数据,但是包含一对函数getter(), setter()。顾名思义,读取访问器属性的时候,调用getter函数返回有效值;写入访问器属性的时候,通过setter传入新值,并由这个函数来决定如何处理传入的数据。此属性有4个特性 :
- [[Configurable]]:可否用delete删除属性,可否修改属性,可否将访问器属性改成数据属性。
- [[Enumerable]]:可否用for-in进行遍历。
- [[Get]]:在读取的时候调用的函数,默认为undefined。
- [[Set]]:在设置的时候调用的函数,默认为undefined。
- 不可直接定义,需调用Object.defineProperty()。举例Object.defineProperty()的例子代码。
- getter和setter不一定同时需要,非严格模式下返回undefined,严格模式下报错。
- Object.defineProperty():修改属性的特性。3个参数:待处理对象、待处理属性、待处理属性的特性({名:值})
- 注意:一旦将[[Configurable]]设置为false,就无法修改回去。那其他许多特性也没办法修改(具体参照[[Configurable]]的定义)
- 如果调用Object.defineProperty()却不设置具体特性的值得话,将默认为false。
-
1 var book = { 2 _year: 2004, 3 edition: 1 4 }; 5 6 Object.defineProperty(book, "year", { 7 get: function(){ 8 return this._year; 9 }, 10 set: function(newValue){ 11 if (newValue >2004){ 12 this._year = newValue; 13 this.edition += newValue - 2004; 14 } 15 } 16 }); 17 //未说明的[[Configurable]]属性默认为false。 18 19 book.year = 2005; 20 alert(book.edition); // 2 21 /* 22 这里的_year表示year属性不能直接访问和设置,只能通过getter和setter处理。 23 也并不代表_year变成了访问器属性,实际上它还是数据属性。具体分析看本节中最后的代码的说明。 24 */
- _defineGetter()/_defineSetter():在不支持Object.defineProperty的浏览器中,可以使用_defineGetter_()或_defineSetter_()两个方法来定义getter函数和setter函数,只不过这个时候不能修改[[Configurable]]和[[Enumerable]]。
- Object.defineProperties() :定义多个属性的多个属性特性。2个参数:待处理对象,待处理对象的属性及其特性({属性:{特性1:xx, 特性2:xx},...})
- 经测试,与Object.defineProperty()同样,如果调用但是不设置具体特性值,则默认为false。
- Object.getOwnPropertyDescriptor() :获取属性的特性。2个参数:待处理对象,待获取的属性。返回一个对象,存储特性以及特性值。
- 属性特性的获取代码,注意区分和理解数据属性和访问器属性:
-
var book = {}; Object.defineProperties(book, { _year: { value : 2004 },//数据属性 edition: { value: 1 },//数据属性 year:{ get: function(){ return this._year; }, set: function(){ if(newValue > 2004){ this._year = newValue; this.edition += newValue - 2004; } }//访问器属性 } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); alert(descriptor.value);//2004 alert(descriptor.configurable);//false alert(typeof descriptor.get);//"undefined" var descriptor = Object.getOwnPropertyDescriptor(book, "year"); alert(descriptor.value); // "undefined" alert(descriptor.enumerable);//false alert(typeof descriptor.get); // "function" /* 1、year为访问器属性,_year为数据属性 2、_year的下划线表示不能直接访问和设置,与其是否是访问器属性无关 3、调用defineProperties()方法但不说明的[[Configurable]], [[enumerable]]等属性默认为false。 */
创建对象
- 归纳见下表(8种方式创建对象)
-
创建方法 创建代码 优点 缺点 通过构造函数或者字面量 //构造函数 var o = new Object(); o.name = "Nicholas"; o.age = 29; o.job = "Doctor"; o.sayName = function(){ alert(this.name) }; //字面量 var o ={ name : "Nicholas", age : 29, job: "Doctor", sayName: function(){ alert(this.name); } }
较为直观的创建单个对象。 用一个接口创建多个对象的时候会产生大量的重复代码。 工厂模式(用包装函数)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, "Teacher"); var person2 = createPerson("Greg", 27,"Doctor");
将创建对象的过程抽象,用函数封装以特定接口创建对象的细节,可以多次调用该工厂函数以创建多个相似的对象。 无法知道一个对象的具体类型(没有解决对象识别的问题)。这里指的是更加具体的类型,或者说属于哪些对象的实例。构造函数模式(自定义构造函数) //首字母大写或者大写驼峰以与一般函数区分 function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = funciton(){ alert(this.name) }; } var person1 = new Person("Nicholas", 29,"Teacher"); var person2 = new Person("Greg", 27, "Doctor");
解决了对象识别的问题:1、新对象的constructor属性指向构造函数person1.constructor == Person.2、instanceof操作符下:person1 instanceof Person;person1 instanceof Object;均返回true每创建一个对象实例(此处为Person实例),都会创建一个不同的sayName函数的实例(虽然同名,实际上却创建了不同的作用域和作用域链),并没有必要。然而若是将sayName函数的声明放在构造函数之外,并且在构造函数中通过this.sayName = sayName;来进行调用。虽然解决了创建多个实例函数的问题,但有新的问题:- 全局环境中定义的函数只在局部作用域使用。- 当对象需要的方法(函数)较多的时候,失去了封装性。原型模式(利用构造函数的prototype属性) function Person(){}; Person.prototype.name ="Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Doctor"; Person.prototype.sayName = function(){ alert(this.name) }; //prototype指向函数原型对象,是所有实例的原型 //以上这些属性和方法为所有实例共享 var person1 = new Person(); var person2 = new Person(); alert(person1.sayName == person2.sayName); //返回true。解决构造函数模式的缺点。 //或者更简单的写法 function Person(){}; Person.prototype = { constructor: Person //这里完全重写了prototype,所以constructor不会 //自动指向构造函数(虽然instanceof可以正确运 //用),可以手动添加。 name: "Nicholas", age: 29, job: "Doctor", sayName : function(){ alert(this.name);} }; Object.defineProperty(Person.prototype, "constructor",{ enumerable: false, value: Person }); //手动添加的constructor的[[enumerable]]默认为true。 //所以可以通过这种方式重新设置(ES5)
解决了构造函数模式的创建多个函数实例的问题,节省了空间、提高了效率。这种方式提供了完整的、比较合理的继承和覆盖机制。实际上还应用在原生对象的构造以及继承上。过于共享:1、会为所有属性和方法赋默认的初值。2、对于共享的基本类型属性,可以采用在实例中设置同名属性的方法对原型中的值进行覆盖。3、然而对于初衷并不是为了共享的引用类型的值(比如说数组等),一旦在一个实例上进行修改,很容易影响到所有实例的这个值。BIG PROBLEM。组合使用构造函数模式和原型模式(常用) 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, "Teacher"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); alert(person1.sayName === person2.sayName); //true。共享方法 alert(person1.friends === person2.friends); //false。各自的引用类型。
构造函数用以定义实例属性、原型模式用以产生共享方法和属性。同时还可以向构造函数传递必要的参数。动态原型模式(动态体现在:共享的属性或者方法如果存在,则不需要添加,不存在则添加。) 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 friend = new Person("Nicholas", 29, "Teacher");
将所有的信息都封装在构造函数中(包括原型对象的初始化)。方法是:判断原型对象的某些属性和方法是否存在,若不存在则会添加。注意:这里不能使用字面量来构造原型对象。就像上面说过的,那么做会完全重写原型对象,从而断开实例和原型对象之间的关系。这里对原型的修改,可以立刻反应在实例中。寄生构造函数 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, "Teacher"); //为Array添加属性和方法,但不想破坏Array function SpecialArray(){ var values = new Array(); values.push.apply(values, arguments); values.toPipedString = function(){ return this.join("|"); }; } var colors = new SpecialArray("red","blue","green"); alert(colors.toPipedString()); //"red|blue|green"
与工厂模式相比:- 使用new操作符- 把包装函数看成构造函数比较少应用,一般用于需要在原生类型基础上添加方法和属性的场合,如左边例子。构造函数返回的对象与构造函数的原型属性没有任何关系,无法使用instanceof确定对象类型。所以一般是在其他模式无法使用的时候才会使用。 稳妥构造函数模式(稳妥对象:没有公共属性,方法不引用this,适合在安全环境(不能用this和new的环境)中)function Person(name , age, job){ var o = new Object(); o.sayName = function(){ alert(name); }; return o; } //除了sayName没有其他方法可以访问name
与寄生构造函数类似,但- 实例方法不引用this- 不使用new调用构造函数即使可以给稳妥对象添加方法和数据成员,但是不可能直接访问构造函数中的原始数据。具有安全性。构造函数返回的对象与构造函数的原型属性没有任何关系,无法使用instanceof确定对象类型。 - 构造函数模式的说明
- 与工厂模式的区别:没有显示创建对象、直接将属性和方法赋予给this对象,没有return语句。实际上,这部分的工作交给了new操作符完成。
- new创建对象的时候经历了:创建新对象、将构造函数的作用域赋给这个新对象(this)、返回新对象。
- 构造函数完全可以当做一般函数使用。
- 原型模式的说明
- 构造函数通过prototype属性指向原型对象、原型对象通过constructor指向构造函数。则该构造函数的实例继承所有原型对象的方法和属性(包括constructor)。
- 除非如表格中为原型对象手动添加属性和方法,不然构造函数的原型对象默认只有constructor属性,以及从Object继承而来的其他方法和属性。实际上,这个原型对象是Object对象的实例。 (涉及到原型链。继承的问题)
- 实际上,以此构造的对象实例内部有[[Prototype]]属性指向构造函数的原型对象(而非构造函数)
-
- 如图,person1和person2不直接具备除了内部属性[[Prototype]]之外的属性和方法,但是可以通过(向上)查找对象的方法继承原型对象的属性和方法。
- 方法:
- isPrototype()/Object.getPrototypeOf(person1):内部属性[[Prototype]]不可以直接访问,所以通过这两个方法找寻实例的原型。
- hasOwnProperty()检测属性是否在实例中、还是原型中。当实例中存在的时候返回true
- Object.getOwnPropertyDescriptor()在ES5中只能找到实例的属性特性。想要找到原型的属性特性只能在原型上应用词函数。
- in操作符
- 单独使用:无论属性在原型还是实例都可以
- for-in:无论属性在原型还是实例都可以,但是若为不可枚举则无法得到。但是若实例中屏蔽(同名可枚举属性)了该不可枚举的属性,同样可以得到。
- ES5中的Object.keys():可以得到所有可枚举的实例属性(注意是实例的属性)。
- Object.getOwnPropertyNames():可以获得所有实例属性(无论可否枚举)
- 注意“覆盖和继承”:向上查找对象(实例)---注意此处只是“覆盖”(或者说屏蔽了上一级的值)而不能通过改变实例的属性和方法来改变原型的属性或者方法。通过delete可以删除实例中的属性,以解除“覆盖”
- 原型的动态性:修改原型会立即影响到实例(因为实际上通过指针链接)
- 整理二者关系:修改原型-->影响实例;修改实例--->覆盖原型。
- 如果直接重写原型(Person.prototype),那么实例中的内部属性[[prototype]]虽然仍然指向原来的原型,但和新的原型之间的关系(引用)就断开了。
继承对象
- 在讨论多层原型、实例的时候需要考虑对象的继承。以下为6中继承方式的总结和对比。
-
继承方法 代码 优点 缺点 原型链 function SuperType(){ this.colors = ["red", "blue", "green"]; //超类型的实例属性 } function SubType(){} SubType.prototype = new SuperType(); //colors属性变成这个原型对象的属性 var instance1 = new SubType(); var instance2 = new SubType(); instance1.colors.push("black"); alert(instance2.colors); //"red,blue,green,black" //对于instance1, instance2来说,colors是共享属性
是ES中实现“继承”的基础。 - 如果只是简单的一层继承(只有一层构造函数、原型对象和它的实例)是没多大问题的。但如果多层的话,超类型中的实例属性会成为子(孙)类型的原型属性,这个时候对于引用类型(数组等)的属性就会出现问题。- 创建子类型实例的时候不能像超类型构造函数传递参数。借用构造函数(伪造对象或经典继承):利用call()和apply()来保持实例属性和方法。 function SuperType(name){ this.colors = ["red", "blue", "green"]; this.name = name; } function SubType(){ SuperType.call(this,"Nicholas"); //继承了实例属性,并传递了参数 this.age = 29; //新的实例属性。 } var instance = new SubType();
解决了原型链的传递参数以及对于实例属性的继承的问题。 显然,没有考虑原型对象中的方法和属性。 组合继承(伪经典继承)= 原型链+借用构造函数(最常用)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; }//借用构造函数继承实例属性 SubType.prototype = new SuperType(); //原型链继承原型对象的方法 /* 两次调用构造函数—— 原理未变,超类构造函数的实例同样变成了 原型属性和方法。但是借用构造函数的继承 方式“覆盖”了原型对象中的同名方法和属性。 从而实现对实例方法和属性的性质的保留。 */ SubType.prototype.sayAge = function(){ alert(this.age); };
两次调用构造函数——原理未变,超类构造函数的实例同样变成了原型属性 和方法。但是借用构造函数的继承方式“覆盖”了原型对象中的同名方法和属性。从而实现对实例方法和属性的性质的保留。instanceof()和isPrototypeOf()可用。在左栏中提到的。其实无论在原生对象中的属性和方法,还是实例中的属性和方法同样存在,不过后者覆盖了前者,才不至于共享超类构造函数中的实例属性。由此带来的缺点:实际上,两次调用了构造函数。call()一次,new一次。--- 这个缺点由寄生组合式解决。原型式继承 //原理: function object(o){ function F(){} F.prototype = o; return new F(); } //以此直接创建传入对象o的实例。 var person ={ name : "Nicholas", friends: ["Sherry", "Court", "Van"] }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barrie"); alert(person.name); //"Nicholas" alert(yetAnotherPerson.name); //"Linda" alert(person.friends); //"Sherry, Court, Van, Rob, Barrie" //Object.create()对原型式继承进行规范 var anotherPerson = Object.create(person); //same as 上述例子 /* 第二个参数为额外添加的属性或方法的格式与 Object.defineProperties()的第二个参数的相 同(考虑描述符),以此方法指定的任何属性都 会覆盖原型对象的同名属性。 */
无严格意义上的构造函数。借助原型,基于已有的对象(原生对象)创建新对象,同时不必因此创建自定义的类型。- 要求有一个对象作为依据,以此创建(实际上相当于副本,但是可以对基本类型进行改变对属性方法进行扩展)- 只考虑“相似”的时候对于引用类型会“共享” 寄生式继承:(在原型式的基础上) function createAnother(original){ var clone = object(original); //原型式,创建一个“相似”对象 clone.sayHi = function(){ alert("hi") }; //增强这个对象。原型式是在构造后直接对实例改 //变的。 return clone; } var person = { name: "Nicholas", friends: ["Sherry", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi();//"hi"
主要考虑对象(的相似)而非自定义类型或者构造函数的情况下,选择这个模式。object()不是必须的,只要可以返回新对象的函数都可以。无法函数复用。与创建对象时提到的构造函数模式的缺点相似。 寄生组合式继承(理想方式)function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); prototype.constructor = subType; subType.prototype =prototype; } 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); };
inheritPrototype(SubType, SuperType);//本来就只是为了得到超类原型对象的副本//用这种方法代替原先的调用构造函数获取原型对象//的方法和属性的方式。 - 一般有两种实现继承的方式:接口继承和实现继承,前者继承方法签名,后者继承实际的方法。显然,ES的函数没有签名,所以只能用实现继承。
- 原型链:原型具有原型,原型是另一个原型的实例。通过利用重写原型来实现,如图,即SubType.prototype = new SuperType();
-
- 注意
- 观察书中相关代码,可以发现。构造函数SuperType()中的属性(不共享的),出现在了SuperType.prototype的实例SubType.prototype中,而SubType()中的属性,出现在SubType.prototype的实例instance中。因为这些是实例属性。而prototype中的属性和方法(共享)属于原型属性或方法,仍然存放在原型中。
- instance.constructor不再指向SubType(),因为其原型对象被重写(成另一个对象的实例),constructor属于继承的属性,所以随着SuperType.prototype指向SuperType()。注意到不需要考虑指向Object()的问题。
- 同一层的构造函数、原型对象和实例的关系:构造函数的prototype属性指向其原型对象;其原型对象的constructor属性指向构造函数(若constructor不是继承而来的时候,即只考虑原型链上只有一层的情况);而实例中的内部属性[[Prototype]]指向原型对象,若原型对象(用字面量的方式)完全重写,这这个指向就断开了。
- instanceof操作符、isPrototypeOf()方法。
- “覆盖”:在子类型中不仅重写原型属性会屏蔽超类型中的属性,重写方法也是。屏蔽的意思是,子类型中调用的是子类型中的值或者方法,超类型中调用的是超类型中原本的属性和方法。
- 再次注意,慎重使用字面量写对象,此时这个对象的原型默认为Object.prototype,可能会断开预想中的原型链。