ECMAScript中没有类的概念,因此它的对象与基于类的语言中的对象有所不同。
1.理解对象
创建对象最简单的方式是创建一个Object实例,再为它添加属性和方法,如下:
<span style="font-family:SimSun;font-size:12px;">var people = new Object(); people.age = 20; people.sayAge = function(){ alert(this.age); };</span>
以对象字面量的方式创建对象:
<span style="font-family:SimSun;font-size:12px;">var people = { age:20, sayAge:function(){ alert(this.age); } };</span>
2.创建对象
工厂模式:
<span style="font-family:SimSun;font-size:12px;">function createPeople(name, age){ var o = new Object(); o.name = name; o.age = age; o.sayAge = function(){ alert(this.age); }; return o; } var p1 = createPeople("AA", 19); var p2 = createPeople("BB", 19);</span>
使用工厂模式创建对象的不足在于无法知道一个对象的类型,没解决对象识别的问题(对象都Object类型,这里只是人为给该方法命名为createPeople,使创建出的对象”看似“people),因此引出下一种模式;
构造函数模式:
<span style="font-family:SimSun;font-size:12px;">function People(name, age){ this.name = name; this.age = age; this.sayAge = function(){ alert(this.age); }; } var p3 = new People("CC", 21); var p4 = new People("DD", 22);</span>
使用该模式创建People的实例,要使用new操作符,调用构造函数实际上经过以下四步骤:
- 创建新对象;
- 将构造函数作用域赋给新对象(因此this指向了这个新对象);
- 为新对象添加属性(执行构造函数中的代码);
- 返回新对象
使用该模式没有显式创建对象、直接将属性和方法赋给this对象、没return语句。
上面p3、p4都保存有People的一个不同的实例,p3、p4都有一个constructor(构造函数)属性,指向People,如下:
<span style="font-family:SimSun;font-size:12px;">alert(p3.constructor == People); // true</span>
下面使用instanceof操作符检测对象类型,如下:
<span style="font-family:SimSun;font-size:12px;">alert(p3 instanceof People); // true </span>
创建自定义的构造函数可以将它的实例标识为一种特定的类型,解决了工厂模式解决不了的对象识别问题。
对于任何函数,能通过new操作符调用的,就可以作为构造函数;不能有通过new操作符调用的,就跟普通函数一样。
<span style="font-family:SimSun;font-size:12px;">// 作为普通函数调用 People("EE", 23); // 添加到了window window.sayAge(); // "EE" // 在另一个对象的作用域中调用(另一个对象作为调用者) var o = new Object(); People.call(o, "FF", 24); o.sayAge(); // 24</span>
上面第一个例子中,因为在全局作用域中调用一个函数时,this对象是指向Global对象的,在浏览器中即是window对象,因此可以通过window对象调用sayAge()方法。
虽然使用构造函数创建对象解决了对象类型识别问题,但这种模式不足就在于每个方法都要在每个实例上重新创建一遍。因为在ECMAScript中的函数也是对象,每定义一个函数也是实例化了一个对象,因此p3、p4中的sayAge()方法并不是同一个Function实例,上面使用构造函数创建对象的模式在逻辑上其实与以下的是等价的。
<span style="font-family:SimSun;font-size:12px;">function People(name, age){ this.name = name; this.age = age; this.sayAge = new Function("alert(this.age)"); } alert(p3.sayAge == p4.sayAge ); // false</span>
使用这种方式,不同实例上的同名函数是不相等的,这时可以用下面的方式解决这问题:
<span style="font-family:SimSun;font-size:12px;">function People(name, age){ this.name = name; this.age = age; this.sayAge = sayAge; } function sayAge(){ alert(this.age); }; alert(p3.sayAge == p4.sayAge ); // 使用这种方式会返回true</span>
这样把sayAge()函数的定义定义在构造函数外部,在构造函数里将sayAge属性等于全局的sayAge()函数,这样sayAge属性包含的是一个指向sayAge()函数的指针,因此p3、p4就共享了这个全局的sayAge()函数;那问题又来了,如果对象需要定义很多方法时,那么将要定义很多个全局函数,在此引出下一种模式;
原型模式:
先看一个例子:
<span style="font-family:SimSun;font-size:12px;">function People(){} People.prototype.age = 20; People.prototype.sayAge = function(){ alert(this.age); }; var p5 = new People(); p5.sayAge(); // 20 var p6 = new People(); p6.sayAge(); // 20 alert(p5.sayAge == p6.sayAge ); // true</span>
使用原型对象的好处在于可以让所有实例共享它所包含的属性和方法,不必在构造函数中定义对象的信息,而是将这些信息添加到原型对象上,这里的构造函数是空函数,属性和方法被加到People的prototype属性上,但是使用构造函数创建对象时,对象还是具有这些属性和方法,与传统构造函数不同的是,新对象的属性和方法是由所有实例共享的。要理解原型模式原理,还要先理解原型对象的性质。
每当创建一个函数时,都会为该函数创建一个prototype属性,这个属性指向函数的原型对象(原型对象的作用是包含可以由特定类型的所有实例共享的属性和方法);而所有原型对象都会自动获得一个构造函数属性(constructor),这个属性包含一个指向prototype属性所在函数的指针。前面的例子就是People.prototype.constructor指向People。
当调用构造函数创建一个新实例时,该实例 内部将包含一个指针,指向构造函数的原型对象,这个指针一般称为[[Prototype]]。这个连接存在于实例与构造函数的原型对象之间,而不是实例与构造函数间。
使用对象实例无法访问原型对象,例如p5.prototype是访问不到原型对象的,也没有方式可以访问[[Prototype]],不过一些浏览器在每个对象上都支持一个__proto__属性,通过该属性可以访问到原型对象(p5.__proto__)。
当访问对象的某一属性时,都会搜索给定名字的属性,首先搜索对象实例本身,如果找到给定名字的属性,则返回;若没找到,再继续搜索指针指向的原型对象,若找到,则返回。这也是多个对象实例共享原型保存的属性和方法的原理。如果人为给实例添加一个与原型中某一属性同名的属性,那么实例中的该属性将屏蔽原型中的那个属性,但原型的被屏蔽的属性的值还是不变的。
使用hasOwnProperty()方法可以用于检测一个属性是存在于实例中还是原型中,只有在属性存在于实例中时,该方法才返回true。hasOwnProperty()方法常被用于for-in循环中过滤原型中的属性,因为for-in循环默认返回的是所有能通过对象访问的、可枚举的(enumerated)属性,既包括实例中的属性也包括原型中的(这也是for-in循环与传统的for循环的区别)。
接下来使用对象字面量的方式重写上面的原型对象,如下:
<span style="font-family:SimSun;font-size:12px;">function People(){} People.prototype = { age:20, sayAge:function(){ alert(this.age); } };</span>
上面将People.prototype设置为等于一个以对象字面量形式创建的新对象,结果也是相同的,唯一例外的是constructor属性不再指向People。因为这里使用{}创建原型对象,每次使用{}都会创建一个新对象,在这里{}就是对象(Object),new Object就相当于{},因为此时constructor属性指向的是Object。
<span style="font-family:SimSun;font-size:12px;">var people = new People(); alert(people.constructor == People); // false alert(people.constructor == Object); // true</span>
使用构造函数与原型模式:
由前面的介绍可知,使用原型模式下所有实例的属性都是共享的,对其中一个实例的进行修改都会影响到其他实例的属性,但有时我们并不希望实例所有的属性都一样,这时就可以使用组合构造函数与原型模式一起使用,构造函数用于定义实例属性,而原型模式用于定义方法和共享属性。
<span style="font-family:SimSun;font-size:12px;">function People(name, age){ this.name = name; this.age = age; } People.prototype = { sayAge:function(){ alert(this.age); } }; var p1 = new People('AA', 20); p1.name = 'CC'; // 修改p1的name属性 var p2 = new People('BB', 21); alert(p1.name); alert(p2.name); alert(p1.name == p2.name); // false alert(p1.sayAge == p2.sayAge); // true</span>
Author:顾故
Sign:别输给曾经的自己