`
zccst
  • 浏览: 3292602 次
  • 性别: 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 面向对象编程

    但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。 那么,如果我们要把"属性"(property)和"方法"(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么...

    JavaScript面向对象编程指南

     面向对象编程的基础知识及其在JavaScript中的运用;  数据类型、操作符以及流程控制语句;  函数、闭包、对象和原型等概念,以代码重用为目的的继承模式;  BOM 、DOM、浏览器事件、AJAX和JSON;  如何实现...

    设计模式:可复用面向对象软件的基础--详细书签版

    本书假设你至少已经比较熟悉一种面向对象编程语言,并且有一定的面向对象设计经验。当我们提及“类型”和“多态”,或“接口”继承与“实现”继承的关系时,你应该对这些概念了然于胸,而不必迫不及待地翻阅手头的...

    讲解JavaScript的面向对象的编程

    本人在带学生使用EXT框架时,我发现学生阅读Ext的sample代码有问题,特别是对JavaScript的面向对象编程的书写方式不熟悉,于是,写了四个sample来说明它现代JS编程的对类的定义方式、类继承的方式,以及怎样发展到...

    JS面向对象编程基础篇(二) 封装操作实例详解

    本文实例讲述了JS面向对象编程封装操作。分享给大家供大家参考,具体如下: Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,...

    protolib:使用原型系统进行面向对象编程的 Javascript 库

    原型库protolib是一个 Javascript 库,用于使用原型系统进行面向对象编程。 它基于的文章 。安装新产品管理要将protolib安装到项目中,请在项目的根目录中输入以下命令。 npm install protolib --save鲍尔要使用将...

    学习Javascript面向对象编程之封装

    但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。 那么,如果我们要把”属性”(property)和”方法”(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该...

    matlab购物车代码-ObjectOrientedProgrammingBasics:面向对象的编程基础〜OOP的开始

    matlab购物车代码面向对象编程基础 面向对象的编程基础〜OOP的开始 面向对象编程的概念 面向对象编程(OOP)是一种基于“对象”概念的编程范例,它可以包含字段(通常...支持面向对象编程的语言通常以类或原型的形式

    lua-object:Lua 的基于原型的 OOP 库

    实际上,就基于原型的面向对象编程而言,下面的“类”是对象。 -- require object module local object = require ( " object " ) -- define simple class local HelloClass = object: extend ( function ( class...

    深入理解JavaScript系列.chm

    18.面向对象编程之ECMAScript实现 19.求值策略 20.《你真懂JavaScript吗?》答案详解 21.S.O.L.I.D五大原则之接口隔离原则ISP 22.S.O.L.I.D五大原则之依赖倒置原则DIP 23.JavaScript与DOM(上)——也适用于新手 24....

    python基础教程之面向对象的一些概念

    我们这么早切入面向对象编程的原因是,Python的整个概念是基于对象的。了解OOP是进一步学习Python的关键。 下面是对面向对象的一种理解,基于分类。 相近对象,归为类 在人类认知中,会根据属性相近把东西归类,并且...

    JavaScript面向对象的程序设计(犯迷糊的小羊)

    “面向对象编程”(Object Oriented Programming,缩写为OOP)本身是一种编程的思维模式,它把世界的一切看作是对象的集合,世界的运转就是靠一个个对象分工、合作的结果,体现一切皆“对象”思想; 而在程序设计...

    二十三种设计模式【PDF版】

    在真正可复用的面向对象编程中,GoF 的《设计模式》为我们提供了一套可复用的面向对象技术,再配合 Refactoring(重构方法), 所以很少存在简单重复的工作,加上Java 代码的精炼性和面向对象纯洁性(设计模式是 java 的...

    Delphi 深度探索(第二版)〖含随书光盘源代码〗

    5.4 面向对象的界面复用技术 5.5 vcl中的容器类 5.6 所见即所得的delphiweb开发利器--intraweb开发指南 5.7 基于bold的uml模型驱动的数据库应用开发 5.7.1 object rdbms mapping原理简介 5.7.2 使用...

    javascript基于原型链的继承及call和apply函数用法分析

    1. 继承是面向对象编程语言的一个重要特性,比如Java中,通过extend可以实现多继承,但是JavaScript中的继承方式跟JAVA中有很大的区别,JS中通过原型链的方式实现继承。 (1)对象的原型:因为JS中,函数也是对象,...

    深入理解JavaScript系列

    深入理解JavaScript系列(18):面向对象编程之ECMAScript实现 深入理解JavaScript系列(19):求值策略 深入理解JavaScript系列(20):《你真懂JavaScript吗?》答案详解 深入理解JavaScript系列(21):S.O.L....

    深入理解JavaScript系列(.chm)

    深入理解JavaScript系列(18):面向对象编程之ECMAScript实现 深入理解JavaScript系列(19):求值策略 深入理解JavaScript系列(20):《你真懂JavaScript吗 》答案详解 深入理解JavaScript系列(21):S O L I...

    VanillaJS-Object-Oriented-Notes:有关JS ES6的面向对象的注释

    教程3 创建原型并向对象添加其他功能。教程4 使用Object.create()方法和prototype.constructor参数进行原型制作。教程5 Object.create()方法及其用法。Tutorial6,Tutorial7 [Javascript ES 6 +] 定义类,构造...

    Java基础知识点总结.docx

    五、 封装(面向对象特征之一)★★★★ 23 六、 继承(面向对象特征之一)★★★★ 25 七、 接口(面向对象特征之一)★★★★ 28 八、 多态(面向对象特征之一)★★★★ 30 九、 java.lang.Object 31 十、 异常★...

    JavaScript基础和实例代码

    4.1 面向对象编程与基于对象编程 4.1.1 什么是对象 4.1.2 面向对象编程 4.1.3 面向对象编程:继承 4.1.4 面向对象编程:封装 4.1.5 面向对象编程:多态 4.1.6 基于对象编程 4.2 JavaScript对象的生成 4.2.1 HTML文档...

Global site tag (gtag.js) - Google Analytics