面向对象的程序设计
面向对象的语言都有一个标志,那就是它们都有类的概念,但ECMAScript中没有类的概念,ECMA-262中把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数”。
每个对象都是基于引用类型创建的,引用类型可以是原生类型,也可以是开发者定义的类型。
理解对象
最简单的创建自定义对象的方法(创建一个object实例),然后再为它添加属性和方法。
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = Softeware Engineer";
person.sayName = function (){
alert(this.name);
}
上面例子,通过对象字面量的写法:
var person = {
name:"Nicholas",
age:29,
job:Software Engineer,
sayName:function(){
alert(this.name);
}
}
属性类型
在定义只有内部才用的特性时,描述了属性的各种特征。定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们。为了表示特性时内部值,该规范把他们放在了两对方括号中,例如[[Enumerable]]。
ECMAScript中有两种属性:数据属性和访问器属性。
1.数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。
- [[Configurable]]
- []
数据属性可以通过Object.defineProperty()方法修改属性的默认特性。
var person = {};
Object.defineProperty(person,"name",{
writable:false,
value:"Nicholas"
});
2.访问属性
包含一对getter和setter函数也需要Object.defineProperty()来定义。
定义多个属性
通过Object.defineProperties()方法定义多个属性。
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;
}
}
}
});
读取属性的特性
通过Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。
这个方法可以接收两个参数:属性所在的对象和要读取其描述符的属性名称。
返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get和set;如果是数据属性,这个对象的属性有configurable、enumerable、writable和value。
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;
}
}
}
});
var descriptor = Object.getOwnPerpertyDescriptor(book,"_year");
descriptor.value //2004
descriptor.configurable //false
typeof descriptor.get //"undefined"
var descriptor2 = Object.getOwnPerpertyDescriptor(book,"year");
descriptor2.value //undefined
descriptor2.enumerable //false
typeof descriptor.get //"function"
创建对象
工厂模式
工厂模式抽象了创建具体对象的过程。用函数封装以特定接口创建对象的细节。
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");
工厂模式虽然解决多个相似对象问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数模型
ECMAScript中构造函数可以用来创建特定类型的对象。像object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外可以创建自定义的构造函数,从而定义自定义对象的属性和方法。
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");
此处,两个对象都有一个constructor(构造函数)属性,该属性指向Person。
alert(person1.constructor == Person); //true
alert(person2.constructor == preson); //true
对象的constructor属性最初是用来标识对象的类型,还是instanceof操作符要更可靠些。我们在这个例子中创建的所有对象既是Object实例,同时也是Person的实例:
alert(person1 instanceof Object); //true
alert(person2 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Person); //true
创建自定义的构造函数意味着将来可以用它的实例标识为一种特定的类型
将构造函数当做函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;反之,跟普通函数没什么区别。
//当做构造函数使用
var person = new Person("Nicholas",29,"Software Engineer");
person.sayName(); //"Nicholas"
//作为普通函数调用
Person("Greg",27,"Doctor"); //添加到window
window.sayName(); //"Greg"
//另一个对象的作用域中调用
var o = new Object();
Person.call(o,"kristen",25,"Nurse");
o.sayName(); //"kristen"
构造函数的问题
每个方法都要在每个实例上重新创建一遍,所以,在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是一个Function实例。因为,ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化一个对象。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)");
}
以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建function新实例的机制仍然是相同的。不同实例上的同名函数是不相等的:
alert(person1.sayName == person2.sayName); //false
然而,创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面,因此,大可通过把函数定义转移到构造函数外部来解决这个问题。
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");
通过这种代码,person1和person2对象就共享了在全局作用域中定义的一个sayName()函数。这样确实解决了两个函数做同一件事的问题。
这样写法引发的新问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们自定义的引用类型就毫无封装性可言。
原型模式
每个函数都有prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
使用原型对象的好处是可以让所有对象实例共享它所含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到对象的原型中。
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"
alert(person1.sayName == person2.sayName); //true
理解原型对象
继承
许多OO(面向对象)语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。
由于函数没有签名,则只支持实现继承,而且主要依靠原型链实现。
原型链
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数、原型和实例的关系:
让原型对象等于另一个类型的实例=》再层层递进,构成了原型链。
实现原型链的基本模式:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.subprototype;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
下图就是上面代码中的逻辑关系:
上面例子中,调用instance.getSuperValue()会经历的三个搜索步骤:
1)搜索实例;
2)搜索SubType.prototype;
3)搜索SuperType.prototype
1.别忘记默认的原型
前面展示的例子中还少了一环。所有引用类型默认都继承了Obeject,而这个继承也是通过原型链实现的。
展现完整的原型链:
所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也真是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。
2.确定原型和实例的关系
第一种方式,使用instanceof操作符,用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
由于原型链的关系,我们可以说instance是Object、SuperType或SubType中任何一个类型的实例。
第二种方式,使用isPrototypeOf()方法。同样,因此,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
谨慎地定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但是不管怎样,都要在SuperType的实例替换原型之后,再定义这两个方法。
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.subprototype;
};
//重写超类型中的方法
SubType.prototype.getSuperValue = function(){
return false;
}
var instance = new SubType();
alert(instance.getSuperValue()); //false
还有一点需要注意,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType();
//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
getSubValue : function (){
return this.subproperty;
},
someOtherMethod : function (){
return false;
}
}
var instance = new SubType();
alert(instance.getSuperValue()); //error!
以上代码展示了刚刚吧SuperType的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断。
5.原型链的问题
最主要的问题来自包含引用类型值的原型。包含引用类型值的属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上回变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
function SuperType(){
this.color = ["red","blue","green"];
}
function SubType(){
}
SubType.prototype = new SuperType();
var instance1 = new SuperType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SuperType();
alert(instance2.colors); //"red,blue,green,black"
SubType的所有实例都会共享这一个colors属性。而我们队instance1.colors的修改能够通过instance2.colors反应出来。
原型链的第二个问题:在创建子类型的实例时们不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数中传递参数。
由于以上的问题,实践中很少会单独使用原型链。
借用构造函数
在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数。
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"
SuperType.call(this);
代码“借调”了超类型的构造函数。通过使用call()方法/apply()方法。我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了。
1.传递参数
对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数
function SuperType(name){
this.name = name;
}
function SubType(){
//继承了SuperType,同时还传递了参数;
SuperType.call(this,"Nicholas");
//实例属性
this.age = 29;
}
vatr instance = new SubType();
alert(instance.name); //"Nicholas"
alert(instance.age); //29
2.借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题–方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数。
组合继承
结合原型链和借用构造函数的技术,使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
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.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"
insance1.sayAge(); //29
var instance2 = new SubType("Greg",27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Grey"
instance2.sayAge(); //27
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。
原型式继承
函数表达式
定义函数的两种方式:函数声明和函数表达式。
函数声明:
function functionName(arg0,arg1,arg2){
//函数体
}
即(function关键字 函数名) 的方式,在某些浏览器中,给函数定义了一个非标准的name属性,通过这个属性可以访问到给函数指定的名字。
//只在Firefox、Safari、Chrome和Opera有效
alert(functionName.name); //"functionName"
函数声明的一个重要特征就是函数声明提升,意思是在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。
sayHi();
function sayHi(){
alert(“Hi!”);
}
函数表达式:
最常见的形式:
var functionName = function(arg0,arg1,arg2){
//函数体
}
这种形式看起来像是常规的变量赋值语句,即创建一个函数并将它赋值给变量functionName。这种函数叫做匿名函数(因为function关键字后面没有标识符,也叫拉姆达函数)。匿名函数的name属性是空字符串。
函数表达式与其他表达式一样,在使用前必须先赋值。
sayHi(); //错误:函数还不存在
var sayHi = function(){
alert("Hi!");
};
理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。
//不要这样做!
if(condition){
function sayHi(){
alert("Hi!");
}
} else {
function sayHi(){
alert("Yo!");
}
}
表面上可能此代码没有问题,但实际上,这在ECMAScript中属于无效语法,JS引擎会尝试修正错误,将其转换为合理的状态。但是浏览器修正错误的语法并不一致,大多数浏览器会返回第二个声明,忽略condition;有些会在condition为true的时候,返回第一个声明。
但是如果是使用函数表达式就没有什么问题了。
//可以这样做
var sayHi;
if(condition){
sayHi = function (){
alert("Hi!");
}
} else {
sayHi = function (){
alert("Yo!");
}
}
这个例子不会有意外,不同的函数会根据condition被赋值给sayHi。
能够创建函数再赋值给变量,也就能够把函数作为其他函数的值返回。
function createComparisonFunction(propertyName){
return function(obj1,obj2){
var value1 = obj1[propertyName];
var value2 = obj2[propertyName];
if(value1 < value2){
return -1;
} else if(value1 > value2){
return 1;
} else {
return 0;
}
}
}
createComparisonFunction()就返回了一个匿名函数。返回的函数可能会被赋值给一个变量或者以其他方式被调用,把函数当成值来使用的情况下,都可以使用匿名函数。不过这不是匿名函数的唯一途径。
7.1递归
递归函数是在一个函数通过名字调用自身的情况下构成的。
function factorial(num){
if(num<=1){
return 1;
} else {
return num +factorial(num-1);
}
}
这是一个经典的递归阶乘函数。在下面的情况中使用此函数,会报错。
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //出错!
在调用anotherFactorial()时,由于必须执行factorial()(因为函数体内有factorial函数),而factorial已经不再是函数,所以就会导致错误。
在这种情况下,使用arguments.callee(指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用)可以解决这个问题。
function factorial(num){
if(num<=1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
但是,在严格模式下,不能通过脚本访问arguments.callee,访问这个属性会导致错误。不过可以使用命名函数表达式来达成相同的效果。
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});
这种方式在严格模式和非严格模式下都行得通。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式,就是在一个函数内部创建另一个函数,仍一前面的createComparisonFunction()函数为例 :
function createComparisonFunction(propertyName){
return function(object1,object2){
**var value1 = object[propertyName];
**var value2 = object[propertyName];
if(value1 < value2){
return -1;
} else if(value1 > value2){
return 1;
} else{
return 0;
}
}
}