练习中使用的是IE11,如果有错误之处,还请各位朋友多多指教。本文关于原型难以描述,故多用代码展示
原型是JS中一个很重要的概念,也是JS中一个难点,语言上难以描述,原型对象的属性和方法叫做原型属性和原型方法,构造函数中的属性和方法叫做实例属性和实例方法,它们的区别就是:对于一个对象的多个实例之间,它们的实例属性和实例方法是各不一样的,前面的面向对象中已经证明,而他们的原型属性和原型方法是一模一样的,完全相等的
构造函数方式声明原型对象:
1 //构造函数定义的成员变量时实例成员 2 function Box(user,age){ 3 this.user=user; //实例属性 4 this.age=age; 5 this.run=function(){ //实例方法 6 return this.user+" "+this.age+" "+"运行中..."; 7 } 8 } 9 10 //原型声明,构造函数中什么也不写,然后通过prototype对象来添加属性和方法,调用都是一样的 11 function Box1(){}; 12 Box1.prototype.user='abc'; //通过prototype定义的叫做 原型属性 13 Box1.prototype.age=22; //下面这个方法叫做原型方法 14 Box1.prototype.run=function(){ return this.user+" "+this.age+" "+"运行中..."; }; 15 16 var box=new Box1(); 17 alert(box.user); // abc 18 alert(box.age); // 22 19 alert(box.run()); // abc 22 运行中... 20 21 //声明了两个对象 从下面的结果可以知道,这两个对象的引用不相等,但是他们中间的方法和属性是完全相等的 包括引用 22 var box1=new Box(); 23 var box2=new Box(); 24 alert(box1.user); //abc 25 alert(box2.user); //abc 26 alert(box1.age); //22 27 alert(box2.age); //22 28 29 //如果是实例方法,不同的实例化,他们的方法的引用地址是不一样的,是唯一的 30 //但是原型对象,不同的实例化,他们的方法的引用地址也是一样的,共享的,大家都一样 31 alert(box1.run==box2.run); // true 32 alert(box1==box2); //false
1、 原型的作用主要作用
用来共享一些属性和方法,我们每创建一个函数,都会自动的创建一个原型对象,原型对象是由函数下面的一个属性[__proto__]来指向的,这个属性是一个指针,它指向了原型对象的constructor属性,我们可以通过这两个属性就可以访问原型对象中的属性和方法了。
constructor是一个构造属性,是可以获取构造函数的本身的,它的作用其实就是被原型指针[__proto__]定位,然后获取到构造函数的本身
1 alert(Box.prototype); //访问方法的属性prototype 2 alert(box1.prototype); //undefined 这个属性是一个对象,是访问不到的 3 alert(box1.__proto__); //object Object 低版本的IE可能打印不出来 4 alert(box1.constructor); //function Box(){}; //构造属性 5 alert(Box.constructor); // function Function(){...}; Box 本身就是一个Function类型 6 //alert(box1.prototype.constructor) //error
2、isPrototypeOf() 方法
判断一个实例对象是否是指向了该构造函数的原型对象,可以用 isPrototypeOf() 方法来判断,如果指向了返回为true,没有则返回为false,一切对象都是继承自Object对象
1 alert(Box.prototype.isPrototypeOf(box1)); //true 只要是实例化的,都指向了原型对象 2 alert(Box.prototype.isPrototypeOf(box2)); //true 3 alert(Object.prototype.isPrototypeOf(box1));//true 因为一切对象都是 Object 类型的,故指向了 4 var box=new Object(); 5 alert(Box.prototype.isPrototypeOf(box)); // false box对象是Object类型的对象,Box其实类似于是继承自Object类型
3、原型模型的执行流程:遵循JS中的就近原则
先查找构造函数实例里面的方法和属性,即先查找实例属性,如果有,立即返回值或者执行对应的方法
如果实例属性或者实例方法中没有,则去原型对象中查找相应的属性和方法,有就返回或执行方法,如果没有就返回undefined或者报错
1 var box1=new Box(); 2 var box2=new Box(); 3 box1.name='kkk'; //给对象box1添加一个实例属性, 4 alert(box1.name); //访问的是box1的实例属性,box2是访问不到的,因为原型属性中也没有 5 alert(box2.name); 6 box1.user='jjj'; //其实是实例属性,并没有重写原型属性的值 7 alert(box1.__proto__.user);//abc 访问原型属性中的值 8 alert(box1.user);//jjj 访问的是实例属性中的值 9 alert(box2.user);//abc box2 中不存在实例属性 user 就返回的是原型属性,它访问不到box1中的实例属性,因为他们之间共享的只是原型属性和方法
4、属性的删除和修改
可以通过 delete 关键字来删除实例属性和原型属性,删除和修改原型属性可以通过两种方式:(1) 通过实例对象的指针修改:box1.__proto__.age; (2) 定义原型属性一样用构造函数修改 prototype: Box.prototype.age;
构造函数中是改了就生效,不管何时声明的对象,而在后面的字面量形式中只有在声明实例对象之前该才有效(详细的见后面字面量形式创建中)
1 delete box1.user; //删除对象box1中的实例属性 2 alert(box1.user); //abc 因为前面删除了实例属性中的user属性,返回的就是原型属性中的user属性 3 //delete box1.__proto__.age; //通过这种方式可以删除原型对象中的属性,但是别这样弄,牵一发而动全身 4 //delete Box.prototype.age; //也是删除了原型对象中的属性 age 5 alert(box2.age); //undefined 因为前面删除了原型属性中的age属性 6 box1.__proto__.age=33; //修改了原型属性中的值 7 alert(box2.age); //33 8 Box.prototype.age=44; //修改了原型属性中的值 9 alert(box2.age); //44
5、hasProperty() 方法和 in 操作符
判断某个对象是否拥有某个实例属性,可以通过 hasOwnProperty() 方法来测试,有就返回true,否则返回false
in 操作符可以判断对象中是否包含某个属性,不管这个属性是原型属性还是实例属性,包含则返回true
可以通过 hasOwnProperty() 方法和 in 操作符 共同来判断某个对象是否包含某个原型属性
var box1 = new Box(); var box2 = new Box(); 1 box1.name='jjj'; 2 alert(box1.hasOwnProperty('name')); //true 3 alert(box2.hasOwnProperty('name')); //false box2 并没有实例属性name 4 alert(box1.hasOwnProperty('user')); //false user 属性是原型属性,不是实例属性 5 6 alert('name' in box1); //false 7 alert('user' in box1); //true 原型属性 8 box1.name='jjj'; 9 alert('name' in box1); //true 实例属性 10 11 //通过 hasOwnProperty() 方法和 in 操作符 共同来判断某个对象是否包含某个原型属性 12 function checkPropo(object,element){ //判断的是在某个对象中的属性,故要将对象和属性传递过来 13 if(!object.hasOwnProperty(element)){ //先判断是否存在实例属性,如果存在就返回false 14 if(element in object){ //如果存在就返回true 15 return true; 16 }else{ 17 return false; 18 } 19 }else{ 20 return false; 21 } 22 //上面的代码可以用一个表达式来表示:return !object.hasOwnProperty(element)&& (element in object); 23 } 24 25 alert(checkPropo(box1,'name')); //false 26 box1.name='name'; 27 alert(checkPropo(box1,'name')); //false 虽然有属性 name,但是这是一个实例属性 28 alert(checkPropo(box1,'user')); //true //原型属性 user 是存在的
字面量形式创建原型对象:
1 function Box(){}; 2 Box.prototype={ // 通过字面方法来创建原型属性和方法,这样有点封装的感觉 3 user:'abc', 4 age:123, 5 run:function(){return this.user+" "+this.age+" 运行中...";} 6 } 7 8 //创建两个对象 9 var box1=new Box(); 10 var box2=new Box(); 11 //运行结果是一样的 12 alert(box1.run()); //abc 123 运行中... 13 alert(box2.run()); //abc 123 运行中... 14 alert(box1.run==box2.run); //true
6、字面量方式创建原型对象注意的问题一:构造属性的指向
字面量创建的方式 用constructor 属性指向的是Object对象,而不是实例本身,但是构造函数方式创建的这相反;
如果想让constructor指向实例[Box],可以采用强制指向的方式
1 var box1 = new Box(); 2 3 alert(box1.constructor); //function Object() { [native code] } 4 alert(box1.constructor == Box); //false 5 alert(box1.constructor == Object); //true 6 alert(box1 instanceof Box); //true 7 alert(box1 instanceof Object); //true 8 alert(Box.constructor); //function Function... 9 alert(Box.prototype); //object Object //使用构造函数名(对象名)访问prototype 10 alert(box1.__proto__); //object Object //使用对象实例访问prototype的指针 11 12 // 如果想让constructor指向实例[Box],可以采用强制指向的方式 13 function Box(){}; 14 Box.prototype={ 15 constructor:Box, //强制原型对象来指向Box 16 user:'abc', 17 age:123, 18 run:function(){return this.user+" "+this.age+" 运行中...";} 19 } 20 21 //创建两个对象 22 var box1=new Box(); 23 var box2=new Box(); 24 25 //强制constructor指向Box的时候,返回结果如下 26 alert(box1.constructor == Box); //true 27 alert(box1.constructor == Object); //false
之所以出现上面的原因是因为:通过Box.prototype={..};方式创建的时候,都创建一个新的对象,而每次创建一个函数,都会同时创建它自己的prototype,那么这个新的对象也就会自动获取它自己的constructor属性,这样新对象的constructor属性重写了Box实例的constructor属性,因此会执行新的对象,而这个新的对象又没有指定构造函数,故默认的就是Object
7、字面量形式创建原型对象的问题二:原型属性的重写
原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型,故应该注意避免此种情况的发生
1 var box2=new Box(); 2 Box.prototype={ //字面量方式不存在修改原型属性,直接是覆盖掉原来的,因为每次都是创建一个新的对象 3 user:444 //这里不会保留之前原型的任何信息 4 //把原来的原型对象和构造函数对象之间的关系给切断了 5 } 6 //字面量方式创建,只要声明了,以后随便怎么重写原型对象,已经创建的实例对象的值不会改变 7 var box1=new Box(); 8 9 alert(box1.user); // 444 重写中赋值为444 10 alert(box1.age); //undefined 因为第二次重写中没有这个属性 11 12 alert(box2.age);//123 因为在对象的定义是发生在用字面量形式重写原型属性之前,故以后的原型属性的修改和box2无关
9、通过原型模式扩展类型的方法
原型对象不仅仅可以在自定义对象的情况下使用, 而 ECMAScript 内置的引用类型都可以使用这种方式,并且内置的引用类型本身也使用了原型。
通过prototype来添加方法,访问的时候要用这种类型的变量点这个方法,和访问系统内置的方法是一样的,但是最好不要用这种方式来添加方法,因为可能会存在命名冲突的问题,特别是在代码量大的时候,容易照成命名冲突问题。
1 String.prototype.addString=function(s){ //传递一个参数过来, 2 return '【' + s + '】'; 3 } 4 5 alert('abc'.addString('111'));// 【111】 6 7 String.prototype.addString=function(){ //可以不进行传参,通过this来代表当前调用这个方法 的字符串 8 return this+"被添加了!"; 9 } 10 11 alert("abcd".addString()); // abcd被添加了!
原型模式创建对象的缺点以及采用的方式:
原型模式创建对象也有自己的缺点, 它省略了构造函数传参初始化这一过程, 带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。
原型中所有的属性都能被很多实例共享,共享对于函数非常适合,对于包含基本值的属性页还可以,但是如果属性包含引用类型就一定存在问题,见代码中
1 function Box(){}; 2 Box.prototype={ 3 constructor:Box, 4 user:'abc', 5 age:22, 6 family:['哥哥','姐姐','妹妹'], 7 run:function(){ 8 return this.user+" "+this.age+" 运行中..."; 9 } 10 } 11 12 //var box=new Box(123); //缺点之一就是不能够传递参数 13 14 var box1=new Box(); //缺点二就是原型的共享性,这也是它最大的优点 15 alert(box1.family); //哥哥,姐姐,妹妹 16 box1.family.push('弟弟'); //在第一个实例后修改了引用类型,保持了共享,但本质上不希望它共享 17 alert(box1.family); //哥哥,姐姐,妹妹,弟弟 18 19 var box2=new Box(); 20 alert(box2.family); //哥哥,姐姐,妹妹,弟弟 共享了box1引用类型添加后的原型
11、组合构造函数 + 原型模式
这种方式能够很好的解决传参和引用共享的问题,是创建对象比较好的方法
1 function Box(user,age){ //构造函数,这里面声明一些会变的属性和引用类型的属性 2 this.user=user; 3 this.age=age; 4 this.family=['哥哥','姐姐','妹妹']; 5 } 6 7 Box.prototype={ //原型模式 8 constructor:Box, 9 run:function(){ 10 return this.user+" "+this.age+" 运行中..."; 11 } 12 } 13 14 //下面可以看出通过构造函数总写一些自己会变的属性等,即使值改变了也不会被共享出去 15 var box1=new Box('abc',22); 16 alert(box1.run()); // abc 22 运行中... 17 alert(box1.family); //哥哥,姐姐,妹妹 18 box1.family.push("弟弟"); 19 alert(box1.family); //哥哥,姐姐,妹妹,弟弟 20 21 var box2=new Box('jack',33); 22 alert(box2.family); //哥哥,姐姐,妹妹 并没有共享对象box1中的引用类型 23 alert(box2.run()); //jack 33 运行中... 和box1 中的输出结果是不一样的
11、动态原型模型
将构造函数和原型模型封装在一起,也能够解决共享的问题,但是要注意两个问题,一是资源的浪费,还有就是要注意,不可以再使用字面量的方式重写原型,因为会切断实例和新原型之间的联系
在下面的代码中,当第一次调用构造函数时,run()方法发现不存在,然后初始化原型。当第二次调用, 就不会初始化, 并且第二次创建新对象, 原型也不会再初始化了。 这样及得到了封装, 又实现了原型方法共享,并且属性都保持独立
function Box(user,age){ this.user=user; this.age=age; this.family=['哥哥','姐姐','妹妹']; if(typeof this.run != 'function'){//判断this.run是否存在,因为执行一次后类型返回值就为为function alert("原型初始化开始");//没有判断之前,每new一次Box都会执行一次, Box.prototype.clss='person'; Box.prototype.run=function(){ return this.user+" "+this.age+" 运行中..."; }; alert("原型初始化结束"); } } var box1=new Box('abc',22); var box2=new Box('jack',33);
//也可以判断原型中任意一个属性的类型返回值,是否等于undefined,若等于原型就还没有创建 //因为原型中的属性一般都是一开始就有特定的值的,除非故意赋值为undefined[这样没意义了] function Box(user,age){ this.user=user; this.age=age; this.family=["哥哥","姐姐","妹妹"]; if(typeof this.clss == "undefined"){ alert("原型初始化开始");//没有判断之前,每new一次Box都会执行一次, Box.prototype.clss="person"; Box.prototype.run=function(){ return this.user+" "+this.age+" 运行中..."; }; alert("原型初始化结束"); } } var box1=new Box('abc',22); var box2=new Box('jack',33);
12、寄生构造函数
寄生构造函数,其实就是工厂模式+构造函数模式。这种模式比较通用,但不能确定对象关系,所以,在可以使用之前所说的模式时,不建议使用此模式;在什么情况下使用寄生构造函数比较合适呢?假设要创建一个具有额外方法的引用类型。由于之前说明不建议直接 String.prototype.addstring,可以通过寄生构造的方式添加
function myString(string){ var str = new String(string); str.addString = function(){ return this + ",被添加了!";//this 指的是对象str下的值string } return str; } var box = new myString("abcd"); alert(box.addString());
13、稳妥构造函数
在一些安全的环境中, 比如禁止使用 this 和 new, 这里的 this 是构造函数里不使用 this,这里的 new 是在外部实例化构造函数时不使用 new。这种创建方式叫做稳妥构造函数。
1 function Box(name, age) { 2 var obj = new Object(); 3 obj.name = name; 4 obj.age = age; 5 obj.run = function () { 6 return name + age + '运行中...'; 7 }; 8 return obj; 9 } 10 11 var box1 = Box('Lee', 100); 12 alert(box1.run()); 13 14 var box2 = Box('Jack', 200); 15 alert(box2.run());
用的最多的应该是组合原型模式+构造函数以及动态原型模式