javascript面向对象程序设计系列(一)---创建对象
javascript是一种基于对象的语言,但它没有类的概念,所以又和实际面向对象的语言有区别,面向对象是javascript中的难点之一。现在就我所理解的总结一下,便于以后复习:
一、创建对象
1、创建自定义对象最简单的方式就是创建Object的实例,并在为其添加属性和方法,如下所示:
var cat = new Object(); //以猫咪为对象,添加两个属性分别为猫咪姓名和猫咪花色,并添加一个方法,说出猫咪的姓名 cat.name = "cc"; cat.color = "white"; cat.say = function(){ alert(this.name); }
近几年,对象字面量成为创建对象的首选方式,如下:
var cat = { name: "cc", color: "white", say: function(){ alert(this.name); } }
以上两种方式都可以用来创建单个对象,但这些方式具有明显的缺点:在创建多个同一类型的对象的时候,会产生很多重复代码,因此,人们开始使用工厂模式的一种变体来解决这个问题。
2、工厂模式创建对象
利用工厂模式的思想,用一个函数来封装创建对象的细节,并提供特定接口来实现创建对象,上面的例子可以写成如下:
function createCat(name,color){ var o = new Object(); o.name = name; o.color = color; o.say = function(){ alert(this.name); } return o; } var cat1 = createCat("cc","white"); var cat2 = createCat("vv","black");
函数createCat()可以根据接受到的猫咪的姓名和花色来创建一个猫咪对象,我们可以重复调用这个函数,每次调用都会返回一个包含两个属性和一个方法的对象,该对象表面了猫咪的姓名和花色,并可以说出猫咪的姓名。
利用工厂模式创建对象可以很好的解决上面的定义大量重复代码的问题,但我们却无法真正区分对象的类型,我们只能知道创建的对象是Object,而无法识别cat1和cat2都是猫咪对象。为解决这个问题,javascript提出了一种构造函数模式。
3、构造函数模式
我们知道在ECMAScript中定义了很多原生的构造函数,比如说Array、Date,我们可以用这些原生构造函数创建特定类型的对象。此外,javascript也允许我们创建自定义的构造函数,定义对象类型的属性和方法,从而用于创建某一类的对象。我们利用构造函数模式重写上面的例子如下:
function Cat(name,color){ this.name = name; this.color = color; this.say = function(){ alert (this.name) } } var cat1 = new Cat("cc","white"); var cat2 = new Cat("vv","black");
我们可以用Cat()函数代替createCat()函数,相对于createCat()函数,Cat()函数存在以下不同:
1> 没有显示的创建对象;
2> 直接将属性和方法付给了this对象;
3> 没有return语句;
同时,我们还注意到Cat()函数的函数名首字母大写。按照书写规范,构造函数最好都以大写字母开头,其它函数都以小写字母开头,当然小写字母开头的函数也可以当成构造函数来创建对象。其实构造函数本质就是一个函数,只不过用它来创建对象。
当我们使用Cat()构造函数创建一个实例对象时,必须使用new运算符,使用这种方式创建对象会经过以下4个步骤:
1> 创建一个新对象
2> 将构造函数中this指向新创建的对象
3> 执行构造函数中的语句(即为新对象添加属性和方法)
4> 返回这个新对象
在上面的例子中,分别创建了猫咪的两个不同实例,这两个实例对象都有一个constructor(构造函数)属性,用于指向创建实例的构造函数,即Cat()
cat1.constructor == Cat //=> true cat2.constructor == Cat //=> true
对象的constructor属性是用来标识对象类型的。但javascript提供了一个操作符instanceof来检测对象的类型,上面例子中创建的对象cat1和cat2即是Object的实例也是Cat的实例
cat1 instanceof Object //=> true cat1 instanceof Cat //=> true cat2 instanceof Object //=> true cat2 instanceof Cat //=> true
以上可知,利用构造函数模式,我们可以将创建的对象标识为一种特定的类型。上面我们也提到过构造函数其实也是函数,它和普通函数的区别就在于调用方式的不同。任何函数,只要通过new运算符来调用,就可以当做构造函数,而任何函数,不通过new运算符调用,就和普通的函数调用没有区别,看下面的实例:
//当做构造函数使用 var cat1 = new Cat("cc","white"); cat1.say(); //=> cc //当做普通函数调用 Cat("vv","block"); //普通函数运行,其this默认为window(ECMAScript3下) window.say(); //=> vv //在另一个对象作用域中调用 var o = new Object(); Cat.call(0,"ss","yellow"); o.say(); //=> yellow
以上三种使用方式,注意this的值
构造函数虽然解决了上面存在的一些问题,但它也有自己的缺点,就是每当创建一个对象时,其内部的所有属性和方法都会重新创建一次,都会占有一定的内存,上面实例中创建的两个猫咪对象cat1和cat2,它们的say()方法虽然相同,但却不是同一个实例,占有不同的内存。
cat1.say == cat2.say; //=> false
但在实际使用中,每个对象中的方法实现的是同样的功能,我们完全没有必要创建多个Function实例,因此我们可以通过将函数定义到构造函数外面来解决这个问题。
function Cat(name,color){ this.name = name; this.color = color; this.say = sayName; } function sayName(){ alert(this.name); } var cat1 = new Cat("cc","white"); var cat2 = new Cat("vv","black");
上例中,我们将say函数移到了外面,这样cat1和cat2就可以共享在全局中定义的sayName函数了,但这样做又存在一个新问题,就是定义在全局中的函数其实只是被某一类对象所调用,又使得全局作用域过于混乱,而对象也没有封装性可言了。这时,我们可以使用原型模式来解决。
4、原型模式
javascript中,我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象中的所有属性和方法都会被构造函数创建的实例对象所继承,即每个函数的prototype都是通过调用该构造函数而创建的实例对象的原型对象。我们看一个例子
function Person(){ } Person.prototype.name = "cc"; Person.prototype.age = "2"; Person.prototype.job = "software engineer"; Person.prototype.sayName = function(){ alert(this.name); } var person1 = new Person(); person1.sayName(); //=> "cc" var person2 = new Person(); person2.sayName(); //=>"cc" person1.sayName == person2.sayName; //=> true
我们将Person的属性都放在其原型对象中,其创建的实例对象person1和person2都包含一个内部属性,该属性指向Person的原型函数Person.prototype。我们用图示表示(在这里偷懒,直接上高程的图了):
上图中展示了Person构造函数,Person的原型属性以及Person的两个实例对象之间的关系。其中,Person具有一个prototype属性指向其原型对象,而Person的原型对象中又有一个constructor函数指向Person。其两个实例对象的内部属性[[Prototype]](目前还没有标准的方式访问)都指向了Person.prototype。从上图我们可以看出,其实创建的实例对象和构造函数并没有直接关系。
上面的例子中,虽然person1和person2都不具有属性和方法,但却可以调用sayName()方法。每当读取对象的某个属性时,都会执行一次搜索,首先搜索实例对象本身,如果没有找到则继续搜索对象指向的原型对象,如果有则返回,如果没有则返回undefined。
上面的对象实例可以访问原型中的属性和方法,但却不能重写原型中的值。如果我们在实例中重写一个与原型中相同名字的属性,就会在实例中创建该属性,并屏蔽原型中的同名属性,但不会修改原型中的属性。如下所示:
function Person(){ } Person.prototype.name = "cc"; Person.prototype.age = "2"; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); } var person1 = new Person(); var person2 = new Person(); person1.name = "vv"; person1.name; //=> vv person2.name; //=> cc Person.prototype.name; //=> cc
前面实例中每次给原型添加属性和方法都要输入Person.prototype。因此我们可以使用对象字面量来重写原型对象,如下所示:
function Person(){ } Person.prototype = { name : "cc", age : 2, job : "Software Engineer", sayName : function(){ alert(this.name); } }
上面的代码定义的Person.prototype与上面定义的原型对象几乎相同,但又一个例外,使用对象字面量重新定义原型对象(相当于重新定义了一个对象),其constructor属性就不在指向Person了,而是指向Object构造函数。此时使用instanceof操作符还可以返回正确结果,但使用constructor属性则无法确定对象的类型了。
var newPerson = new Person(); newPerson instanceof Object; //=> true newPerson instanceof Person; //=> true newPerson.constructor == Object; //=> true newPerson.constructor == Person; //=> false
如果我们需要constructor属性,可以手动设置其值。
注意:我们可以随时添加原型的属性和方法,并可以在实例中立即反应过来,但我们一定要注意重写原型对象的位置。调用构造函数创建对象会添加一个指向原型的指针,如果我们重写对象的原型函数,就切断了已创建实例和构造函数的关系。
function Person1(){ } Person1.prototype.name = "cc"; Person1.prototype.say = function(){ alert(this.name); } var friend1 = new Person1(); friend.say(); //=> cc function Person(){ } var friend = new Person(); Person.prototype = { constructor : Person, name : "cc", age : 2, job : "Software Engineer", sayName : function(){ alert(this.name); } } friend.sayName(); //=> error
原型模式的缺点:
1> 无法通过构造函数传递初始化参数
2> 引用类型的实例共享
我们看一个实例:
function Person(){ } Person.prototype = { constructor : Person, name : "cc", age : 2, job : "Software Engineer", friends : ["vv","dd"], sayName : function(){ alert(this.name); } } var person1 = new Person(); var person2 = new Person(); person1.friends.push("aa"); person1.friends; //=> vv,dd,aa person2.friends; //=> vv,dd,aa person1.friends == person2.friends; //=> true
以上代码,当我们修改了person1.friends时,相应的person2.friends也会改变,因为它们指向同一个数组。但在实际中,我们通常希望实例拥有自己的独立的属性,因此提出了一个组合使用构造函数模式和原型模式的方法,这种混合模式,是目前使用最广泛、认同度最高的一种创建自定义类型的方法。
function Cat(name,color){ this.name = name; this.color = color; this.firends = ["aa","bb"]; } Cat.prototype = { constructor : Cat, sayName : function(){ alert(this.name); } } var cat1 = new Cat("cc","white"); var cat2 = new Cat("vv","block"); cat1.firends.push("dd"); cat1.firends; // => aa,bb,dd cat2.firends; //=> aa,bb cat1.firends == cat2.firends; //=> false cat1.sayName == cat2.sayName; //=> true
从以上实例可知,将我们希望实例对象独立拥有的属性放到构造函数中,将我们希望实力对象共享的属性都放到原型中,有效的解决了上面存在的问题。
5、检测对象类型的几个方法
1> isPrototypeOf() 方法
目前我们在所有实现中都无法访问到[[prototype]],但我们可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系,如果一个对象的[[prototype]]属性指向调用isPrototypeOf()方法的对象,就返回true,否则返回false,如下:
Person.prototype.isPrototypeOf(person1); //=> true Person.prototype.isPrototyoeOf(person2); //=> true
以上,我们用Person的原型对象测试person1和person2,都返回true。
2> Object.getPrototypeOf() 方法 (ECMAScript5新定义)
这个方法返回[[prototype]]的值,如:
Object.getPrototypeOf(person1) == Person.prototype; //=> true Object.getPrototypeOf(person1).name; //=> "cc"
3> hasOwnProperty() 方法
该方法用于检测一个属性是否存在于一个实例中,而不是存在于原型中,如:
person1.name; //=> cc 来自于原型 person1.hasOwnProperty("name"); //=> false person1.name = "vv"; //=> 来自于实例 person1.hasOwnProperty("name"); //=> true
上面例子中,当person1的name属性来自于原型时,hasOwnProperty()返回false,给person1重写name属性后,则返回true。
4> in运算符
有两种方式使用in操作符:单独使用或者再for-in循环中使用。
单独使用时,in运算符可以用来检测给定属性是否能够被对象所访问,不管该属性是存在于实例中还是原型中
person1.name; //=> cc //=>来自原型 "name" in person1; //=> true person1.name = "vv"; //=>来自实例中 "name" in person1; //=> true
上面例子中可见,无论对象的属性来自原型还是来自实例,只要能被person1对象访问就返回true
在for-in循环中,返回所有能够被对象访问的、可枚举的属性,其中即包括实例中的属性,也包括原型中的属性。屏蔽了原型中不可枚举的属性的实例属性也会在for-in循环中返回,因为所有开发人员定义的属性都是可枚举的(IE8及更早版本下不会返回)。