javascript面向对象

来源:互联网 发布:8寸windows平板电脑 编辑:程序博客网 时间:2024/05/20 08:44

ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。

用自定义对象的最简单方式就是创建一个 Object 的实例,然后再为它添加属性和方法,如下所示:

var person = new Object();    person.name = "Nicholas";    person.age = 29;    person.job = "Software Engineer";    person.sayName = function(){        alert(this.name);    };

在这个例子中,创建了一个名为 person 的对象,并为它添加了三个属性( name 、 age 和 job )和一个方法( sayName() )。其中, sayName() 方法用于显示 this.name (将被解析为 person.name )的值。
按照这种方式,每次创建一个对象都需要new一个,这样写起来非常复杂,代码也变得很冗余,有没有方法可以减少代码量呢,这个时候可以想到使用函数,在面向对象中,这种函数叫构造函数。

function createPerson(name, age, job){    var obj = new Object();    obj.name = name;    obj.age = age;    obj.job = job;    obj.sayName = function(){        alert(this.name);    };    return obj;}var person1 = createPerson("Nicholas", 29, "Software Engineer");var person2 = createPerson("Greg", 27, "Doctor");

这样,每次想要创建一个对象,只需要调用这个函数就可以了,变得非常方便,但这个时候又遇到一个问题,没有解决对象识别的问题(即怎样知道一个对象的类型,或者说新建一个对象的时候,没有使用关键字new)。
再对这个代码进行修改:

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");

对比可以看到,函数名是大写字母开始的,函数里省略了new Object()这个显示创建对象,没有看到return obj的返回,取而代之的是用this指向这些属性;其实是因为它隐含地创建了一个var this = new Object(),还有隐含了返回里的return this;

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。
在前面例子的最后, person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person ,如下所示。

alert(person1.constructor == Person); //truealert(person2.constructor == Person); //true

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。

构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中, person1 和 person2 都有一个名为 sayName() 的方法,但那两个方法不是同一个 Function 的实例。
以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建 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");

在这个例子中,我们把 sayName() 函数的定义转移到了构造函数外部。而在构造函数内部,我们将 sayName 属性设置成等于全局的 sayName 函数。这样一来,由于 sayName 包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的同一个 sayName() 函数。这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。
这些问题可以通过使用原型模式来解决。
我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 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

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。
原型模式的最大问题是由其共享的本性所导致的。原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,因为通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的简化版的原则例子。

function Person(){}Person.prototype = {    constructor: Person,    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"alert(person1.friends === person2.friends); //true

在这个原型对象中,我们添加一个friends的数组属性,结果发现,引用类型的数据会因为其中一个对象修改了里面的参数而影响到另一个对象对原属性的引用。解决这个问题可以组合使用构造函数模式和原型模式。
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。下面的代码重写了前面的例子。

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("Nicholas", 29, "Software Engineer");var person2 = new Person("Greg", 27, "Doctor");    person1.friends.push("Van");alert(person1.friends); //"Shelby,Count,Van"alert(person2.friends); //"Shelby,Count"alert(person1.friends === person2.friends); //falsealert(person1.sayName === person2.sayName); //true

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName() 则是在原型中定义的。而修改了 person1.friends (向其中添加一个新字符串),并不会影响到 person2.friends ,因为它们分别引用了不同的数组。