JavaScript面向对象
面向对象
引用类型的实例就是对象,创建对象很简单:
var zhangsan = { |
Javascript中的对象的定义是:无序属性的集合,其属性可以包含基本值、对象或者函数
。这和其他的面向对象的语言是有很大的差别,其他的面向对象语言中有两个很重要的概念,类和实例,类就是对象的模板,实例就是根据模板创建的对象。而在JavaScript中并没有类的概念。所以如果现在需要创建另外一个对象lisi
,而lisi
和zhangsan
一样,也有相同的属性,应该怎么去做。这里就需要一个同类型的模板,在JavaScript中,用原型(prototype)来实现面向对象。
var Person = { |
在上面的例子中使用了__proto__
属性,这是对象的一个属性,是一个指向原型对象的指针,修改了原型对象,也就具有了原型对象中的方法,所以Person
就成为了zhangsan
和lisi
的共同的模板。
需要特别说明的是,ECMASript中管这个指向原型对象的指针为[[prototype]],但并没有标准的访问方式,而这里的__proto__
是在FixFox,Chrome和Safari所共同支持的
创建对象
上面提到了原型的概念,也用到了__proto__
属性,但实际中并不会用到该属性去修改原型对象,为了更好的理解原型的概念,下面还是从创建对象开始说起。依旧是为了解决具有共同属性的一些对象,我们如何创建一个模板,让创建对象变的更加轻松。
工厂模式
工厂模式就是一种创建具体对象的设计模式,因为没有类的概念,所以发明一种函数,封装所有创建对象的细节:
function createPerson(name, age){ |
工厂模式很好的解决了创建一类对象的需求,但是还有个问题不能解决,就是对象的识别问题,这里很难知道zhangsan
,lisi
是什么类型的。
构造函数模式
前面提到过构造函数就是一类特殊的函数,专门用来创建特定类型的对象,并且需要用new来调用。之前介绍过原生的构造函数,例如Object()
,Array()
。这里需要创建自定义的构造函数,从而定义自定义对象的属性和方法:
function Person(name, age){ |
构造函数和工厂模式的普通函数相比,有如下的特点:
- 没有显示的创建对象
- 直接将方法和属性赋值给this
- 没有return语句
- 方法名首字母大写
构造函数的调用也会经历下面4个过程
- 创建一个新对象
- 将构造函数的作用域赋值给新对象(因此this指向了新对象)
- 执行构造函数中的代码
- 返回新对象
构造函数模式就很好的解决了面向对象的问题,现在也可以验证对象所属的类型:
var res = zhangsan instanceof Person; //true |
并且每个对象都有一个属性指向它的构造函数constructor
alert(zhangsan.constructor == Person); //true |
构造函数当做普通函数
前文提到了构造函数和普通函数的4个区别,但这些区别只是我们观察到的区别,而不是解释器用来识别构造函数的依据。解释器是被构造函数的唯一依据是调用的方式,也就是是不是通过new
调用的。具体的说,任何函数,只要通过new调用,那么它就是构造函数,反之则不是:
function Person(name, age){ |
这里需要回顾一下函数的内部属性之一this
(另一个是arguments
),this
总是指向调用函数的对象,所以全局函数的this指向全局对象,这里就是window
。
构造函数的问题
构造函数的最主要的问题在于对于函数属性的问题,构造函数为每一个实例创建了函数属性都创建了一个新的Function
实例,也就是:
alert(zhangsan.tellName == lisi.tellName); //false |
为相同的任务而创建了两个完全一样的Function实例确实没有必要,所以可以把函数定义放到构造函数外面:
function Person(name, age){ |
这样解决了函数属性的问题,新的问题又随之而来。解决的办法实际上是把属于某个类的函数,为了实现所有实例都共享一个函数的目的,而强行的放到了全局环境中。产生两个问题:
- 全局函数名不副实,只能被某些对象所调用;
- 自定义的类型毫无封装性而言。
这些问题,都得通过原型模式来解决。
原型模式
构造函数的问题在于共享的函数不知道该放到哪,放到构造函数本身就会重复创建函数对象,放到全局环境则影响封装,而这里就引入了一个专门的对象,来存放一类对象公有的属性和方法。我们创建的每一个函数,都会有一个prototype
(原型)属性,这属性是一个指针,指向一个对象,而这个对象就包含了可以由特定的类型的所有实例所共享的属性和方法。也就是prototype指向的对象就是构造函数创建的对象的原型对象,这里就是共享的方法和属性所在的位置:
function Person(){ |
创建的新对象都共享了设置在prototype
的属性和方法。
理解原型模式
无论什么时候,只要创建了函数,就会根据一组特定的规则为该函数创建一个prototype
的属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象自动获得一个constructor
的属性,该属性包含一个指针,反过来指向函数。也就是Person.prototype.constructor
指向Person
。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor
的属性,其他属性都从Object
继承而来。当调用构造函数创建一个实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型对象,管这个指针叫做[[prototype]]
,也就是上文中用到的__proto__
属性。如下图展示了各个对象之间的关系:
原型链
之前用到的__prototype__
属性并不是标准的访问途径,关于对象和其原型之间的联系,标准定义了两个方法:
isPrototypeOf
:判断是不是原型和对象的关系getPrototypeOf
:获得对象的原型对象
Person.prototype.isPrototypeOf(zhangsan); //true |
创建的对象其实并没有name等属性,但是也可以访问,这是通过查找对象的属性的过程来实现的。当访问一个对象的属性时,解释器首先会在该对象上找对应的属性,如果没有则会继续在它的原型对象上找,如果还是没有就继续往上找原型对象的原型对象,一直到Object
的prototype
,如果还没找到才返回undefined
。这个搜索的链就是原型链:
zhangsan ---> Person.prototype ---> Object.prototype ---> null |
这就解释了为什么zhangsan上可以访问name
属性,也可以访问Object
原型上定义的属性.
也可以推论出,在新建的对象上定义同样的属性并不是修改原型中的属性,但会覆盖掉,在该对象上就相当于是屏蔽了原型中的对应属性:
lisi.name = "lisi"; |
属性来自
访问一个对象的属性时,这个属性既可能是来自对象本身,也可以是来自其原型对象,如何去区分一个属性到底来自哪里:
hasOwnProperty()
:确定属性是不是来自对象本身in
:确定整个原型链上是不是存在该属性
delete zhangsan.name; |
结合这两个方法就可以判断属性是来自对象本身还是其原型对象了。
构造函数模式和原型模式的结合
上面的例子我们也看到了原型模式的缺点,原型对象就是一个共享属性的存放地,一个实例对象的属性究竟是来自自身还是来自其原型对象是不确定的。对于属性是基本值的情况还是可以接受的,就如上面的例子,给新的对象添加同名属性时,可以隐藏原型对象的属性。然而对于属性是引用类型的情况,问题就变的不能接受了:
function Person(){ |
给zhangsan
增加了一个朋友,结果由于属性书共享在原型对象上的,所以全部的实例都增加了这个朋友,这显然是不能接受的。
构造函数模式的问题是所有的实例都有一份自己的属性,而原型模式的问题是所有的属性都共享在原型对象上,所以其实可以把它们很好的结合起来:
function Person(name, age) { |
这样就可以很好的解决方法的共享和属性的隔离了。
简化原型模式的写法
上面我们基本解决了创建对象的问题,通过构造函数和原型模式,还有最后一个问题,就是针对原型模式,写法比较复杂,每次给原型对象增加属性或者方法就得重新写一遍xxx.prototype.xxx = "xxx"
,我们试图把所有的属性一次性的赋值:Person.prototype = {
tellName: function(){
alert(this.name);
},
sayHi: function(){
alert("hello I am " + this.name);
}
};
这样导致的问题是并不是去给Person.prototype
增加方法,而是直接替换了,这样Perosn.prototype
的constructor
就不再指向Person
了,而是指向Object
,所以需要专门设定:Person.prototype = {
constructor: Person,
tellName: function(){
alert(this.name);
},
sayHi: function(){
alert("hello I am " + this.name);
}
};