JavaScript学习(四):面对对象的程序设计
理解对象
创建自定义对象的两个方法:创建实例添加属性或者以字面量的形式定义。
1.属性类型
数据属性:有以下四个属性描述其行为特性。
[[configurable]]: 能否delete,能否修改属性特性。默认ture(一旦设置为false就不能二次修改了)
[[Enumerable]]: 能否通过for-in循环返回属性。默认ture
[[Writable]]: 能否修改属性值。默认ture
[[Value]]: 属性的数据值。默认undefined
Object.defineProperty()方法 修改以上特性。
var person = {}; Object.defineProperty(person,'name',{ writable: false; value: 'Eric'; })
访问器属性:他们包含一对getter和setter函数,有以下四个属性描述其行为特性。
[[configurable]]: 能否delete,能否修改属性特性。默认ture
[[Enumerable]]: 能否通过for-in循环返回属性。默认ture
[[get]]: 在读取时调用的函数。默认undefined
[[set]]: 写入时调用的函数。默认undefined
Object.defineProperty()方法
var book = { _year : 2004, edition: 1 }; Object.defineProperty(book,'year',{ get: function() { return this._year+1; }, set: function(newValue) { if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } })
//这是使用访问器属性的常见方法,即设置一个属性会导致其它属性发生变化。
其对应的旧有方法是__defineGetter__() 和 __defineSetter()
2.定义多个属性
由于定义多个属性的方法的可能性很大,ECMAScript5有定义了新的方法Object.defineProperties()
Object.defineProperties(book,{
...
})
3.读取属性的特性
Object.getOwnPropertyDescriptor(): 接受两个参数,属性所在的对象和要读取其描述符的属性名称。
创建对象
1.工厂模式
用函数来封装以特定接口创建对象的细节。
function creatPerson(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); }; return o; } person1 = creatPerson('Eric',24,'engineer'); console.log(person1.constructor); //Object
没有解决对象识别问题。
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('Eric',24,'engineer'); console.log(person1.constructor); //Person console.log(person1 instanceof Person); //true
创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型。
将构造函数当作函数使用
Person('Eric',24,'engineer'); window.sayName(); var o = new Object(); Person.call(o,'Eric',24,'engineer'); o.sayName();
构造函数的问题
主要问题就是每个方法都要在实例上创建一遍,浪费空间。
3.原型模型
我们创建的每一个函数都有一个prototype属性,这个属性是一个指针,指向构造函数创建的实例的原型对象。如果构造函数中共享的方法和属性能放在这个原型对象中,那么我们就不需要在创建每一个实例的时候把那些公用的信息也都创建一遍。
function Person(name,age,job) { Person.prototype.name = name; //这里不能再用this指代,因为是构造函数有原型属性,而不是实例有原型属性。 Person.prototype.age = age; Person.prototype.job = job; Person.prototype.sayName = function() { alert(this.name); }; } var person1 = new Person('Eric',24,'engineer'); person1.sayName();
理解原型模型
创建自定义构造函数之后,原型对象默认只会取得constructor属性指向构造函数;其它方法则继承自Object。
在调用构造函数创建实例之后,实例将包含一个指针[[Prototype]]指向构造函数的原型对象。
通过isPrototypeOf()方法确认对象实例和原型之间是否存在这种关系。
通过Object.getPrototypeOf()取得一个对象的原型。
实例中的属性会屏蔽掉原型中的同名属性,只能通过delete操作符完全删除实例属性,才能回复圆形中属性的访问。
继承自Object的方法hasOwnProperty()可以检测给定属性是否存在与对象示例中。
原型与in操作符
通过对象能访问到给定属性时,In操作符会返回true。无论该属性存在于实例中还是原型中。
结合hasOwnProperty()可以确定属性存在的位置。
function hasPrototypeProperty(object,name) { return !object.hasOwnProperty(name) && (name in object); }
Object.keys()方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
Object.getOwnPropertyNames()会获得所有的实例属性。
更简单的原型语法
Person.prototype = { name:'Eric', age: 24, job: 'engineer', sayname: function() { console.log(this.name); } }
我们在这里使用的语法,本质上完全重写了默认的prototype对象,因此实例的constructor不再指向原先的构造函数,而是指向Object。所以尽管instanceof还能返回正确的结果,但是constructor已经无法确定对象的类型了。
如果constructor很重要,可以在重写prototype时设置 constructor:Person
但是这样设置会导致constructor属性的[[Enumerable]]特性设置为true,可枚举。
因此,如果你使用兼容ECMAScript5的JavaScript引擎,可以尝试用Object.defineProperty()来定义constructor。
Object.defineProperty(Person.prototype,'constructor',{ enumerable: false, value: Person });
原型的动态性
随时可以为原型添加属性方法,而且其修改能在所以有对象的实例中反映出来。但是在实例创建之后,如果是重写整个原型对象,则是切断了实例和原型对象之间的联系,就无法获取原型属性和方法了。
原生对象的原型
所有的原生对象的引用也都是采用原型的方式创建的。例如在Array.prototype中可以找到sort()方法,在String.prototype中可以找到subString()方法。
既然如此,我们也可以通过添加原生对象的原型的属性和方法来扩充原生对象的功能。
String.prototype.startWith = function(text) { return this.indexOf(text) == 0; };
但是不推荐这样做。
原型对象的问题
原型带来的问题首先就是所有的实例在默认情况下都要取得相同的属性值。
更大的问题是当属性是引用类型值的时候,每个实例取得的只是引用的指针,也就是说每个实例对引用类型值的修改都会在全局反映。
4.组合使用构造函数模式和原型模式
function Person(name,age,job) { this.name = name; this.age = age; this.job = job; this.friends = ['Cherry','Bob','Mark']; } Person.prototype = { sayname: function() { console.log(this.name); } } Object.defineProperty(Person.prototype,'constructor',{ enumerable: false, value: Person });
这是用来定义引用类型的一种默认模式。
5.动态原型模式
function Person(name,age,job) { this.name = name; this.age = age; this.job = job; this.friends = ['Cherry','Bob','Mark']; if (typeof this.sayName != 'function') { Person.prototype.sayName = function() { console.log(this.name); }; } }
不需要拆分构造函数和原型,所以对原型所做的修改能立即在实例中反映。其中if语句检查任何一个在初始化是应该存在原型中的属性和方法就可以,这样原型就避免反复创建。
注:在动态原型模型中,创建原型不能用字面量的形式。因为是实例先创建然后才执行初始化原型的代码。这样就是实例创建后重写了原型,切断了联系。
6.寄生构造函数模式
这个模式可以在特殊情况下用来为对象创建构造函数。比如我们想创建一个具有特殊方法的额外数组,由于不能直接修改Array构造函数,因此可以使用这个模式。
function SpecialArray() { var values = new Array(); values.push.apply(values,arguments); values.toPipedString = function() { return this.join('|'); }; return values; } var friends = new SpecialArray('Cherry','Bob','Mark'); friends.toPipedString(); //"Cherry|Bob|Mark"
注:返回的对象其实与构造函数和原型属性之间没有关系,和在函数外部创建的对象没有什么不同,因此instanceof不能用来确定此模式下的对象类型。
7.稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。
这种模式主要有两点不同,一是新创建的实例方法不引用this,而是不使用new操作符调用构造函数。
function Person(name,age,job) { var o = new Object(); o.sayName = function() { console.log(name); }; return o; } var person1 = Person('Eric',24,'engineer');
在这种模式下,除了调用sayName之外没有其他方法访问其数据成员,这就提供了一种安全性。
继承
1.原型链
每一个实例包含指向构造函数原型的指针,原型函数都有一个指向构造函数的指针。如果我们让原型函数等于另一个构造函数的实例呢?
别忘记默认的原型
所有的引用类型都继承了Object类型,这个继承也是通过原型链实现的,这也是所有自定义函数都会继承toString()等默认方法的根本原因。
确定原型与实例的关系
instanceof操作符 isPrototypeOf()方法
谨慎地定义方法
给原型添加方法的代码一定要放在替换原型的语句之后。
通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样就会重写原型链。
原型链的问题
主要问题是包含引用类型值的原型。
在创建子类型的实例是,很难向超类型的构造函数中传递参数。
2.借用构造函数
函数只不过是在特定环境中执行代码的对象,通过使用apply()和call()方法,在子类型构造函数内部调用超类型构造函数,从而实现经典继承。
传递参数
function SuperType(name) { this.sayName = function() { console.log(name); //参数的传递是在arguments属性中的,不能通过this拿到。 }; } function SubType(name) { SuperType.call(this,name); //即实现了继承,又传递了参数,解决了原型继承的缺点 } var instance = new SubType('Eric'); instance.sayName();
借用构造函数的问题
方法都在构造函数中定义,复用就无从谈起,超类型构造函数的原型对子类型也不可见。
3.组合继承
function SuperType(name) { this.name = name; this.colors = ['red','black','pink']; } SuperType.prototype.sayName = function() { console.log(this.name); //this是通过构造函数的实例环境和原型的联系找到 } function SubType(name,age) { SuperType.call(this,name); this.age = age; } SubType.prototype = new SuperType(); //这里实际上并不需要真的把子类型的原型作为超类型的实例,只要子类型的原型能获得超类型的原型的属性和方法就可以了 SubType.prototype.constructor = SubType; //是重写SubType.prototype.constructor,不是修改SuperType.prototype.constructor SubType.prototype.sayHi = function() { console.log('Hi'+'you are'+this.age); } var instance = new SubType('Eric',24); instance.sayName(); instance.sayHi();
最常用的继承模式。
4.原型式继承
基于已有的对象创建新对象,将已有的对象作为原型实现继承。
function object(o) { function F() {}; F.prototype = o; return new F(); //不执行new操作则返回的是构造函数而不是实例 }
以上操作相当于一次浅复制。
ECMAScript5通过新增Object.create()方法规范化了原型继承。
在传入一个参数的情况下,Object.create()与object()函数行为相同。
第二个参数同Object.defineProperties()方法的第二个参数格式相同,通过自己的描述符定义属性。
var instance = Object.creat(Person,{ name: { value: 'Eric' } });
5.寄生式继承
寄生式继承和寄生构造函数和工厂模式类型(先拿复制品,再添加自己的属性和方法,最后返回函数)。
function creatAnthor(original) { var clone = object(original); clone.sayHi = function () { console.log('Hi'); }; return clone;
}
6.寄生组合式继承
在组合式继承中,我们实际上调用了两次超类型构造函数,并且将超类型构造函数的属性和方法分别写在子类型的原型和子类型的实例中。
用寄生组合式继承就可以避免这个问题。
function object(o) { function F() {}; F.prototype = o; return new F(); } function inheritPrototype(SuperType,SubType) { var prototype = object(SuperType.prototype); prototype.constructor = SubType; //如果不设置,constructor指向SuperType SubType.prototype = prototype; } function SuperType(name) { this.name = name; this.colors = ['red','black','pink']; } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name,age) { SuperType.call(this,name); this.age = age; } inheritPrototype(SuperType,SubType); SubType.prototype.sayAge = function() { console.log(this.age); } var instance = new SubType('Eric',24); instance.sayName(); instance.sayAge();
避免创建不必要的、多余的属性,与此同时,最关键的是原型链还能保持不变。是最理性的继承范式。