4.原型
每个函数都有一个prototype属性,这个属性是一个对象,用途是包含可以由特定类型的所有实例共享的属性和方法。
逻辑上可以这么理解:prototype通过调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
function Box(){}; //构造函数体内什么都没有,这里如果有,叫做实例属性、实例方法Box.prototype.name = 'Lee'; //原型属性Box.prototype.age = 20;Box.prototype.run = function(){ //原型方法return "姓名:" + this.name + ",年龄:" + this.age;}var box1 = new Box(); //实例化console.log(box1.name); //Leeconsole.log(box1.run()); //姓名:Lee,年龄:20
那么,构造方法和原型方法的区别为共享,具体结束如下:
(1)如果是实例方法不同的实例化,他们的方法地址是不一样的,唯一的,详见JavaScript之面向对象与原型笔记整理--------创建对象(1)。
(2)如果是原型方法,那么他们的地址是共享的,大家都是一样的。
看下面这个例子:
function Box(){};Box.prototype.name = 'Lee'; Box.prototype.age = 20;Box.prototype.run = function(){ return "姓名:" + this.name + ",年龄:" + this.age;}var box1 = new Box(); var box2 = new Box();console.log(box1.run==box2.run); //true 如果是构造函数的话则为false,因为引用地址不同
为了更好的理解,接下来给出构造函数的声明方式和原型模式的声明方式的示意图。
构造函数方式 原型模式声明方式
在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。__proto__属性是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性constructor。通过这两个属性,就可以访问到原型里的属性和方法了。
看下例子:
function Box(){};Box.prototype.name = 'Lee'; Box.prototype.age = 20;Box.prototype.run = function(){ return "姓名:" + this.name + ",年龄:" + this.age;}var box1 = new Box(); console.log(box1.prototpye); //undefined 这个属性是一个对象,访问不到,通过__proto__指针才能访问到prototype原型对象console.log(box1.__proto__); //这个属性是一个指针,指向prototype原型对象
console.log(Box.prototype); //使用构造函数名(对象名)访问prototype,和上一行输出结果一样
//constructor构造属性,可以获取构造函数本身,作用是被原型指针定位,然后得到构造函数本身,其实就是对象实例对应原型对象的作用console.log(box1.constructor); //function Box(){}
//判断一个对象实例是不是指向了原型对象,基本上,只要实例化了,他自动指向Box.prototype.isPrototypeOf(box1); //true var obj = new Object(); //只要实例化就默认指向了Object.prototype.isPrototypeOf(obj ); //true
Box.prototype.isPrototypeOf(obj); //false
通过例子和图示理解了声明方式,看一下原型模式的执行流程:
(1)先查找构造函数实例里的属性或方法,如果有,立刻返回。
(2)如果构造函数实例里没有,则去它的原型对象里找。如果有,就立刻返回。
实例如下:
//情况一function Box(){};Box.prototype.name = 'Lee'; Box.prototype.age = 20;Box.prototype.run = function(){ return "姓名:" + this.name + ",年龄:" + this.age;}var box1 = new Box(); box1.name = 'Luck'; //实例属性,并没有重写原型属性console.log(box1.name); //Luck 就近原则
//多说几句:
var box2 = new Box();
console.log(box2.name); //Lee 实例属性不会共享,所以box2访问不到实例属性,就只能访问原型
//若想box1访问原型对象,删除实例中的属性
delete box1.name;
console.log(box1.name); //Lee
//覆盖原型里的属性
Box.prototype.name = "Lily";
console.log(box1.name); //Lily//情况二function Box(){this.name = 'Luck';};Box.prototype.name = 'Lee'; Box.prototype.age = 20;Box.prototype.run = function(){ return "姓名:" + this.name + ",年龄:" + this.age;}var box1 = new Box(); console.log(box1.name); //Luck 先访问构造函数实例里的方法
//多说几句:
//如何判断属性是否在构造函数里?使用hasOwnProperty来判断
console.log(box1.hasOwnProperty('name')); //true 实例里有返回true 没有返回false
//不管实例属性或原型属性是否存在,只要有,就返回true,两边都没有返回false
console.log('name' in box1); //true
//那么,判断只有原型中有属性:
function isProperty(object,property){
return !object.hasOwnProperty(property)&&(property in object)
}
isProperty(box1,name);
构造函数实例属性和原型属性示意图
上面这个示意图说明了,box1中,name属性冲突时,就近原则,先访问构造函数里的name。box2中,name不冲突,直接访问原型属性name。
原型字面量方法:
为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式,体现更好的封装性。
实例如下:
function Box(){};Box.prototype={name:'Lee',age:20,run:function(){return "姓名:" + this.name + ",年龄:" + this.age;}}var box = new Box();console.log(box.run()); //姓名:Lee,年龄:20
使用构造函数创建原型对象和使用字面量创建原型对象在使用上基本相同,但是还有一些区别,字面量创建的方式使用constructor属性不会指向实例,而会指向Object,构造函数创建的方式则相反。
请看如下代码:
//为了省事将两种情况写在了一起,测试时请分开测试function Box(){};Box.prototype.name = 'Lee';Box.prototype.age = 20;Box.prototype.run = function(){return "姓名:" + this.name + ",年龄:" + this.age;}var box = new Box();console.log(box.constructor); //function Box(){}function Box(){};//使用字面的方式创建原型对象,这里{}就是对象,是Object。//new Object()就相当于{}Box.prototype={name:'Lee',age:20,run:function(){return "姓名:" + this.name + ",年龄:" + this.age;}}var box = new Box();console.log(box.constructor); //function Object(){}
//字面量方式创建原型对象验证方法:
console.log(box.constructor == Box); //false
console.log(box.constructor == Object); //true
//构造函数方法创建原型对象验证方法:
console.log(box.constructor == Box); //true
console.log(box.constructor == Object); //false
字面量方式为什么constructor会指向Object?
因为Box.prototype={};这种写法其实就是创建了一个新对象,而每创建一个对象,就会同时创建它的prototype,这个对象也会自动获取constructor属性。所以,新对象的constructor重写了Box原来的constructor,因此会指向新对象,那个新对象没有指向构造函数,那么就默认为Object。
如果想让字面量方式的constructor指向实例对象,可以强制指向:
Box.prototype = {
constructor:Box; // 直接强制指向即可,添加此句
}
接下来看另一个问题:
原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型。实例如下:
Box.prototype={
constructor:Box, //强制指向Boxname:'Lee',age:20,run:function(){return "姓名:" + this.name + ",年龄:" + this.age;}}//下面重写了原型对象Box.prototype={age:18, //这里不会保留之前原型的任何信息,吧原来的原型对象和构造函数对象实例之前的关系切断了 }
var box = new Box();
console.log(box.age); //18console.log(box.name); //undefinedconsole.log(box.run()); //run() is not a function 报错
原型对象不仅仅可以在自定义对象的情况下使用,而ECMAScript内置的引用类型都可以使用这种方法,并且内置的引用类型本身也使用了原型。
//数组排序var box = [5,1,6,9,3,5];console.log(box.sort()); //1,3,5,5,6,9//查看sort是否是Array原型对象里的方法console.log(Array.prototype.sort); //function sort(){} 证明是//类似的还有console.log(String.prototype.substring); //function substring(){} 证明是//内置引用类型的功能扩展//现在我们想添加一个原型方法,先判断这个方法是否存在。console.log(String.prototype.addstring);//undefined 不存在,可以添加String.prototype.addstring = function(){return this + "被添加了";}var box = 'Lee';console.log(box.addstring()); //Lee被添加了
尽管给原生的内置引用类型添加方法使用起来特别方便,但我们不推荐使用这种方法,因为它可能会导致命名冲突,不利于代码维护。
原型模式创建对象也有自己的缺点,它省略了构造函数传初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,共享。
原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以,但如果属性包含引用类型,就存在一定的问题:
原型的缺点:
function Box(){}//实例化的时候没有办法通过传参改变它们的值Box.prototype = {constructor:Box,name:'Lee',age:20,family:['哥哥','姐姐'],run:function(){return this.name + this.age + this.family;}}var box1 = new Box();console.log(box1.family);//'哥哥','姐姐'box1.family.push('弟弟'); //在第一个实例修改后引用类型,保持了共享console.log(box1.family.push('弟弟'));//哥哥,姐姐,弟弟var box2 = new Box();console.log(box2.family); //哥哥,姐姐,弟弟 共享了box1添加后的引用类型的原型
数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化的数据需要保持自己的特性,而不能共享。
为了解决构造传参和共享问题,使用组合构造函数+原型模式。
5.组合构造函数+原型模式
function Box(name,age){ //保持独立的用构造函数this.name = name;this.age = age;this.family = ['哥哥','姐姐'];}Box.prototype = { //保持共享的用原型constructor:Box,run:function(){return this.name + this.age;}}var box1 = new Box('Luck',18);console.log(box1.run()); //Luck18box1.family.push('弟弟');console.log(box1.family); //哥哥,姐姐,弟弟var box2 = new Box('Lee',20);console.log(box2.run()); //Lee20console.log(box2.family); //哥哥,姐姐 引用类型没有使用原型,所以没有共享
这种混合模式很好的解决了传参和引用共享的大难题,是创建对象比较好的方法。
原型模式,不管你是否调用了原型中的共享方法,它都会初始化原型中的方法,并且在声明一个对象时,构造函数+原型部分让人感觉有很怪异,最好就是把构造函数和原型分装到一起,为了解决这个问题,可以使用动态原型模式。