1. 面向对象设计
1.1 理解对象
1.1.1 属性类型
(1). 数据属性:相当于对象的字段,包含一个数据值的位置,在这个位置可以读取和写入值。数据属性中有4个描述其行为的特性:
l [[Configurable]]:表示能否通过delete删除属性从而重新定义属性
l [[Enumerable]]:表示是否通过for-in循环返回属性
l [[Writable]]:表示能否修改属性的值
l [[Value]]:包含这个属性的数据值
要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。
(2). 访问器属性:相当于对象的属性,访问器属性不包含数据值,它们包含一对儿getter和setter函数,访问器属性有如下4个属性:
l [[Configurable]]:表示能否通过delete删除属性从而重新定义属性
l [[Enumerable]]:表示是否通过for-in循环返回属性
l [[Get]]:在读取属性时调用的函数
l [[Set]]:在写入属性时调用的函数
访问器属性不能直接定义,只能使用Object.defineProperty()来定义
1.1.2 定义多个属性
由于对象定义多个属性的可能性很大,ECMAScript5定义了一个Object.defineProperties()方法,利用该方法可以通过描述符一次性定义多个属性
1.1.3 读取属性的特性
使用ECMAScript5的Object.getOwnPropertyDescriptor()的方法,可以取得给定属性的描述符。如果是数据属性,这个对象的属性有configurable,enumerable,writable和value;如果是访问器属性,这个对象的属性有configurable,enumerable,get和set;
1.2 创建对象
1.2.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("chuck", 29, "softer"); |
工厂模式虽然解决了创建了多个相似对象的问题,但却没有解决对象识别的问题 |
1.2.2 构造函数模式
可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); } }
var person2 = new Person("chuck",27,"tester");
alert(person2.constructor == person) //true alert(person2 instanceof Person) //true |
要创建Person的新实例,必须使用new操作符,以这种方式调用构造函数会经历以下4个步骤:
(1). 创建一个新对象
(2). 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
(3). 执行构造函数中的代码(为这个新对象添加属性)
(4). 返回新对象
对象的constructor属性最初是用来标识对象类型的,使用构造函数的问题就是每个方法都要在每个实例上面实现一遍。
1.2.3 原型模式
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
1.2.3.1 理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype的属性,这个函数指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。Person.prototype.constructor 指向Person.
1.2.3.2 原型与in操作符
有两种方式使用in操作符,单独使用和for-in循环使用
单独使用,in操作符会在通过对对象能够访问指定属性时返回true,无论属性存在实例还是原型中
‘name’ in person1
for-in循环使用,返回的是所有能够通过对象访问的、可枚举的属性,无论属性存在实例还是原型中。
1.2.3.3 更简单的原型语法
针对每次添加原型方法和属性都要写一遍Person.prototype,更常见的做法是直接给原型对象赋值一个对象:
function Person() {
}
Person.prototype = {
name: "chuck", sayName: function() { alert(this.name); } } |
上诉代码存在一个弊端,constructor属性不再指向Person,可以收到赋值给constructor
function Person() {
}
Person.prototype = { constructor:Person, name: "chuck", sayName: function() { alert(this.name); } } |
1.2.3.4 原型的动态性
原型中查找值是一次搜索,因此我们对原型对象做的任何修改能够立即从实例中反映出来
function Person() {
}
var friend = new Person();
Person.prototype.sayName=function(){ alert("chuck"); }
friend.sayName(); // chuck |
但是对于重写原型对象的赋值,会切断现有原型与已经存在的对象实例之间的联系
function Person() {
}
var friend = new Person();
Person.prototype = { name: "chuck", sayName: function() { alert(this.name); } }
friend.sayName(); // error |
1.2.3.5 原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。
1.2.3.6 原型对象的问题
原型模式的最大缺点就是所有的原型对象的属性是被所有实例共享的,修改某一实例的属性会影响到其他实例的属性值。
1.2.4 组合模式
创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例类型,而原型模式用于定义方法和属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对对象的引用,最大限度的节省了内存。这是最常用的创建对象的方式。例如:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; }
Person.prototype = { constructor: Person, sayName: function() { alert(this.name); } }
var person1 = createPerson("chuck", 29, "softer"); var person2 = new Person("chuck", 27, "tester");
alert(person1.name == person2.name) //false alert(person1.sayName == person2.sayName) //true |
1.2.5 动态原型模式
动态原型模式把所有的信息都封装在构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。
例如:
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);
} } } |
1.2.6 寄生构造函数模式
使用寄生构造函数的方式和工厂构造函数基本一致,只是调用的方式有所不同。构造函数在不返回值的情况下,默认会返回新构造函数,而通过在构造函数的末尾添加一个return语句,可以重写构造函数的返回值。返回的对象与构造函数或者与构造函数的原型属性之间没有关系。
function Person(name, age) {
var obj = new Object(); obj.name = name; obj.age = age;
return obj; }
var friend = new Person("chuck", 30); |
1.2.7 稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this对象。例如friend对象只有sayName方法可以使用。
function Person(name, age) {
var obj = new Object();
//可以在这里定义私有变量和类型
//添加方法 obj.sayName = function(){ alert(name) }
return obj; }
var friend = new Person("chuck", 30); friend.sayName(); |
1.3 继承
1.3.1 原型链
原型链为实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
1.3.1.1 别忘记默认的原型
所有引用对象都默认继承了Object,这个继承也是通过原型链实现的。
1.3.1.2 确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。
第一种方式是使用instanceof操作符,只要用这个操作符才测试实例与原型链中出现过的构造函数
alert(instance instanceof Obejct); //true alert(instance instanceof SuperType); //true alert(instance instanceof SubType); //true |
第二种方式是使用isPrototypeOf()方法。使用原型链中出现的原型来检测是否是该对象实例的原型
alert(Object.prototype.isPrototypeOf(instance)); //true alert(SuperType.prototype.isPrototypeOf(instance)); //true alert(SubType.prototype.isPrototypeOf(instance)); //true |
1.3.1.3 谨慎的定义方法
需要谨慎的给原型链定义方法,以免造成代码错误。
1.3.1.4 原型链问题
原型链的最主要问题是所有的原型链方法和属性都是共享的,针对某个实例的原型链属性的修改会影响到其他对象的属性。
1.3.2 借用构造函数
在解决原型中包含引用类型值所带来的过程中,开发人员使用一种叫做借用构造函数的技术。这种技术的基本思想相当简单,即在子构造函数中调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此可以使用apply()和call()函数在新创建的对象上执行构造函数
function SuperType(){ this.colors = ["red","blue"]; }
function SubType(){ SuperType.call(this); }
var ins1 = new SubType(); var ins2 = new SubType();
ins1.colors.push("black"); alert(ins1.colors); // red,blue,black alert(ins2.colors); //red,blur |
借用构造函数可以向超类构造函数中传递参数,但构造函数存在问题,就是构造函数中的函数无法共享,在每个实例中都单独存在。
1.3.3 组合继承
组合继承指的是将原型链和借用构造函数的技术组合在一起,思路的使用原型链实现对原型方法进行继承,而通过借用构造函数实现对实例属性的继承。
function SuperType(name) { this.name = name; this.colors = ["red", "blue"]; }
SuperType.prototype.sayName = function(){ alert(this.name); }
function SubType(name, age) { SuperType.call(this,name);
this.age = age; }
SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); } |
1.3.4 原型式继承
借助原型可以基于已有的对象创建新的实例,同时还不必因此创建自定义类型,在object内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的实例对象。
function object(o){ function F(){}; F.prototype = o; return new F(); } |
ECMAScript5通过新增Object.create()方法规划了原型式继承。这个方法接受两个参数,一个用作新对象原型的对象和(可选的)一个新对象定义额外属性的对象,第二个参数定义的属性会覆盖原型对象上同名属性。
1.3.5 寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象。
function object(o){ function F(){}; F.prototype = o; return new F(); }
function createAnother(original){ var clone = object(original); clone.sayHi = function(){ alert('hi'); } return clone; } |
1.3.6 寄生式组合继承
组合继承是JavaScript最常见的继承模式,不过,它也有自己的不足。组合继承方式的最大问题是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一种是在子类型构造函数内部,所谓寄生式组合继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非就是超类型原型的一个副本而已。
function SuperType(name) { this.name = name; this.colors = ["red", "blue"]; }
SuperType.prototype.sayName = function(){ alert(this.name); }
function SubType(name, age) { SuperType.call(this,name);
this.age = age; }
SubType.prototype = inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function(){ alert(this.age); }
var ins1 = new SubType(); var ins2 = new SubType();
ins1.colors.push("black"); alert(ins1.colors); // red,blue,black alert(ins2.colors); //red,blur
function object(o){ function F(){}; F.prototype = o; return new F(); }
function inheritPrototype(SubType,SuperType){ var prototype = object(SuperType.prototype); prototype.constructor = SubType; SubType.prototype = prototype; } |
这种方式不仅只调用了一次SuperType的构造函数,而且避免了在SubType.prototype上面创建不必要的属性。