ECMAScript中没有类的概念,里面对于对象的描述是:“无序属性的集合、其属性可以包含基本值、对象或者函数”
一、理解对象
创建自定义对象的方法:
- 以前:
var person = new Object();
person.name = "McRay";
person.sayName = function() { };
- 现在(对象字面量):
var person = {
name: "McRay";
sayName : function(){}
};
二、创建对象
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("McRay",23,"Student");
var person2 = createPerson("Greg",27,"Doctor");
虽然解决了创建多个相似对象的问题,但是却没有解决对象识别的问题(即怎么知道对象的类型)。
2、构造函数模式:创建自定义的构造函数,从而定义自定义对象类型的属性和方法
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){};
}
var person1 = new Person("McRay",23,"Student");
var person2 = new Person("Greg",27,"Doctor");
与createPerson()
中的不同之处:
- 没有显式地创建对象;
- 直接将方法和属性赋给了this对象;
- 没有return语句;
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
上面的代码中有一个缺点就是每一个构造方法都必须在每个实例上重新创建一遍,可以通过把函数定义转移到构造函数外部来解决这个问题:
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){}
var person1 = new Person("McRay",23,"Student");
var person2 = new Person("Greg",27,"Doctor");
3、原型模式
我们创建的每个函数都有一个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();
var person2 = new Person();
person2.sayName();
alert(person1.sayName == person2.sayName);//true
下面是Person构造函数、Person的原型属性以及Person现有的两个实例之间的关系图:
1、理解原型对象
- 从图中可以看出实例与构造函数没有直接的关系。
- 每当代码读取某个对象的某个属性时,都会执行一次搜索,首先从对象实例本身开始,然后一直搜索指针指向的原型对象,直到找到属性。
- 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果为实例添加了一个与实例原型中同名的属性,那么该属性会覆盖原型中的那个属性。
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"
- 可以使用
hasOwnProperty()
方法检测一个属性是存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例中,才会返回true,注意是只存在。
2、原型与in操作符
- 当单独使用in操作符时,会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
- 同时使用
hasOwnProperty()
和in操作符就可以确定该属性到底是存在于对象中,还是存在于原型中,如下所示
function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name) && (name in object);
}
- 要取得对象上所有可枚举的实例属性,可以使用Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。如果你向得到所有实例属性,无论是否枚举,可以使用
Object.getOwnPropertyNames()
方法。
3、更简单的原型语法
function Person(){}
Person.prototype = {
name : "Nicholas",
age : 27,
job: "Software Engineer",
sayName : function() { }
};
- 需要注意的是原型对象的
constructor
属性不再指向Person了,变成了新对象的constructor
属性,如果想指向原来的Person对象,可以如下设置:
function Person(){}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 27,
job: "Software Engineer",
sayName : function() { }
};
但是如果这样设置的话,会将constructor
属性的[[Enumerable]]特性被设置为true。所以还可以使用
Object.defineProperty()```方法
//重构构造函数
Object.defineProperty(Person.prototype,"constructor",{
enumerable : false,
value : Person
});
4、原型的动态性
由于在原型中查找值得过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例中反映出来——即使是先创建了实例后修改原型也照样如此.
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi();//"hi"
原因可以归结为实例与原型之间的松散连接关系。尽管可以随时为原型添加属性和方法,但是如果重写整个原型对象,就会切断了现有原型与任何之前已经存在的对象实例之间的联系,因为实例中的指针仅指向原型,而不指向构造函数。
5、原型对象的问题
由于其共享的本性,如果对于包含引用类型值的属性来说,问题就比较突出了,请看下面的例子:
function Person(){}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 27,
job: "Software Engineer",
friends : ["Shelby","Court"],
sayName : function() { }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends);//"Shelby,Court,Van"
alert(person2.friends);//"Shelby,Court,Van"
alert(person1.friends == person2.friends);//"true"
4、组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,解决了原型模式的问题。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby","Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("McRay",23,"Student");
var person2 = new Person("Greg",27,"Doctor");
person1.friends.push("Van");
alert(person1.friends);//"Shelby,Court,Van"
alert(person2.friends);//"Shelby,Court"
alert(person1.friends == person2.friends);//"false"
5、动态原型模式
function Person(name,age,job){
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.sayName != "function") {
Person.prototype.sayName = function(){}
};
}
var friend = new Person("Nicholas",27,"Software Engineer");
friend.sayName();
6、寄生构造函数模式
基本思想就是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
function Person(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 friend = new Person("Nicholas",27,"Software Engineer");
friend.sayName();
7、稳妥构造函数模式
与寄生构造函数模式的不同是:新创建对象的实例方法不引用this;不适用new操作符调用构造函数。
function Person(name,age,job){
var o = new Object();
o.sayName = function(){};
return o;
}
var friend = Person("Nicholas",27,"Software Engineer");
friend.sayName();
四、继承
谈到面向对象,就不得不说到继承这个概念,因为javascript中的函数是没有签名的,所以ECMAScript只支持实现继承。
1、原型链
原型链是JS继承中的一个重要概念,其基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。具体的做法就是让原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着指向另一个构造函数的指针,如此层层递进,就构成了实例与原型的链条,这就是原型链的概念。
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;
};
var instance = new SubType();
alert(instance.getSuperValue());//true
关系图如下:
- 别忘记默认的原型Object。
- 不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
- 在通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做就会重写原型链。
- 原型链的问题还是包含引用类型值得原型的问题。
借用构造函数
解决原型中包含引用类型值所带来问题的过程中,可以借用构造函数,别忘了函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法可以在新创建对象上执行构造函数,如下所示:
function SuperType(name){
this.name = name;
this.color = ["red","blue","green"];
}
function SubType(){
//继承了SuperType
SuperType.call(this,"Nicholas");
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.color);//"red,blue,green"
- 借用构造函数带来的问题是方法都在构造函数中定义,因此函数复用就无从谈起。
组合继承
将原型链和借用构造函数的技术组合一起,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
function SuperType(name){
this.name = name;
this.colors = ["red","green","blue"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name,age){
//继承实例属性
SuperType.call(this,name);//第二次调用SuperType();
this.age = age;
}
//继承原型属性和方法
SubType.prototype = new SuperType(); //第一次调用SuperType();
SubType.prototype.constuctor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.color);//"red,blue,green"
组合继承的问题是多次调用超类型构造函数而导致的低效率问题,可以与寄生模式组合使用
原型式继承
function object(o){
function F(){}
F.prototype = o;
return new F();
}
可以使用Object.create()方法规范化了原型式继承。接收两个参数,一个是用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
寄生式继承
function createAnother(original){
var clone = object(original);
clone.sayHi = function() {
}
return clone;
}
寄生式组合继承
背后思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function SuperType(name){
this.name = name;
this.colors = ["red","green","blue"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name,age){
//继承实例属性
SuperType.call(this,name);//第二次调用SuperType();
this.age = age;
}
inheritPrototype(SubType,SuperType){
var prorotype = Object(superTYpe.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
//继承原型属性和方法
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.color);//"red,blue,green"