面向对象编程-基础(工厂、构造函数、原型、混合、动态等模式)

本文参考:js高级程序设计 第三版
这篇文章我啃了大半天写的,是很烦,没有毅力看下去的注定还是不会

(1)、工厂模式:
封装一个函数createPerson,这个函数可以创造一个人对象,包含三个属性和一个方法,然后利用这个函数分别创建了2个对象p1,p2.

function createPerson(name,age,job){
                        var p=new Object();

                        p.name=name;

                        p.age=age;

                        p.job=job;

                        p.showName=function(){

                                alert(this.name);

                        };

                        return p;

                }

                var p1=createPerson(‘jxj1‘,24,‘student‘);

                var p2=createPerson(‘jxj2‘,25,‘teacher‘);

工厂模式下解决了创建多个相似对象的问题,但是却没有解决对象识别问题(不知道这个对象的类型是数组或函数或正则等)
alert(p1 instanceof Object); //true
alert(p1 instanceof createPerson); //false
alert(p2 instanceof Object); //true
alert(p2 instanceof createPerson); //false

(2)、构造函数模式
创建一个构造函数,习惯上构造函数的首字母大写,非构造函数第一个字母小写,同样也包含三个属性和一个方法,然后利用这个函数分别创建了2个实例对象p1,p2.(构造函数也可以当作普通函数来使用,只有使用了new来调用,才作为构造函数使用)
我们先用构造函数从写上面工厂模式下的函数

function Person(name,age,job){
                        this.name=name;

                        this.age=age;

                        this.job=job;

                        this.showName=fucntion(){

                                alert(this.name);

                        };

                };

                var p1=new Person(‘jxj1‘,24,‘student‘);

                var p2=new Person(‘jxj2‘,25,‘teacher‘);

这个构造函数的例子取代了前面的普通函数,二者之间到底有什么区别呢
相对工厂模式构造函数
1、没有显式的创建对象,就是没有在函数里面var p=new Object();
2、直接将属性和方法直接的付给了this对象(理解this的朋友知道,这样的构造函数在没有new新对象时,this指向全局对象window);
3、没有return语句
再次解释构造函数中的this,构造函数的作用就是为了创建对象,这里我们有new 了二个新的对象 p1、p2,此时再调用p1和p2时this就指向了自己,而不是window(不懂得去查看this的作用域)
p1和p2是Person的不同实例,这二个对象都有一个属性constructor(构造函数的属性),该属性指向Person这个构造函数
alert(p1.constructor==Person); //true
alert(p2.constructor==Person); //true

好了,之所以介绍构造函数,还没有说它的优点呢,前面说了工厂方式不能够解决对象的识别问题,那么构造函数就可以识别
alert(p1 instanceof Object); //true
alert(p1 instanceof Person); //true
alert(p2 instanceof Object); //true
alert(p2 instanceof Person); //true
其实说了这么多,构造函数还是不完美的,有缺点,有没有注意到构造函数中有个showName的方法,该方法有个function函数,问题就是出现在这里
p1,p2是Person的二个不同的实例,p1和p2中的showName方法是不一样的(不同实例的同名函数不相等)
alert(p1.showName == p2.showName); //false
换句话说,每次的实例化的构造函数都是下面这样的(只是为了理解,不可以这样写)

function Person(name,age,job){

                        this.name=name;

                        this.age=age;

                        this.job=job;

                        this.showName=new fucntion(){

                                alert(this.name);

                        };

                };

总之,每次实例化时,都产生一个新方法,这就是缺点

好吧,想办法解决:

function Person(name,age,job){
            this.name=name;
            this.age=age;
            this.job=job;
            this.showName=showName;
        };
        fucntion showName(){
                alert(this.name);
            };
        var p1=new creatPerson(‘jxj1‘,24,‘student‘);
        var p2=new creatPerson(‘jxj2‘,25,‘teacher‘);

在这个例子中,我们把showName()函数的定义转移到了构造函数外部。而在构造函数内部,我们将sayName 属性设置成等于全局的sayName 函数。这样一来,由于sayName 包含的是一个指向函数
的指针,因此p1 和p2 对象就共享了在全局作用域中定义的同一个sayName()函数。这样做确实解决了两个函数做同一件事的问题,可是showName就成了全局函数,如果该构造函数有很多方法,呢么会不会疯啊,那么这样的解决办法可以解决,但是不好,好吧继续找方法

(3)、原型
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。。。。接下来先不解释含义了,因为不容易理解,举个例子:

function Person(name,age,job){
            this.prototype.name=‘jxj1‘;
            this.prototype.age=24;
            this.prototype.job=‘student‘;
            this.prototype.showName=fucntion(){
                alert(this.name);
            };
        };
        var p1=new Person();
        p1.showName();//jxj1
        var p2=new Person();
        p2.showName();//jxj1
        alert(p1.showName==p2.showName);//true

通过上面的代码,有没有觉得,工厂模式和构造函数模式的缺点,都不会在这里出现啊,这就是原型的强大之处,好吧,接下来我们来理解原型到底是什么

直接说概念太抽象,对着图说吧
我们用原型写的构造函数,将所有属性和方法都添加到Person的prototype属性中,此时的构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属
性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由(让)所有实例共享的。换句话说,p1 和p2 访问的都是同一组属性和同一个sayName()函数。
看着图。。。。。
只要创建一个新函数(Person),就会为这个函数创建一个prototype的属性,这个属性指向行原型对象(Person protype),而原型对象会获得一个属性constructor,该属性指向原型属性所在的函数指针Person。当调用构造函数创建新实例以后(person1,person2),该实例内部将包含一个指针[[prototype]],指向构造函数的原型对象.可以看出来,person1和person2与构造函数没有直接的关系,它们操作的是构造函数的对象原型(Person protype)。请记住:实例中的指针仅指向原型,而不指向构造函数。
看看下面的二句:
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
这2句表明了,person1和person2内部都有一个指针[[prototype]]指向原型对象(Person.prototype)。
在ECMAScript5中新增加了一个方法Object.getPrototypeof(),该方法可以返回实例对象(person1,person2)中的指针值
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"

下面是是书上对原型中属性和方法访问的过程还是很容易理解的,所以我就copy了:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。
也就是说,在我们调用person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例person1 有sayName 属性吗?”答:“没有。”然后,它继续搜索,再问:“person1 的原型有sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

看了上面的原理,下面我们举个例子:

function Person(){
        }
        Person.prototype.name = "Nicholas";
        Person.prototype.age = 29;
        Person.prototype.job = "Software Engineer";
        Person.prototype.sayName = function(){
        alert(this.name);
        };
        var person1 = new Person();
        var person2 = new Person();
        person1.name = "Greg";
        alert(person1.name); //"Greg"——来自实例
        alert(person2.name); //"Nicholas"——来自原型

该例子可以看出,当代码在读取对象属性时,先查找本身自己对象实例person1,当实例中找到了name,就不会再找对象原型中的相应属性了;若是没有找到,就继续搜索原型对象。。。。
换句话说,当实例中存在和原型对象中同名的属性,那么会优先选择实例中的属性和属性值。。。。
这里要想person1.name显示Nicholas,就需要使用delete.person1.name删除实例中的同名属性才可以。。。。。

接下来问题来了,有时候我们需要检测一个属性到底是在实例中还是在原型对象中,我们该怎么办
hasOwnProperty()是从Object继承来的,hasOwnProperty()方法可以检测到一个属性是存在于原型中还是对象实例中,只有当存在对象对象实例中返回true(其实存在原型中,或是其它非对象实例中都会返回false)
in :当通过对象能够访问指定的属性时就返回true(只要能访问,不管存在于实例中还是原型中)

可以封装一个函数来确定,属性到底是存在于对象中还是原型中:
                function hasPrototypePropery(obj,name){
                        return !obj.hasOwnProperty(name)&&(name in obj);
                };
有些人肯定会疑惑,hasprototypepropery()方法就足够了啊,干么还要in呢?我一开始也是这么疑惑,逆向思维就知道了。。。
假设现在我要确定一个属性是存在于原型对象中的,而hasPrototypePropery()只能确定存在对象实例中和非对象实例中,所以只有当在非实例对象中 !obj.hasOwnProperty(name)且能够通过对象访问到该属性时,才能确定一定是在对象原型中,,,,,(不懂得慢慢想)

前面我们用原型的方法解决了工厂和构造函数的缺点,但是,原型写的有点负责代码又多,有那么多重复的对象.prototype所以我们要改写一下了

function Person(){
        };
        Person.prototype(){
            name:‘jxj1‘,
            age:24,
            job:‘student‘,
            showName:function(){
                alert(this.name);
            }
        };

这种通过对象字面量的方式来写原型对象和原来的写法产生的效果是一样的,但是有一个变了,前面说过每创建一个函数,这个函数就会产生一个prototype属性,指向对象原型,而对象原型中
的constructor属性会指向创建的那个函数,现在constructor属性不再指向Person了,改编成指向Object构造函数
如果你需要constructor属性很重要,可以改写一下:

function Person(){
        };
        Person.prototype(){
            constructor:Person,
            name:‘jxj1‘,
            age:24,
            job:‘student‘,
            showName:function(){
                alert(this.name);
            }
        };

原生模式的重要性并仅仅体现在上面的哪些自定义类型方面,就连原生的引用类都是采用这种模式创建的。原生引用类型(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!";
alert(msg.startsWith("Hello")); //true

这里新定义的startsWith()方法会在传入的文本位于一个字符串开始时返回true。既然方法被
添加给了String.prototype,那么当前环境中的所有字符串就都可以调用它。由于msg 是字符串,
而且后台会调用String 基本包装函数创建这个字符串,因此通过msg 就可以调用startsWith()方法。

当然,非特殊情况下,不推荐在程序中修改原生对象的原型,修改的方法可以会在其它方法中产生命名冲突

原型的缺点:
对比你下原型和构造函数发现了,原型模式省略了初始化参数这一环节,结果导致所有的实例都共享取得相同的属性值,其实我们并不像看到这样的结果,这还不是最大的问题。
原生最大的问题就是由共享的本性所导致的,原型中所有属性被实例共享,这没有什么不妥,况且,实例中同名的属性优先级更高。然而,对于包含引用类型值的属性来说,问题就大了。
例如:

function Person(){
        }
        Person.prototype = {
            constructor: Person,
            name : "Nicholas",
            age : 29,
            job : "Software Engineer",
            friends : ["Shelby", "Court"],
            sayName : function () {
            alert(this.name);
        }
        };
        var person1 = new Person();
        var person2 = new Person();
        person1.friends.push("Van");
        alert(person1.friends); //"Shelby,Court,Van"
        alert(person2.friends); //"Shelby,Court,Van"
        alert(person1.friends === person2.friends); //true

当实例person1对象修改的属性中包含引用类型(比如数组)的值,会反映到person2对象中,我既然创建的是二个实例,怎么会想她们共享一个数组呢,oh  。。。ON

现在问题又来了,原型也有缺点,继续想办法吧:
(4)、构造函数与原型的混合模式
我们需要将二者的优点都结合起来,抛弃她们的缺点,分析一下吧
首先,构造函数中的方法每次实例化都会是不一样的,但是原型可以改正这个缺点,所以用原型模式来定义方法
其次,当其中一个实例改变原型中引用类型的值,同时另外一个实例在原型中的相应值也会跟着改变,但是构造函数可以改掉这个缺点,所以,用构造函数模式定义引用类型值的属性
总结,构造函数用于定义实例的属性(包括基本数据类型和引用数据类型),而原型模式用于定义方法和共享的属性。
例子:这个例子也就是前面我们举的例子

function Person(name,age,job){
                        this.name=name;
                        this.age=age;
                        this.job=job;
                        this.friend=[‘a1‘,‘a2‘];
                };
            Person.prototype={
                    constructor:Person,
                    showName:function(){
                            alert(this.name);
                    }
            };
            var p1=new Person(‘jxj1‘,24,‘student‘);
            var p2=new Person(‘jxj2‘,25,‘student‘);
            p1.friend.push(‘a3‘);
            alert(p1.friend);//‘a1,a2,a3‘
            alert(p2.friend);//‘a1,a2‘
            alert(p1.friend==p2.friend);//false
            alert(p1.showName==p2.showName);//true

从例子中可以看出来,现在引用类型的数据值没有共享,函数方法变成了共享,所以好像是没有问题了。。。。。
(5)、动态原型模式
动态原型模式,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过
检查某个应该存在的方法是否有效,来决定是否需要初始化原型。来看一个例子:

function Person(name, age, job){
            //属性
            this.name = name;
            this.age = age;
            this.job = job;
            //方法
            if (typeof this.sayName != "function"){
                Person.prototype.sayName = function(){
                alert(this.name);
            };
            }
        }
        var friend = new Person("Nicholas", 29, "Software Engineer");
        friend.sayName();

这里只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。
不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。其中,if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆
if 语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用instanceof 操作符确定它的类型。
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

时间: 2024-09-29 08:24:18

面向对象编程-基础(工厂、构造函数、原型、混合、动态等模式)的相关文章

[.net 面向对象编程基础] (18) 泛型

[.net 面向对象编程基础] (18) 泛型 上一节我们说到了两种数据类型数组和集合,数组是指包含同一类型的多个元素,集合是指.net中提供数据存储和检索的专用类. 数组使用前需要先指定大小,并且检索不方便.集合检索和声明方便,但是存在类型安全问题,本来使一个类型安全的C#变得不安全了. 集合为了解决数组预设大小的问题,采取了一种自动扩容的办法,这样当大小不够时,他就创建一个新的存储区域,把原有集合的元素复制过来.如此又对性能上也是有很大的影响. 上节我们说到解决这些缺陷的方法,那就是.NET

[.net 面向对象编程基础] (5) 基础中的基础——变量和常量

[.net面向对象编程基础]  (5) 基础中的基础——变量和常量 1.常量:在编译时其值能够确定,并且程序运行过程中值不发生变化的量. 通俗来说,就是定义一个不能改变值的量.既然不能变动值,那就必须在定义的时候初始化. 示例: 语法: const 类型名 常量名=常量表达式; 例子: const string  cat="hello cat"; 2.变量:具有变量名和值的对象 变量就是把数据暂存于某处内存中,以备调用. 示例: 声明:[访问修饰符] 类型 变量名[=初值][变量名=[

[.net 面向对象编程基础] (14) 重构

[.net 面向对象编程基础] (14) 重构 通过面向对象三大特性:封装.继承.多态的学习,可以说我们已经掌握了面向对象的核心.接下来的学习就是如何让我们的代码更优雅.更高效.更易读.更易维护.当然了,这也是从一个普通程序员到一个高级程序员的必由之路.就看病一样,普通医生只能治标,高级医生不但看好病,还能除病根. 1.什么时重构? 重构(Refactoring)就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量.性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性.

[.net 面向对象编程基础] (7) 基础中的基础——修饰符

[.net 面向对象编程基础] (7) 基础中的基础——修饰符 在进入C#面向对象核心之前,我们需要先对修饰符有所了解,其实我们在前面说到变量和常量的时候,已经使用了修饰符,并且说明了变量和常量的修改符. 首先 修饰符是什么? 修饰符用于限定类型以及类型成员的申明 从定义可以看出按功能分为两类:访问修饰符和声明修饰符 1.访问修饰符 (5个) 访问修饰符 说明 public 公有访问.不受任何限制. private 私有访问.只限于本类成员访问,子类,实例都不能访问. protected 保护访

[.net 面向对象编程基础] (21) 委托

[.net 面向对象编程基础] (20)  委托 上节在讲到LINQ的匿名方法中说到了委托,不过比较简单,没了解清楚没关系,这节中会详细说明委托. 1.什么是委托? 学习委托,我想说,学会了就感觉简单的不能再简单了,没学过或都不愿了解的人,看着就头大,其实很简单.委托在.net面向对象编程和学习设计模式中非常重要,是学习.net面向对象编程必须要学会并掌握的. 委托从字面上理解,就是把做一些事情交给别人来帮忙完成.在C#中也可以这样理解,委托就是动态调用方法.这样说明,就很好理解了. 平时我们会

[.net 面向对象编程基础] (17) 数组与集合

[.net 面向对象编程基础] (17) 数组与集合 学习了前面的C#三大特性,及接口,抽象类这些相对抽象的东西以后,是不是有点很累的感觉.具体的东西总是容易理解,因此我们在介绍前面抽象概念的时候,总是举的是具体的实例以加深理解. 本节内容相当具体,学起来也相当轻松. 1.数组 1.1 什么是数组? 数组是一种数据结构,包含同一个类型的多个元素. 1.2数组的初始化 string[] mystringArray; 类型+方框号 数组名 1.3数组初始化 我们知道数组是引用类型,所以需要给他分配堆

[.net 面向对象编程基础] (16) 接口

[.net 面向对象编程基础] (16) 接口 关于“接口”一词,跟我们平常看到的电脑的硬件“接口”意义上是差不多的.拿一台电脑来说,我们从外面,可以看到他的USB接口,COM接口等,那么这些接口的目的一就是让第三方厂商生产的外设都有相同的标准,也是提供一个对外通信或操作的入口. 只是C#的接口除了以上特点之外,还具有一种类似于模板的功能,我们定义一组接口,就像是一个模板.这点和抽象类不同,抽象类是先有子类或都子类的概念,从中抽象出一个类.而接口更像是我们要设计一台机器,先把这台机器对外的功能接

[.net 面向对象编程基础] (15) 抽象类

[.net 面向对象编程基础] (15) 抽象类 前面我们已经使用到了虚方法(使用 Virtual修饰符)和抽象类及抽象方法(使用abstract修饰符)我们在多态一节中说到要实现类成员的重写必须定义为一个虚方法或抽象方法.这节单独把抽象类提出来,是因为抽象是.net实现面向对象编程重要的重要思想,定义抽象类就象一个模板一个提纲,它可以理解为中央的指导思想,我们通过派生类去具体实现它.由此可见,抽象类本身没有任何作用,也不能被实例化,因为他本身就不具有具体的事物.比如上节的动物类的例 子,我们实

[.net 面向对象编程基础] (9) 类的成员(字段、属性、方法)

[.net 面向对象编程基础] (9) 类的成员(字段.属性.方法) 前面定义的Person的类,里面的成员包括:字段.属性.方法.事件等,此外,前面说的嵌套类也是类的成员. a.类的成员为分:静态成员(static)和非静态成员 b.静态成员用static标识,不标识则默认为非静态成员 c.静态成员属于类所有,动态成员则属于实例所有,即对象 d.静态成员为类所有实例共享,无论类有多少实例或副本,静态成员只占用存中一块区域.非静态成员则在类的每个实例,都创建一个内存域. 下面主要说明一下类的主要

[.net 面向对象编程基础] (11) 面向对象三大特性——封装

[.net 面向对象编程基础] (11) 面向对象三大特性——封装 我们的课题是面向对象编程,前面主要介绍了面向对象的基础知识,而从这里开始才是面向对象的核心部分,即 面向对象的三大特性:封装.继承.多态. 1.封装概念 封装:每个对象都包含有它能进行操作的所有信息,这个特性称为封装.这样的方法包含在类中,通过类的实例来实现. 2.封装的优点 A.良好的封装能够减少耦合(比如实现界面和逻辑分离) B.可以让类对外接口不变,内部可以实现自由的修改 C.类具有清晰的对外接口,使用者只需调用,无需关心