JS面向对象
对象是无序属性的集合,其属性可以包含值、对象或者函数。每个对象都是基于一个引用类型创建的。
一、对象的属性
对象的属性在创建时都带有一些特征值,JS通过这些特征值来定义它们的行为。
1. 属性类型
JS在定义只有内部采用的特性(attribute)时,描述了属性(property)的各种特征。这些特性是为了实现JavaScript引擎用的,因此在JS中不能直接访问它们。为了表示特性是内部值,该规范把它们放在了两对儿方括号中,例如[[Enumerable]]。JS中有两种属性:数据属性和访问器属性。
@1. 数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性:
i. [[Configurable]]:表示是否能通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问其属性。默认值为true。
ii. [[Enumerable]]:表示能否通过for-in循环返回属性。默认值为true。
iii. [[Writable]]:表示能否修改属性的值。默认值为true。
iiii. [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为undefined。
要修改属性默认的特性,必须使用Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符对象的属性必须是:configurable、enumerable、writable和value。设置其中的一个或多个值,可以修改对应的特性值。
在调用Object.defineProperty()方法创建一个新的属性时,如果不指定,configurable、enumerable、writable特性的默认值都是false。
@2. 访问器属性
访问器属性不包含数据值,它们是一对getter和setter函数。getter、setter函数分别在读取、写入访问器属性时调用。访问器属性有如下4个特性:
i. [[Configurable]]:表示是否能通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问其属性。默认值为true。
ii. [[Enumerable]]:表示能否通过for-in循环返回属性。默认值为true。
iii. [[Get]]:在读取属性时调用的函数。默认值为undefined。
iiii. [[Set]]:在写入属性时调用的函数。默认值为undefined。
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
2. 定义多个属性
Obje.defineProperties()方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性和第一个对象要添加或修改的属性一一对应。
3. 读取属性的特性
Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个描述符对象。
二、创建对象
用Object构造函数或对象字面量方式创造多个相似对象会产生大量重复代码,因此产生了以下几种模式:
1. 工厂模式
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");
此模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎么样知道一个对象的类型)。
2. 构造函数模式
function Person(name, age, job){ this.name = name; 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");
在这个例子中,Person()函数取代了createPerson()函数。另外,除了相同部分外,还存在以下不同之处:没有显式地创建对象;直接将属性和方法赋给了this对象;没有return语句。
按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数应该以一个小写字母开头。
要创建Person的新实例,必须使用new操作符。以这种方式调用函数实际上会经历以下4个步骤:#1创建一个新对象;#2将构造函数的作用域赋给新对象;#3执行构造函数中的代码;#4返回新对象
在前面例子的最后,person1和person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person。对象的constructor属性是用来标识对象类型的。
创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型。
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。
构造函数的问题:每个实例的共用方法都要在每个实例上重新创建一遍。因此,不同实例上的同名函数是不相等的。可以将函数定义转移到构造函数外部来解决:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName(){ alert(this.name); } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
但是新问题又出现了:在全局作用域中定义的函数实际上只能被某个对象调用,而且如果有多个方法就要定义很多全局函数,不过这些问题都可以通过使用原型模式来解决。
3. 原型模式
每创建一个构造函数,就会同时创建它的prototype对象(这个属性是一个指针,指向一个对象),这个对象会自动获得constructor属性,而这个对象包含可以有特定类型的所有实例共享的属性和方法。如下面的例子所示:
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"
Person()构造函数:prototype属性,指向Person原型对象;
Person Prototype原型对象:constructor属性,指向Person()构造函数;其他共享属性和方法;
person1实例:[[prototype]]指向原型对象,可通过isPrototypeOf()方法检验,Object.getPrototypeOf(实例)方法获取;
属性搜索顺序:实例本身——>原型对象,可通过hasOwnProperty()方法区分是实例属性还是原型属性。
in操作符判断是否存在某个属性。
更简单的原型语法:
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
这里使用的语法本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象类型了,如果真的需要用到constructor的值,可以如下设置:
function Person(){ } Person.prototype = { constructor : Person, name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
注意,以这种方式设置的constructor的[[Enumerable]]特性为true,而原生的constructor属性是不可枚举的。因此可以使用Object.defineProperty()方法定义constructor。
* 实例中的指针仅指向原型,而不指向构造函数。
所有原生的引用类型都是采用的原型模式,所以可以通过修改原生对象的原型来添加方法。(不推荐)
原型模式的最大问题是由于共享属性导致每个实例的引用类型属性无法分别设置。
4. 组合使用构造函数模式和原型模式
组合使用构造函数模式与原型模式,集两种模式之长。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。下面的例子重写了前面的例子:
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");
5. 动态原型模式
此模式是为了封装构造函数和原型,具体实现如下:
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); }; } }
非常完美。
6. 寄生构造函数模式
这个模式可以在特殊的情况下用来为对象创建构造函数,比如创建一个具有额外方法的特殊数组,且不能直接修改Array构造函数,则可以使用这个模式:
function SpecialArray(){ //create the array var values = new Array(); //add the values values.push.apply(values, arguments); //assign the method values.toPipedString = function(){ return this.join("|"); }; //return it return values; } var colors = new SpecialArray("red", "blue", "green"); alert(colors.toPipedString()); //"red|blue|green"
注意,返回的对象与构造函数或者与构造函数的原型属性之间没有关系(此模式类似于工厂模式),为此不能依赖instanceof操作符来确定对象类型。
7. 稳妥构造函数模式
这种模式与寄生构造函数模式有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。
三、继承
许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。JS没有方法签名,所以只支持实现继承。
1. 原型链
原型链作为实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法,具体指让原型对象等于另一个类型的实例,则此原型对象将包含一个指向另一个原型的指针,层层递进就构成了实例与原型的链条。基本模式如下:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //inherit from SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function (){ return this.subproperty; }; var instance = new SubType(); alert(instance.getSuperValue()); //true
关键点在于将SubType的原型替换成了SuperType的实例。最终结果就是:instance指向SubType的原型,SubType的原型又指向SuperType的原型。
属性查找按照原型链向上查找。
给原型添加方法的语句一定要放在替换原型的语句之后,且不能使用对象字面量创建原型方法,否则会重写原型链。
原型链继承的问题:无法单独修改某一个实例的引用类型属性。
2. 借用构造函数
基本思想是在子类型构造函数的内部调用超类型构造函数,实现如下:
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ //inherit from 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"
这种方式使得每个实例都会具有自己的属性副本,还可以在子类型构造函数中向超类型构造函数传递参数。
3. 组合继承
指将原型链继承和借用构造函数的技术组合到一起,其思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
4. 原型式继承
如果只想让一个对象与另一个对象保持相似,可以使用原型式继承。其基本思想是定义一个函数,在这个函数内部,创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。从本质上讲,这个函数对传入其中的对象执行了一次浅复制。示例如下:
function object(o){ function F(){} F.prototype = o; return new F(); } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
JS在新规范中增加了Object.create()方法。这个方法接受两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象(属性描述符)。
5. 寄生式继承
基本思路:#1通过调用函数创建一个新对象;#2以某种方式来增强这个对象;#3返回这个对象。实现代码如下:
function createAnother(original){ var clone = object(original); clone.sayHi = function(){ alert("hi"); }; return clone; }
6. 寄生组合式继承
组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一是在创建子类型原型的时候,另一次是在子类型构造函数内部。组合继承只是通过借用构造函数创建实例属性屏蔽掉了原型链继承中的引用类型无法独享的问题,实际上新对象的原型中仍有被继承对象的实例属性(无用,浪费空间)。
寄生式组合继承的思路是以复制超类型的原型,来重写子类型的原型(具体通过修改constructor、prototype属性实现),这样就可以代替第一次调用用超类型构造函数,从而使得子类型的原型中除去超类型的实例属性。具体代码实现如下:
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); };
非常完美。