本文参考: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 操作符确定它的类型。
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。