[Object]面向对象编程(高程版)(二)原型模式

作者:zccst

三、原型模式
每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特点类型的所有实例共享的属性和方法。换言之,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。
例如:

Js代码  收藏代码
  1. function Person(){}  
  2.   
  3. Person.prototype.name = "nick";  
  4. Person.prototype.age  = 26;  
  5. Person.prototype.job  = 'design';  
  6. Person.prototype.sayName = function(){alert(this.name);}  
  7.   
  8. var p1 = new Person();  
  9. p1.sayName();//nick  
  10.   
  11. var p2 = new Person();  
  12. p2.sayName();//nick  
  13.   
  14. alert(p1.sayName == p2.sayName); //true  

此时构造函数是空函数,即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话,p1和p2访问的都是同一组属性和同一个sayName()函数。

要理解原型模式的工作原理,必须先理解ECMAScript中原型对象的性质。

1,理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建了一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函 数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。(__proto__,不是所有浏览器都可见)。不过要明确真正 重要的一点,是这个连接存在于实例与构造函数的原型对象之间,而不是实例与构造函数之间。



虽然某些实现中无法访问到内部的__proto__属性,但在所有的实现中都可以通过isPrototypeof()方法来确定对象之间是否存在 这种关系。从本质上讲,如果对象的__proto__指向调用isPrototypeof()方法的对象,那么这个方法就返回true,如下所示:
Js代码  收藏代码
  1. alert(Person.prototype.isPrototypeof(p1));  //true  
  2. alert(Person.prototype.isPrototypeof(p2));  //true  


每当代码读取某个对象的属性时,都会执行一次搜索,先从对象实例本身开始。如果实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找 到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。当调用 p2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

备注:原型最初只包含constructor属性,而该属性也是共享的,因此可以通过对象实例访问。

虽然通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,二该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改 那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过使用delete操作符则可以完全删除实例属 性,从而让我们能够重新访问原型中的属性,如下所示:
Js代码  收藏代码
  1. function Person(){}  
  2.   
  3. Person.prototype.name = "nick";  
  4. Person.prototype.age  = 26;  
  5. Person.prototype.job  = 'design';  
  6. Person.prototype.sayName = function(){alert(this.name);}  
  7.   
  8. var p1 = new Person();  
  9. var p2 = new Person();  
  10.   
  11. p1.name = "Greg";  
  12. alert(p1.name); // Greg  来自实例  
  13. alert(p2.name); // nick  来自原型  
  14.   
  15. //重点是这里  
  16. delete p1.name;  
  17. alert(p1.name); // nick  来自原型  


使用hasOwnProperty()方法可以检测一个属性是存在与实例中,还是存在于原型中。这个方法(不要忘记他是从Object继承来的)只在给定属性存在于对象实例中,才会返回true。
Js代码  收藏代码
  1. alert(p1.hasOwnProperty("name"); // false  
  2.   
  3. p1.name = "Greg";  
  4. alert(p1.name);//Greg 来自实例  
  5. alert(p1.hasOwnProperty("name"); // true  
  6.   
  7. alert(p2.name);//nick 来自原型  
  8. alert(p2.hasOwnProperty("name"); // false  
  9.   
  10. delete p1.name;  
  11. alert(p1.name);//nick 来自原型  
  12. alert(p1.hasOwnProperty("name"); // false  


2,原型与in操作符
Js代码  收藏代码
  1. alert(p1.hasOwnProperty("name"); // false  
  2. alert("name" in p1); //true  
  3.   
  4. p1.name = "Greg";  
  5. alert(p1.name);//Greg 来自实例  
  6. alert(p1.hasOwnProperty("name"); // true  
  7. alert("name" in p1); //true  
  8.   
  9. alert(p2.name);//nick 来自原型  
  10. alert(p2.hasOwnProperty("name"); // false  
  11. alert("name" in p2); //true  
  12.   
  13. delete p1.name;  
  14. alert(p1.name);//nick 来自原型  
  15. alert(p1.hasOwnProperty("name"); // false  
  16. alert("name" in p1); //true  

在以上代码执行过程中,name属性要么是直接在对象上访问到的,要么是通过原型访问到的。因此,调用"name" in p1始终都返回true,无论该属性存在于实例中还是存在于原型中。同时使用hasOwnProperty()和in操作符,就可以确定该属性到底是存在 于对象中,还是存在于原型中,如下所示:
Js代码  收藏代码
  1. function hasPrototypeProperty(object ,name){  
  2.     return !object.hasOwnProperty("name") && (name in object);  
  3. }  

使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。

批注:
可枚举的属性
不可枚举的属性

3,更简单的原型语法
前面例子每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重新整个原型对象,如下面的例子所示:
Js代码  收藏代码
  1. function Person(){  
  2. }  
  3.   
  4. Person.prototype = {  
  5.     name:"Nicholas",  
  6.     age : 29,  
  7.     job : "Software Engineer",  
  8.     sayName : function(){  
  9.         alert(this.name);  
  10.     }  
  11. };  

在上面的代码中,我们将Person.prototype设置为等于一个对象字面量形式创建的新对象,最终结果相同,但有一个例外:constructor属性不再指向Person了。前 面曾经介绍过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们在这里使用的语法,本 质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函 数),不再指向Person函数。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了,如 下所示:
Js代码  收藏代码
  1. var p = new Person();  
  2. alert(p instanceof Person);  
  3. alert(p instanceof Object);  
  4. alert(p.constructor == Person); //false  
  5. alert(p.constructor == Object); //true  

如果constructor真的很重要,可以像下面这样特意将他设置回适当的值:
Js代码  收藏代码
  1. function Person(){  
  2. }  
  3.   
  4. Person.prototype = {  
  5.     constructor:Person,//特意包含了一个constructor属性,并将它的值设置为Person,从而确保通过该属性能够访问到适当的值  
  6.     name:"Nicholas",  
  7.     age : 29,  
  8.     job : "Software Engineer",  
  9.     sayName : function(){  
  10.         alert(this.name);  
  11.     }  
  12. };  

Js代码  收藏代码
  1. var p = new Person();  
  2. alert(p instanceof Person);  
  3. alert(p instanceof Object);  
  4. alert(p.constructor == Person); //false  
  5. alert(p.constructor == Object); //true  


备注:实例的constructor属性与原型对象的constructor属性是指的同一个内容吗?
推理可以得出,应该是指向同一个内容,那就是Person对象自身。

4,原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
Js代码  收藏代码
  1. var p = new Person();  
  2. Person.prototype.sayName = function(){  
  3.     alert("hi");  
  4. }  
  5. p.sayName();//"hi"  

即使p实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间松散的连接关系。对我们调用 person.sayHi()时,首先会在实例中搜索名为sayHi的属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指 针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在哪里的函数。

尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的__proto__指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不是指向构造函数。

Js代码  收藏代码
  1. function Person(){}  
  2. var p = new Person();  
  3. Person.prototype = {  
  4.     constructor:Person,  
  5.     name:"Nicholas",  
  6.     age : 29,  
  7.     job : "Software Engineer",  
  8.     sayName : function(){  
  9.         alert(this.name);  
  10.     }  
  11. };  
  12. p.sayName(); //error  

这个例子中,先创建了Person的一个实例,然后又重写了其原型对象。然后在调用p.sayName()时发生了错误,因为p指向的原型中不包含以改名字命名的属性。如图展示了这个过程的内幕:


从图中可以看出,重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;他们引用的仍然是最初的原型。

5,原生对象的原型
原型对象的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型 (Object,Array,String等)都在其构造函数的原型上定义了方法。例如,在Array.prototype中可以找到sort方法,而在 String.prototype中可以找到substring()方法,如下所示:
alert(typeof Array.prototype.sort);       // function
alert(typeof String.prototype.substring); // function

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。下面的代码就给基本包装类型String添加了一个名为startsWith()的方法:
String.prototype.startsWith = function(text){
    return this.indexOf(text) == 0;
};

var msg = "hello world";
msg.startsWith("hello"); // true

提示:不推荐在产品化的程序中修改原生对象的原型。原因是:在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突。而且这样做有可能会意外地重写原生方法。


6,原型对象的问题
原型模式也不是没有缺点。首先,它省略了为购置还是传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。

原型中所有属性是被很多实例共享的,这种共享对于还是非常合适。对于那些包含基本值的属性倒也说得过去,毕竟通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而对于包含引用类型的属性来说,问题就比较突出了,看下面的例子:
Js代码  收藏代码
  1. function Person(){  
  2. }  
  3.   
  4. Person.prototype = {  
  5.     constructor:Person,  
  6.     name:"Nicholas",  
  7.     age:29,  
  8.     job:"Software Engineer",  
  9.     friends:["Shelby","Court"],  
  10.     sayName:function(){  
  11.     alert(this.name);  
  12.     }  
  13. }  
  14.   
  15. var p1 = new Person();  
  16. var p2 = new Person();  
  17.   
  18. p1.friends.push("Van");  
  19. alert(p1.friends);  // "Shelby, Court, Van"  
  20. alert(p2.friends);  // 【重要】"Shelby, Court, Van"  
  21. alert(p1.friends == p2.friends); // true  

假如我们的初衷就像这样在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部属性。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。


//两个方法
//hasOwnProperty()
//hasPrototypeProperty()
//覆盖原则



//方式二:字面量写法
Person.prototype = {
name : "nick",
age  : 26,
job  : 'design',
sayName : function(){
alert(this.name);
}
};

var p1 = new Person();//后定义对象
var p2 = new Person();
//p1.sayName();
//alert(p1.sayName() == p2.sayName());
//console.log(p1.__proto__);//Object { name="nick", age=26, job="design"}
//console.log(p1);//Object { name="nick", age=26, job="design"}

//alert(Person.prototype.isPrototypeOf(p1));//true
//alert(Person.prototype.isPrototypeOf(p2));//true
//尽管instanceof可以返回正确的结果。


//对比先定义对象与后定义对象的区别
console.log(person);//Person {}
console.log(person.constructor);//Person()
alert(person.constructor == Person); //true
alert(person.constructor == Object); //false

console.log(p1);//Object { name="nick", age=26, job="design"}
console.log(p1.constructor);//Object()
alert(p1.constructor == Person);//false 本质是完全重写了默认的原型对象(prototype)。因此constructor属性也就变成了新对象的constructor属性(指向 Object构造函数),不再指向Person函数。可以通过constructor : Person设回正确的值。
alert(p1.constructor == Object);//true
原文地址:https://www.cnblogs.com/shsgl/p/4289873.html