注: ★ 表示重要程度
面向对象的三大特征:
-
封装:封装的优势在于定义只可以在类内部进行对属性的操作,外部无法对这些属性指手画脚,要想修改,也只能通过你定义的封装方法;
-
继承:继承减少了代码的冗余,省略了很多重复代码,开发者可以从父类底层定义所有子类必须有的属性和方法,以达到耦合的目的;
-
多态:多态实现了方法的个性化,不同的子类根据具体状况可以实现不同的方法,光有父类定义的方法不够灵活,遇见特殊状况就捉襟见肘了
1. 理解对象 ★★★★★
1.1 属性类型
ES5中定义了只有内部才用的特性(attribute),描述了属性(property)的各种特征,不能直接访问,为了表示特性是内部值,放在了两对方括号中,例如[[Enumerable]],ECMAScript中有两种属性:数据属性和访问器属性
1.1.1 数据属性
- [[Configurable]]: 表示能否通过delete删除属性从而重新定义属性,默认
true
。 - [[Enumerable]]: 表示能否用过for-in循环返回属性,默认
true
。 - [[Writable]]: 表示能否修改属性的值,默认
true
。 - [[Value]]: 包含这个属性的数据值,默认
undefined
若要修改属性默认的特性,必须使用ES5的**Object.defineProperty()**方法,接受三个参数,所在对象、属性名、描述符对象,描述符对象的属性必须是:Configurable、Enumerable、Writable、Value,默认是都是false
1.1.2 访问器属性
访问器属性不包含数据值:他们包含一对儿getter和setter函数(不过,这两个函数都不是必须的)。在读取访问器属性时,会调用getter函数,写入访问器属性时,会调用setter函数。访问器属性有如下4个特性:
- [[Configurable]]: 表示能否通过delete删除属性从而重新定义属性,默认
true
。 - [[Enumerable]]: 表示能否用过for-in循环返回属性,默认
true
。 - [[Get]]: 在读取属性时调用的函数,默认
undefined
。 - [[Set]]: 在写入属性时调用的函数,默认
undefined
。
访问器属性不能直接定义,必须使用**Object.defineProperty()**来定义。
访问器旧方法: __defineGetter, __defineSetter。
1.2 定义多个属性
利用 Object.defineProperies() 方法可以通过描述符一次定义多个属性,接受两个参数:要修改的对象、要添加或者修改的属性
1.3 读取属性的特性
使用ES5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。该方法有两个参数:所在对象和属性,返回值是一个对象,数据属性或者访问器属性
2. 创建对象
2.1 工厂模式 ★★★★★
function createPerson(name, age, obj) {
var o = new Object();
o.name = name;
o.age = age;
o.obj = obj;
o.sayName = function() {
alert(this.name)
};
return o
}
var person1 = new createPerson("person1", 25, 'obj1');
var person2 = new createPerson("person2", 26, 'obj2');
工厂模式的问题
工厂模式虽然解决了多个相似对象的问题,
但是没有解决对象识别的问题(如何知道一个对象的类型)。
2.2 构造函数模式 ★★★★★
function Person(name, age, obj) {
this.name = name;
this.age = age;
this.obj = obj;
this.sayName = function() {
alert(this.name)
};
}
var person1 = new Person("person1", 25, 'obj1');
var person2 = new Person("person2", 26, 'obj2');
构造函数与工厂函数的不同之处:
- 没有显示的创建对象;
- 直接将属性和方法付给了this对象;
- 没有return语句。
构造函数名必须使用大写字母开头,这个做法借鉴其它OO语言。
new 操作符做了什么?
- 创建一个新对象;
- 将构造函数的作用域给新对象(this指向该对象);
- 返回新对象。
person1和person2的constructor(构造函数)属性,指向Person;
可以使用instanceof操作符验证该实例对象是由谁创建的,这是构造函数胜过工厂模式的地方。
构造函数的问题
每个方法都要在每个实例上重新创建一遍,上面例子中: peron1.sayName == person2.sayName //false, 为了解决两个函数做同一件事的问题,引入了原型模式方案
2.3 原型模式 ★★★★★
2.3.1 理解原型对象
每创建一个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个对象(原型对象),这个对象的用途可以定义所有实例共享的属性和方法。
在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,指向该原型的构造函数。
Person.prototype.isPrototype(person1):判断该实例(person1)是否指定构造函数(Person)原型有关联,返回true/false;
Object.getPrototypeOf(peron1) == Person.prototype: ES5新增方法,返回对象的原型
person1.hasOwnproperty(‘name’): 检测一个属性存在实例中,还是原型中,只有存在实例中才会返回true
2.3.2 原型与in操作符
in 操作符能够访问指定属性时返回ture
,无论该属性存在实例中还是原型中。
alert(name in person) //true
可以与**hasOwnprooperty()**方法结合使用,判断属性是在实例上还是原型上
!Object.hasOwnprooperty(name) && (name in object)
Object.keys():返回对象上所有可枚举的实例属性;
Object.getOwnPropertyNames(): 获取所有属性,包含不可枚举属性,如constructor
2.3.3 更简单的原型语法
function Person() {};
Person.prototype.name = "yxf";
Person.prototype.age = 26;
Person.prototype.job = "cxy";
- 上面例子中每添加一个属性或者方法,都要重新敲一遍
Person.prototype
,为了减少不必要的输入,可以使用对象字面量来重写整个原型对象,如下所示。
Person.prototype = {
name: 'yxf',
age: 26,
job: 'cxy'
}
- 该方法会使原型对象中的constructor不再指向Person,尽管instanceof还能返回正确结果。
这个时候怎么办呢?
我们可以手动设置constructor的值,如下所示:
Person.prototype = {
constructor: Person,
name: 'yxf',
age: 26,
job: 'cxy'
}
- 这个时候又会出现一个问题,通过该方式会使constructor属性的 [[Enumberable]] (可枚举)特性被设置为true,该特性默认是不可枚举的。怎么办呢?
这个时候就需要用到Object.defineProperty(),如下所示:
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
2.3.4 原型的动态性
创建完实例var person 1 = new Person()
后,可以通过Person.prototype.sayHi = function(){}
来追加方法,不能通过字面量来修改原型,因为把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
2.3.5 原生对象的模型
所有原生引用类型(Object,Array,String…),都在构造函数的原型上定义了方法
2.3.6 原型对象的问题
原型模式的最大问题是由其共享的本性导致的;
原型中的所有属性都被实例共享,这种共享对于函数非常合适,对于基本值的属性也行,但是对于包含引用类型值的属性来说,就有大问题了。
在实例上创建一个同名属性,修改该属性会影响原型上的值,如下所示:
Person.prototype = {
constructor: Person,
name: 'yxf',
frinds: ["a", "b"],
sayName: function() {
alert(this.name)
},
sayFrinds: function() {
alert(this.frinds)
}
};
{
let person1 = new Person();
let person2 = new Person();
person1.frinds.push("c");
person1.sayFrinds(); //[a,b,c]
person2.sayFrinds(); //[a,b,c]
}
原型模式到此结束
2.4 组合使用构造函数模式和原型 ★★★★★
构造函数与原型混成的模式,是目前最广泛、认可度最高的一种创建自定义类型的方法,通过构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这样每个实例都有一份实例属性的副本,同时又共享着对方法的引用,最大限度节省了内存。
2.5 动态原型模式 ★
它把所有信息都装在了构造函数中,通过if语句判断,只有指定方法不存在的情况下,才会将他添加到原型中。
注意: 使用该模式时,不能使用对象字面量重写原型
2.6 寄生构造函数模式 ★
不多做介绍,不如留点脑细胞看其他的
2.7 稳妥构造函数模式 ★
不多做介绍,不如留点脑细胞看其他的
3. 继承
许多OO语言支持两种继承方式:接口继承和实现继承,接口继承只继承方法前面,而实现继承则继承实际的方法。因为函数没有签名,在ES中无法实现接口继承,只支持实现继承,而且实现继承主要是依靠原型链实现的
3.1 原型链 ★★★★★
基本思想💡:利用一个原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subProperty = false
}
// 继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subProperty
}
var instance = new SubType();
alert(instance.getSuperValue()) //true
instance.getSuperValue()经历三个步骤:
- 搜索实例;
- 搜索SubType.prototype;
- 搜索SuperType.prototype。
最终结果:instance指向SubType的原型,SubType的原型有指向SuperType的原型,
注意一点,现在instance.constructor指向哪里了?不是SubType,而是SuperType,因为SubType.prototype中的constructor被重写了的缘故。
3.1.1 别忘记默认的原型(Object)
下面展示了完整的原型链:
3.1.2 确定原型和实例的关系
alert(instance instanceof Object); //true
alert(instance instanceof SubType); //true
alert(instance instanceof SuperType); //true
alert(Object.prototype.isPrototypeOf(instance)) //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
3.1.3 谨慎的定义方法
- 子类型若需要添加或者修改超类型的某个方法,一定要放在替换原型语句之后,正确写法如下所示:
- 通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链,会切断原型链,错误写法如下所示:
3.1.4 原型链的问题
- 共享问题
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
所以在实践中很少会单独使用原型链
3.2 借用构造函数 ★★★★★
基本思想💡:在子类型构造函数的内部调用超类型的构造函数。
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"
通过call()或者apply(),是SubType的每个实例都有自己colors属性的副本。
- 可以传递参数
function SubType(){
//继承了 SuperType,同时还传递了参数
SuperType.call(this, "Nicholas");
//实例属性
this.age = 29;
}
- 借用构造函数的问题
此模式只能借用构造函数不能借用原型中定义方法,
由于这些问题,借用构造函数的技术也是很少单独使用的。
3.3 组合继承 ★★★★★
基本思想💡:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
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.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
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
3.4 原型式继承
借助原型可以基于已有的对象创建新对象
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
// var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
// var yetAnotherPerson = Object.create(
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
3.5 寄生式继承
寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
3.6 寄生组合式继承
4. 总结 ★★★★★
4.1创建对象
- 工厂模式
使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。 - 构造函数模式
可以创建自定义引用类型,可以像创建内置对象实例一样使用new
操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。 - 原型模式
使用构造函数的 prototype 属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。
4.2 继承
- 组合继承
使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。 - 原型式继承
可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅
复制。而复制得到的副本还可以得到进一步改造 - 寄生式继承
与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强
对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问
题,可以将这个模式与组合继承一起使用。 - 寄生组合式继承
集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。
参考:
《JavaScript高级程序设计》(第三版)