面向对象(二)-读书笔记
三、原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
字面意思理解上面的话就是:prototype就是通过调用构造函数而创建的那个对象实例的原型对象。
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。举栗:
function Person(){ } Person.prototype.name = "Andy"; Person.prototype.age = 28; Person.prototype.job = "Actor"; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); person1.sayName(); //"Andy" var person2 = new Person(); person2.sayName(); //"Andy" console.log(person1.sayName == person2.sayName); //true
上面栗子,我们将sayName()方法和所有属性直接添加到Person的prototype属性中,构造函数变成了空函数。虽然如此,我们仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。
与构造函数不同的是,新对象的属性和方法是所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个方法。
1.理解原型对象
无论何时,只要创建了一个新函数,那么就会根据一组特定的规则为这个函数创建一个prototype的属性,并且这个属性指向函数的原型对象。
在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。上面的例子,Person.prototype.constructor指向Person。我们可以通过这个构造函数,继续为原型对象添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,都是从Object继承而来的。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262 第5 版中管这个指针叫[[Prototype]]。
要明确真正重要的一点是:这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
还是拿上面的例子做个关系图:
上图展示了Person构造函数、Person的原型属性以及Person两个实例之间的关系。
Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。
原型对象中除了constructor属性外,还有后来添加的其他属性。
Person的每个实例-person1和person2都包含一个内部属性,该属性仅仅指向了Person.prototype,也就是它们和构造函数没有直接的关系。
格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。
虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true,如下:
console.log(Person.prototype.isPrototypeOf(person1)); //true console.log(Person.prototype.isPrototypeOf(person2)); //true
因为它们内部都有一个指向Person.prototype的指针,所以都返回了true。
在所有支持的实现中,Object.getPrototypeOf()这个方法可以返回[[Prototype]]的值。如下:
console.log(Object.getPrototypeOf(person1)==Person.prototype); //true console.log(Object.getPrototypeOf(person1).name); //"Andy"
第一行代码确定Object.getPrototypeOf()返回的对象是这个对象的原型。
第二行代码取得原型对象中name属性的值。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,那么返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象找到了这个属性,则返回该属性的值。
也就是说,在我们调用person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例person1 有sayName 属性吗?”答:“没有。”然后,它继续搜索,再问:“person1 的原型有sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。栗子:
function Person(){ } Person.prototype.name = "Andy"; Person.prototype.name = 28; Person.prototype.name = "Actor"; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); var person2 = new Person(); person1.name = "Tom"; console.log(person1.name); //"Tom"---来自实例 console.log(person2.name); //"Andy"---来自原型
上面的栗子,person1的name被一个新值给屏蔽了。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个原型中的属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。
我们可以使用delete操作符完全删除实例属性,从而让我们能够重新访问原型中的属性。如下:
function Person(){ } Person.prototype.name = "Andy"; Person.prototype.name = 28; Person.prototype.name = "Actor"; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); var person2 = new Person(); person1.name = "Tom"; console.log(person1.name); //"Tom"---来自实例 console.log(person2.name); //"Andy"---来自原型 delete person1.name; console.log(person1.name); //"Andy"---来自原型
我们可以使用hasOwnProperty()方法检测一个属性是存在于实例中,还是存在于原型中。这个方法是从Object继承来的,这个方法只在给定属性存在于对象实例中时,才返回true。栗子:
function Person(){ } Person.prototype.name = "Andy"; Person.prototype.name = 28; Person.prototype.name = "Actor"; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); var person2 = new Person(); console.log(person1.hasOwnProperty("name")); //false person1.name = "Tom"; console.log(person1.name); //"Tom"----来自实例 console.log(person1.hasOwnProperty("name")); //true console.log(person2.name); //"Andy"----来自原型 console.log(person2.hasOwnProperty("name")); //false delete person1.name; console.log(person1.name); //"Andy"----来自原型 console.log(person1.hasOwnProperty("name")); //false
上图展示了上面代码的逻辑关系。
2.原型与in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用。
单独使用时,无论该属性存在于实例中还是原型中,in操作符都会返回true。栗子:
function Person(){ } Person.prototype.name = "Andy"; Person.prototype.name = 28; Person.prototype.name = "Actor"; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); var person2 = new Person(); console.log(person1.hasOwnProperty("name")); //false console.log("name" in person1); //true person1.name = "Tom"; console.log(person1.name); //"Tom"---来自实例 console.log(person1.hasOwnProperty("name")); //true console.log("name" in person1); //true
同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底存在于对象中,还是存在于原型中,如下:
function hasPrototypeProperty(object, name){ return !object.hasOwnProperty(name) && (name in object); }
3.更简单的原型语法
function Person(){ } Person.prototype = { name : "Andy", age : 28, job: "Actor", sayName : function () { alert(this.name); } };
我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象。结果虽然相同,但是constructor属性不再指向Person了。我们这边使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了。如下:
var friend = new Person(); console.log(friend instanceof Object); //true console.log(friend instanceof Person); //true console.log(friend.constructor == Object); //true console.log(friend.constructor == Person); //true
如果constructor的值真的很重要,那么可以这样把它设回适当的值。
function Person(){ } Person.prototype = { constructor : Person, name : "Andy", age : 28, job: "Actor", sayName : function () { alert(this.name); } };
但是以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置成true,默认情况下,原生constructor属性是不可枚举的。可以这样:
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job : "Software Engineer", sayName : function () { alert(this.name); } }; //重设构造函数,只适用于ECMAScript 5 兼容的浏览器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
4.原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反应出来---即使是先创建了实例后修改原型也照样如此,举栗:
var friend = new Person(); Person.prototype.sayHi = function(){ console.log("hi"); }; friend.sayHi(); //"hi"(没有问题!)
即使person实例是在添加新方法之前创建的,但它仍然可以访问这个新方法,原因是实例与原型之间的连接关系是松散的。
当我们调用person.sayHi()时,首先会在实例中搜索名为sayHi的属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在那里的函数。
如果重写整个原型对象,如下:
function Person(){ } var friend = new Person(); Person.prototype = { constructor: Person, name : "Andy", age : 28, job : "Actor", sayName : function () { console.log(this.name); } }; friend.sayName(); //error
以上栗子,我们先创建了Person的一个实例,然后又重写了其原型对象,然后调用friend.sayName()时发生了错误,因为friend指向的原型中不包含以该名字命名的属性,如下图:
5.原生对象的原型
所有原生引用类型,都在其构造函数的原型上定义了方法。
栗如:在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法,如下:
console.log(typeof Array.prototype.sort); //"function" console.log(typeof String.prototype.substring); //"function"
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。
6.原型对象的问题
问题:a.省略传递参数,所有实例在默认情况下都取得相同的属性值
b.共享的本性
function Person(){ } Person.prototype = { constructor: Person, name : "Nicholas", age : 29, job : "Software Engineer", friends : ["Shelby", "Court"], sayName : function () { console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); console.log(person1.friends); //"Shelby,Court,Van" console.log(person2.friends); //"Shelby,Court,Van" console.log(person1.friends === person2.friends); //true
在此,Person.prototype 对象有一个名为friends 的属性,该属性包含一个字符串数组。然后,创建了Person 的两个实例。接着,修改了person1.friends 引用的数组,向数组中添加了一个字符串。由于friends 数组存在于Person.prototype 而非person1 中,所以刚刚提到的修改也会通过person2.friends(与person1.friends 指向同一个数组)反映出来。假如我们的初衷就是像这样在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
参考资料
《javascript高级程序设计(第3版)》第6章 面向对象的程序设计