`
zccst
  • 浏览: 3319272 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

 
阅读更多
作者:zccst

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

function Person(){}

Person.prototype.name = "nick";
Person.prototype.age  = 26;
Person.prototype.job  = 'design';
Person.prototype.sayName = function(){alert(this.name);}

var p1 = new Person();
p1.sayName();//nick

var p2 = new Person();
p2.sayName();//nick

alert(p1.sayName == p2.sayName); //true

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

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

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

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



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


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

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

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

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,如下所示:
function Person(){}

Person.prototype.name = "nick";
Person.prototype.age  = 26;
Person.prototype.job  = 'design';
Person.prototype.sayName = function(){alert(this.name);}

var p1 = new Person();
var p2 = new Person();

p1.name = "Greg";
alert(p1.name); // Greg  来自实例
alert(p2.name); // nick  来自原型

//重点是这里
delete p1.name;
alert(p1.name); // nick  来自原型


使用hasOwnProperty()方法可以检测一个属性是存在与实例中,还是存在于原型中。这个方法(不要忘记他是从Object继承来的)只在给定属性存在于对象实例中,才会返回true。
alert(p1.hasOwnProperty("name"); // false

p1.name = "Greg";
alert(p1.name);//Greg 来自实例
alert(p1.hasOwnProperty("name"); // true

alert(p2.name);//nick 来自原型
alert(p2.hasOwnProperty("name"); // false

delete p1.name;
alert(p1.name);//nick 来自原型
alert(p1.hasOwnProperty("name"); // false


2,原型与in操作符
alert(p1.hasOwnProperty("name"); // false
alert("name" in p1); //true

p1.name = "Greg";
alert(p1.name);//Greg 来自实例
alert(p1.hasOwnProperty("name"); // true
alert("name" in p1); //true

alert(p2.name);//nick 来自原型
alert(p2.hasOwnProperty("name"); // false
alert("name" in p2); //true

delete p1.name;
alert(p1.name);//nick 来自原型
alert(p1.hasOwnProperty("name"); // false
alert("name" in p1); //true

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

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

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

3,更简单的原型语法
前面例子每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重新整个原型对象,如下面的例子所示:
function Person(){
}

Person.prototype = {
    name:"Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function(){
        alert(this.name);
    }
};

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

如果constructor真的很重要,可以像下面这样特意将他设置回适当的值:
function Person(){
}

Person.prototype = {
    constructor:Person,//特意包含了一个constructor属性,并将它的值设置为Person,从而确保通过该属性能够访问到适当的值
    name:"Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function(){
        alert(this.name);
    }
};

var p = new Person();
alert(p instanceof Person);
alert(p instanceof Object);
alert(p.constructor == Person); //false
alert(p.constructor == Object); //true


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


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

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

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

function Person(){}
var p = new Person();
Person.prototype = {
    constructor:Person,
    name:"Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function(){
        alert(this.name);
    }
};
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,原型对象的问题
原型模式也不是没有缺点。首先,它省略了为购置还是传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。

原型中所有属性是被很多实例共享的,这种共享对于还是非常合适。对于那些包含基本值的属性倒也说得过去,毕竟通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而对于包含引用类型的属性来说,问题就比较突出了,看下面的例子:
function Person(){
}

Person.prototype = {
    constructor:Person,
    name:"Nicholas",
    age:29,
    job:"Software Engineer",
    friends:["Shelby","Court"],
    sayName:function(){
	alert(this.name);
    }
}

var p1 = new Person();
var p2 = new Person();

p1.friends.push("Van");
alert(p1.friends);  // "Shelby, Court, Van"
alert(p2.friends);  // 【重要】"Shelby, Court, Van"
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
  • 大小: 47.7 KB
  • 大小: 32.1 KB
分享到:
评论

相关推荐

    Javascript面向对象编程.

    面向对象编程(Object-Oriented Programming,OOP)是编程的一种重要范式,JavaScript也完全支持这一特性,尽管它并非一种传统的静态类型语言。这篇博客文章可能详细讨论了如何在JavaScript中实现面向对象编程。 在...

    JavaScript面向对象编程指南(第2版).rar

    面向对象编程(Object-Oriented Programming,OOP)是JavaScript中的一个重要概念,它允许开发者通过对象来组织代码,提高可读性和可维护性。本指南的第二版深入探讨了JavaScript的面向对象特性,旨在帮助开发者更好...

    面向对象编程基础_python面向对象_python教程_

    面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它基于“对象”的概念,将数据和操作这些数据的方法封装在一起。在Python中,面向对象编程是其核心特性之一,使得Python成为了一种非常适合...

    JavaScript面向对象编程指南

    在JavaScript中,面向对象编程(Object-Oriented Programming,简称OOP)是一种重要的编程范式,它允许开发者创建复杂、可重用的代码结构。本指南将深入探讨JavaScript中的面向对象特性,包括类、对象、继承、封装和...

    JavaScript面向对象编程指南 pdf

    面向对象编程(Object-Oriented Programming,OOP)是JavaScript中的一个重要概念,它允许开发者以更加模块化、可复用的方式组织代码。下面将详细探讨JavaScript面向对象编程的基本原理、特性以及实际应用。 1. **...

    javascript 面向对象编程.pdf javascript 设计模式与开发实践.pdf

    面向对象编程(Object-Oriented Programming, OOP)是JavaScript中的核心概念,而设计模式则是解决常见编程问题的经验总结,对于提升代码质量和可维护性至关重要。这两本书——"JavaScript 面向对象编程.pdf"和...

    JavaScript面向对象编程指南 第2版 高清 带索引书签目录_样章.pdf

    首先,该文件提到的书籍是关于JavaScript面向对象编程的,面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它使用“对象”来设计软件。对象可以包含数据(以字段的形式)以及代码(以方法的...

    JavaScript学习深入—面向对象编程

    ### JavaScript学习深入—面向对象编程 #### 一、JavaScript中的类型概述 ...综上所述,JavaScript通过其灵活的类型系统和原型继承机制支持面向对象编程,这为开发者提供了强大的工具来构建复杂的应用程序。

    2.05 面向对象编程应用.pdf

    面向对象编程应用 面向对象编程是一种编程范式,JavaScript作为一种基于对象的语言,广泛应用于Web开发中。面向对象编程的核心思想是将程序分解为对象和类,对象具有自己的属性和方法,而类则是对象的蓝图。下面是...

    JavaScript面向对象编程

    JavaScript面向对象编程是一种基于原型(Prototype)的编程范式,它是动态类型语言,允许开发者创建具有复杂特性的对象。在JavaScript中,面向对象主要通过构造函数、原型链和闭包来实现。以下是对这一主题的详细...

    JavaScript面向对象编程指南.pdf

    #### 二、JavaScript与面向对象编程 尽管JavaScript最初被设计为一种轻量级的脚本语言,并不是为了支持完整的面向对象编程而设计的,但它仍然可以通过原型链和类的方式实现面向对象编程。 ##### 1. 封装 封装是指...

    javascript面向对象编程的几种模式详解

    面向对象编程(Object-Oriented Programming, OOP)是JavaScript的核心特性之一,它提供了多种模式来创建和操作对象。以下是对标题和描述中提到的几种JavaScript面向对象编程模式的详细解释: 1. **构造函数与字面...

    漫谈设计模式-从面向对象开始

    在本文档中,我们将深入探讨设计模式及其在面向对象编程中的应用。设计模式是软件工程中的一套被广泛认可的最佳实践,它们是一些在特定上下文中反复出现的问题的解决方案。了解和应用设计模式可以帮助开发者写出更加...

    第15章 javascript面向对象与原型

    总的来说,JavaScript中的面向对象编程与传统的基于类的OOP有所不同,它通过原型链和构造函数来实现对象的创建和继承。正确理解并运用这些概念,可以让开发者更有效地利用JavaScript进行面向对象的编程。

    JavaScript面向对象编程案例

    面向对象编程(Object-Oriented Programming,OOP)是一种强大的编程范式,它基于“对象”的概念,允许我们通过封装数据和方法来组织代码。在JavaScript中,面向对象编程并不是原生支持的,但它可以通过模拟类和对象...

    JavaScript 面向对象编程

    JavaScript中实现面向对象编程的核心机制之一是构造函数和原型链。构造函数是一种特殊的函数,用于创建和初始化对象。例如: ```javascript function Person(name, email, website) { this.name = name; this....

    javascript object oriented 面向对象编程初步.docx

    面向对象编程(Object-Oriented Programming,简称 OOP)是一种编程范式,它将程序设计围绕“对象”进行组织。JavaScript 虽然是一种基于原型的语言,但同样支持面向对象编程,并且能够通过多种方式创建对象。 ####...

Global site tag (gtag.js) - Google Analytics