JavaScript
六、面向对象的程序设计
6.1 理解对象
- 定义对象
- 构造方法
- 对象字面量
var person={
name: "Nicholas",
age:29,
job:"Software Engineer",
sayName:function(){
alert(this.name);
}
};
6.1.1 属性类型
- 数据属性
该类属性包含一个数据值的位置,可直接定义- 特性
描述这类属性的行为- [[Configurable]]
- 一旦更改为false,则无法再重新配置为true,且false下除writable以外的特性也是无法修改的
- [[Enumerable]]
- [[Writable]]
- [[Value]] 默认为undefined
- [[Configurable]]
- 通过构造函数以及对象字面量定义对象时,以上属性都为true
- Object.defineProperty()
- 对象
- 属性名
- 描述符对象
需要设置的数据属性的特性及设置值- 如果不给属性特性指定值,则属性特性默认为false
- 特性
var person={};
Object.defineProperty(person,"name",{
configurable:false,
value:"Nicholas"
});
- 访问器属性
- 这类属性包含一对getter和setter函数,且不能直接定义,必须调用Object.defineProperty()
- 属性特性
- [[Configurable]]
- [[Enumerable]]
- [[Get]] 默认为undefined
- [[Set]] 默认为undefined
//定义两个值属性以及一个访问器属性
var book={
_year:2004, //下划线标识给属性只能通过对象方法访问
editon:1
};
Object.defineProperty(book,"year",{
get:function(){
return this._year;
},
set:function(newValue){
if(newValue){
this._year=newValue;
this.edition+=newValue-2004;
}
}
});
6.1.2 定义多个属性
- Object.defineProperties()
通过描述符一次性定义多个属性- 对象
- 多个属性
- 包括值属性和访问器属性
- 对应的描述符,描述符中未指定的特性默认为false
var book={};
Object.defineProperties(book,{
_year:{
value:2004
},
edition:{
value:1
},
//访问器属性
year:{
get:function(){
return this._year;
},
set:function(newValue){
if(newValue>2004){
this._year=newValue;
this.edition+=newValue-2004;
}
}
}
});
6.1.3 读取属性的特性
- Object.getOwnPropertyDescriptor()
- 参数
- 对象
- 属性名称
- 返回值
- 一个对象,包含值属性的四个特性,configurable、writable、enumerable、value(注意:都是小写) 或访问器属性对应的特性
- 参数
var descriptor=Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.configurable);//false,未指定的默认为false
6.2 创建对象
Object构造函数或对象字面量都可以构造单个对象,但创建多个同类型的对象,会产生大量重复代码。例如,构造函数需要多次new,然后重复对属性赋值;对象字面量需要重复设置属性。
6.2.1 工厂模式
定义函数,函数根据接收的参数创建包含必需信息的对象并将对象返回。函数可重复调用。
- 特定
- 通过函数封装以特定接口创建对象的细节
- 缺点
- 创建的对象是object,无法定义和识别具体类型
function Person(name, age, job){
var o = new Object();
o.name=name;
o.age=age;
o.job=job;
return o;
}
6.2.2 构造函数模式
要定义具体的类型,就需要自定义构造函数。
- 构造函数
- 构造函数也是普通函数,在定义方式上相同
- 构造函数首字母大写,以在名称上区分构造函数和普通函数
- 必须通过new操作符调用,才能起到构造函数的作用
- 内部会经历以下四个过程
- 创建新对象
- 将构造函数的作用域赋给新对象
- 执行构造函数中的代码为新对象添加属性
- 返回新对象
- 内部会经历以下四个过程
function Person(name, age, job){
this.name=name;
this.age=age;
this.job=job;
this.sayName=function sayName(){
alert(this.name);
}
}
var person1=new Person("Nicholas",29,"Software Engineer");
var person2=new Person("Greg",27,"Doctor");
- 可以进行类型识别(相对于工厂模式的优点),识别方式:
- 新对象的constructor属性
- A instanceof B
6.2.2.1 将构造函数当作函数
//当作构造函数使用
var person=new Person("Nicholas",29,"Software Engineer");
person.sayName();
//当作普通函数使用
Person("Greg",27,"Doctor");
window.sayName();//this指向函数的执行环境,即Global
//在另一个对象的作用域中调用
var o=new Object();
Person.call(o,"Kristen",25,"Nurse");
o.sayName();
6.2.2.2 构造函数的问题
- 如果在为方法属性赋值时定义函数对象,会使每个对象实例都创建一遍函数实例
- 没有必要创建多个完成相同任务的函数实例
- 在构造函数外部定义一个方法,所有由构造函数创建的对象实例都引用同一个方法
- 全局函数名不副实,只为对象调用
- 多个方法意味着多个全局变量
- 方法不具备封装性
6.2.3 原型模式
特点:利用原型对象,共享属性和方法
6.2.3.1 理解原型对象
- 原型
每创建一个函数,会自动为该函数创建一个prototype属性,该属性指向原型对象 - 原型对象
原型对象中的属性和方法由所有实例共享。- constructor
原型对象中默认存在constructor属性,该属性指向原型属性所在的函数- constructor属性继承自Object类型
- isPrototypeOf()
判断原型是否是某对象的原型
- constructor
- 实例
- 通过构造函数创建的实例,其内部将包含一个指针(内部属性,[[Prototype]],无法访问),指向构造函数的原型对象
- ECMAScript 5增加了一个方法访问 [[Prototype]] 的值,Object.getPrototypeOf()
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();
var person2=new Person();
person1.name="Greg";
alert(person1.name);//Greg,来自实例属性
alert(person2.name);//Nicholas,来自原型属性
delete person1.name;
alert(person1.name);//Nicholas,来自原型属性
- 属性搜索
- 先搜索实例属性
- 再搜索原型属性
- 不能通过实例重写原型中的值,对值的修改实际上是为实例对象添加实例属性,从而屏蔽原型属性
- delete操作符可以删除实例属性,从而解除屏蔽
- hasOwnProperty( propertyName )
Object的实例方法,判断对象是否具有某一实例属性
6.2.3.2 原型与in操作符
- in操作符
- 单独使用,判断对象是否具有某一属性,返回bool值
- propertyName in 对象
- 属性名用引号
- for-in循环中使用,返回所有能够通过对象访问的、可枚举的属性,包括实例属性和原型属性。
- for ( var prop in 对象)
- 自定义属性都是可枚举的
- 单独使用,判断对象是否具有某一属性,返回bool值
- Object.keys()
获取对象的所有可枚举的实例属性 - Object.getOwnPropertyNames()
获取所有实例属性,无论是否可枚举
6.2.3.3 更简单的原型语法
- 可用对象字面量形式创建并设置原型对象
- 对象字面量是新建Object类型
- 本质上是重写默认原型对象
- 所以该方式创建的原型对象的constructor属性不再指向原型属性所在的构造函数,而是指向Object的构造函数(constructor继承自Object,保存构造函数)
- 可以再显式设置constructor属性为某一特定对象。
function Person(){}
Person.prototype={
name:"Nicholas",
age:29,
job:"Software Engineer",
sayName:function(){
alert(this.name);
}
}
6.2.3.4 原型的动态性
- 可随时为原型添加属性和方法,并且修改能立即在所有对象实例中反映出来
6.2.3.5 原生对象的原型
- 原生的引用类型中有很多方法也是通过原型模式生成的,共享方法
6.2.3.6 原型对象的问题
- 缺点
- 方法可以共享,但属性共享可能就会有问题
- 当属性值是值类型,则不存在修改值,只是添加了实例属性并将原型属性覆盖(不能通过实例对象重写原型中的值)
- 当属性值是引用类型,则调用实例对原型属性进行修改时会导致原型属性发生变化,该变化会在所有该类型的实例对象上得到反映
function Person(){}
Person.prototype={
name:"Nicholas",
age:29,
job:"Software Engineer",
friends:["Shelby","Court"],
sayName:function(){
alert(this.name);
}
}
var person1=new Person();
var person2=new Person();
person1.friends.push("Van");
alert(person1.friends);//"Shelby,Court,Van"
alert(person2.friends);//"Shelby,Court,Van"
6.2.4 组合使用构造函数模式和原型模式
- 使用最广泛、认同度最高
- 缺陷
- 独立的构造函数与原型,不够封装
- 缺陷
- 实例属性通过构造函数模式进行定义
- 方法和共享属性通过原型模式进行定义
function Person(name, age, job){
this.name=name;
this.age=age;
this.job=job;
}
Person.prototype={
constructor:Person,
sayName:function sayName(){
alert(this.name);
}
}
6.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);
}
}
}
- 原型属性和实例属性都封装在构造函数中
- 只有在第一次调用构造函数时才对原型属性进行设置,而不是每次都为原型属性设置一个新的对象
6.2.6 寄生构造函数模式
与工厂模式类似,在函数内部创建对象并返回,只是是通过new操作符调用函数。
- 适用于为某个原生类型添加额外方法
- instanceof返回的是内部创建的对象的类型
function SpecialArray(){
var values=new Array();
values.push.apply(values,arguments);
values.toPipedString=function(){
return this.join("|");
};
return values;
}
var colors=new SpecialArray("red","green","blue");
alert(colors.toPipedString());//"red|green|blue"
6.2.7 稳妥构造函数模式
- 稳妥对象
没有公共属性,而且方法也不引用this的对象 - 稳妥构造函数
- 与寄生构造函数、工厂模式类似,在内部创建对象并返回
- 但只能在构造函数内部访问构造函数的参数
- 只能通过方法属性将函数参数暴露出来
- instanceof返回的是内部创建的对象的类型
function Person(name,age,job){
var o=new Object();
//o对象并不具有name属性
o.sayName=function(){
//先访问对象作用域,再访问Person执行环境中的name
alert(name);
}
return o;
}
var person=Person("Nicholas",18,"Software Engineer");
alert(person.age);//undefined
6.3 继承
- ECMAScript没有函数签名,所以不存在接口继承(只继承方法签名)
- ECMAScript只支持实现继承(继承实际方法)
6.3.1 原型链
- 利用原型指向另一个类型的实例(该实例作为派生类的原型对象),实现继承(另一个类型的所有属性和方法)
- 所有派生类的实例对象的原型都指向同一个原型对象
//动态原型模式创建自定义类型
function SuperType(){
this.property=true;
}
SuperType.prototype.getSuperValue=function(){
return this.property;
};
//动态原型模式创建自定义类型
funciton SubType(){
this.subproperty=false;
}
//原型指向另一个类型的实例对象
SubType.prototype=new SuperType();
SubType.prototype.getSubValue=function(){
return this.subproperty;
};
var instance=new SubType();
alert(instance.getSuperValue());
6.3.1.1 别忘记默认的原型
- 所有引用类型的默认原型都是Object的实例
6.3.1.2 确定原型和实例的关系
- instanceof操作符
- 在原型链中出现过的构造函数都返回true
- isPrototypeOf()
- 在原型链中出现过的原型都返回true
6.3.1.3 谨慎地定义方法
- 在重新指定原型对象后再添加方法
6.3.1.3 原型链的问题
- 缺点
- 基类中的属性值为引用类型时,对该属性值进行修改,会在所有派生类的实例中反映
- 无法自定义初始化基类
- 所有子类实例用的是同一份父类实例
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数
6.3.2 借用构造函数
function SuperType(){
this.colors=["red","blue","green"];
}
function SubType(){
//1.将SuperType的作用域设置为SubType的作用域(this)
//this,待执行才能明确作用域
//2.调用SuperType函数
SuperType.call(this);
}
//相当于
/*
*function SubType(){
* this.colors=["red,"blue","green"];
*}
*/
- 在派生类中调用基类的构造函数,并将基类的构造函数的执行环境设置为派生类的this
- 在派生类被调用时,才能确定this的具体指向
- 特点
- 伪继承父类的实例属性为子类的实例属性
6.3.2.1 传递参数
- 可以在子类型中向超类型构造函数传递参数
6.3.2.2 借用构造函数的问题
- 缺点
- 类似于构造函数模式
- 虽然属性是实例属性,但方法也成了实例方法
- 派生类和基类的原型之间没有关系
6.3.3 组合继承
将原型链和借用构造函数组合使用
//组合模式定义类型
function SuperType(name){
this.name=name;
this.colors=["red","blue","green"];
}
SuperType.prototype.sayName=function(){
alert(this.name);
};
//借用构造函数实现实例属性继承
funciton SubType(name,age){
SuperType.call(this,name);
this.age=age;
}
//原型链模式实现方法继承
SubType.prototype=new SuperType();
SubType.prototype.sayAge=function(){
alert(this.age);
};
- 最常用
- 利用原型链继承原型方法
- 实际上基类的实例属性也在此时继承为原型属性
- 利用借用构造函数的方式继承实例属性
- 在创建实例对象时,借助基类的构造函数的同时创建了自身的实例属性,从而屏蔽了原型属性
* 缺点
* 两次调用基类的构造函数
* 第一次调用是为派生类指定原型对象时
* 第二次调用是借助构造函数方式中调用,是派生类的实例属性,屏蔽了原型链中继承的属性
6.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);
- Object.create()
规范化了原型式继承- 作为新对象的原型的对象
- 为新对象定义额外属性的对象
var person={
name:"Nicholas",
friends:["Shelby","Court","Van"]
};
var anotherPerson=Object.create(person,{
name:{
value:"Greg"
}
);
alert(anotherPerson.name);//Greg
6.3.5 寄生式继承
类似于利用寄生构造模式、工厂模式创建对象
- 寄生构造函数模式在原始对象的基础上进行增强
- 而这个原始对象可以是通过=原型式继承模式产生的
function createAnother(original){
var clone=object(original);
clone.sayHi=funciton(){
alert("hi");
};
return clone;
}
6.3.6 寄生组合式继承
- 引用类型最理想的继承范式
- 组合式继承的问题
- 两次调用基类的构造函数
- 其中有一次(指定原型对象)可以不需要调用
- 不调用就不需要继承基类的实例属性作为原型属性
- 不调用但能指定原型对象
- 将基类的原型对象作为基础,构造一个新的对象并适当增强
- 借助寄生式继承的概念
- 只继承基类的原型对象,即其中的方法
- 使该对象的原型指向基类的原型对象。
- 将基类的原型对象作为基础,构造一个新的对象并适当增强
- 其中有一次(指定原型对象)可以不需要调用
- 两次调用基类的构造函数
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);
};
//借助构造函数模式继承实例属性
funciton SubType(name,age){
SuperType.call(this,name);
this.age=age;
}
//改进,在指定原型对象的同时只继承原型方法
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge=function(){
alert(this.age);
};