JavaScript高级程序设计学习笔记--面向对象的程序设计(一)-- 创建对象 (工厂模式、构造函数模式、原型模式等)
var person = new Object(); person.name = 'nico'; person.sayName = function(){ alert(this.name); }; person.sayName(); //'nico'
这种方式有明显的缺点:使用同一个接口创建很多对象,会产生大量重复代码。 为解决这个问题,人们开始使用工厂模式的一种变体。
function createPerson (name,age) { var o = new Object(); o.name = name; o.age = age; o.sayName = function() { alert(this.name); }; return o; } var person1 = createPerson('nico',22); var person2 = createPerson('nana',23); person1.sayName(); //'nico' person2.sayName(); //'nana'
工厂模式虽然解决了创建多个相似对象的问题,但却没解决对象识别的问题(即怎样知道一个对象的类型)。
function Person(name,age) { this.name = name; this.age = age; this.sayName = function() { alert(this.name); } } var person1 = new Person('nico',22); var person2 = new Person('nana',23); person1.sayName(); //'nico' person2.sayName(); //'nana'
函数名Person的首字母为大写P。按照惯例,构造函数始终都应该以一个大写字母开头。 要创建Person对象实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
对象的constructor属性最初是用来标识对象类型的。 但是检测对象类型,还是 instanceof操作符更可靠一些。 我们在这个例子中创建的所有对象既是Object的实例,同时也是Person对象的实例。
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型; 而这正是构造函数模式胜过工厂模式的地方。 在这个例子中,person1和person2之所以同是Object的实例,是因为所有对象均继承自Object。
//当作构造函数 var person = new Person('nico',22); person.sayName(); //'nico' //作为普通函数来调用,在全局对象中调用一个函数时,this对象总是指向Global对象(在浏览器中就是window对象)。 Person('nana',22); //方法属性会添加到window window.sayName(); //'nana' //在另一个对象的作用域中调用 var o = new Object(); Person.call(o,'jhon',30); o.sayName(); //'Jhon'
2、构造函数的问题
function Person(name,age) { this.name = name; this.age = age; this.sayName = new Function ("alert(this.name)"); //与声明函数在逻辑上是等价的 }
从这个角度看构造函数,更容易明白每个Person实例都包含一个不同的Function实例。 这个两个函数是不等的,下面代码可以证明:
alert(person1.sayName == person2.sayName); //false
原型模式
function Person(){}; Person.prototype.name = 'nico'; Person.prototype.age = 22; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); person1.sayName(); //'nico' var person2 = new Person(); person2.sayName(); //'nico' alert(person1.name == person2.name); //true
在此,我们将sayName()方法和所有属性直接添加到了prototype属性中,构造函数变成了空函数。 即使如此,也仍然可以通过调用构造函数来创建一个新对象,而新对象还会具有相同的属性和方法。 但与构造函数不同的是,新对象的这些属性和方法是所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。 要理解原型模式的工作原理,必须先理解ECMAScript中原型的性质
alert(Person.prototype.isPrototypeOf(person1)); //true alert(Person.prototype.isPrototypeOf(person2)); //true
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性值。 这正是多个对象实例共享原型所保存的属性和方法的基本原理。
function Person(){} Person.prototype.name = 'nico'; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.name = 'nana'; alert(person1.name); //'nana' alert(person2.name); //'nico'
这个例子中,person1的name被一个新值屏蔽了。 但无论访问person1.name还是person2.name都能正常返回值。即分别是“nana”(来自实例) "nico"(来自原型)。 当作alert()中访问person1.name时,需要读取它的值,因此就会在实例上搜索一个名为name的属性,这个属性确实存在,于是就返回它的值而不再搜索原型了。 当以同样的方式访问person2.name时,并没有在实例上发现该属性,因此会继续搜索原型,结果在那里找到了name属性。
function Person() {} Person.prototype.name = 'nico'; var person1 = new Person(); alert(person1.hasOwnProperty("name")); //false person1.name = 'john'; alert(person1.hasOwnProperty("name")); // true
2、原型与in操作符
function Person() {} Person.prototype.name = 'nico'; var person1 = new Person(); alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true
3、更简单的原型语法
function Person() {} Person.prototype = { name:'nico' };
我们将Person.prototype设置为等于一个以对象字面量形式创建的对象。最终结果相同,但有一个例外:constructor属性不再指向Person了。 前面讲过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们再这里使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性页变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了。例:
var person = new Person(); alert(person instanceof Person); //true alert(person.constructor == Person); //false alert(person.constructor == Object); //true
如果constructor属性很重要,可以特意将它设置回适当的值。 例如:
function Person(){} Person.prototype = { constructor:Person, name:'nico', sayName:function(){ alert(this.name); } }; var person = new Person(); alert(person.constructor == Person) //true
var person = new Person(); Person.prototype.name = 'nico'; alert(person.name); //'nico'
其原因可以归结为实例与原型之间的松散连接关系。当我们调用person.name时,首先会在实例中搜索名为name的属性,在没找到的情况想,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而不是副本,因此就可以在原型中找到新的name的属性并返回值。
function Person(){} var person = new Person(); Person.prototype = { name:'nico' }; alert(person.name); //error
在这个例子中,先创建了Person的一个实例,然后又重写了Person原型。 然后在调用person.name时发生了错误,因为person指向的原型中不包含以改名字(‘name’)命名的属性。 重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系; 它们引用的仍然是最初的原型。
String.prototype.startsWith = function (text) { return this.indexOf(text) == 0 }; var msg = "Hello world"; alert(msg.startsWidth("Hello")); //true
由于msg是字符串,而且后台会调用String基本包装函数创建这个字符,因此通过msg就可以调用startsWidth()方法。
function Person() {} Person.prototype = { name:'nana', friends:['nico'], sayName:function(){ alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push('john'); alert(person1.friends); // "nico,john"); alert(person2.friends); //"nico,john"); alert(person1.friends== person2.friends); //true
在此,Person.prototype对象有一个名为friends的属性,该属性包含一个字符串数组。 然后,创建了Person的两个实例。 接着,修改了person1.friends引用的数组,向数组中添加了一个字符串。 由于数组存在于Person.prototype而非person1,所有刚刚提到的修改也会通过person2.name反映出来。 假如我们的初衷就是这样所有实例中共享一个数组,那么对这个结果没有话可说。 可是,实例一般都是要有属于自己的全部属性的。 而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
function Person(name) { this.name = name; this.friends = ['nico']; } Person.prototype = { sayName:function(){ alert(this.name); } }; var person1 = new Person('lee'); var person2 = new Person('lus'); person1.sayName(); //'lee' person2.sayName(); //'lus' person1.friends.push('nana'); alert(person1.friends); //'nico,nana' alert(person2.friends); //'nico'
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的方法sayName()则是在原型中定义的。 而修改了person1.friends,并不会影响到person2.friends,因为它们分别引用了不同的数组。