JS原型
/*温馨提示:若觉得黑白的反差“亮瞎眼”,可适当调低屏幕亮度~*/
有一段时间,被JS的原型的“原形”搞的晕头转向的,但天下没有攻不下的城池~我认为要理解原型,就要分清楚“原型对象”,“构造函数”,“实例对象”这三者分别是什么,又分别干什么用。
1.原型对象
原型对象就是我们常常看到的prototype,其内部有其自身的属性与方法,其中包含有两个特别的属性:
(1)constructor:该属性指向对象的构造函数(可以理解为C+中的引用,即一个不会自己移动的指针,但可以人为改变指向);
(2)__proto__:该属性指向该对象的原型,可以通过原型访问器访问其原型。(注意:proto左右都是双下划线哦~)
它是这个样子滴~
标准对象原型访问器为:Object.getPrototype(obj)【FirFox,Chrome支持】;
非标准对象原型访问器为:__proto__【除了IE外都支持】
若这两个访问器不起作用,可通过对象的构造函数去找原型:obj.constructor.prototype;(这里通过obj.constructor访问对象的构造函数)
JS就是通过原型来实现继承的,我们通过原型对象设置的属性与方法可以被其所有实例共享,通过实例设置原型对象属性与方法可以被其他实例共享,所有我们一般通过原型对象来设置所有构造对象相同的方法与属性。
2.构造函数
原型的constructor属性指向构造函数,利用new运算符可以依照构造函数来创建并初始化一个新的对象,对象构造函数中的prototype属性指向该对象原型。我们可以为构造函数设置属性与方法来扩展对象的初始化属性与方法,只是初始化由this传递进来的对象。我把它理解为为原型对象(prototype)与实例对象建立联系的良民。
可以通过传参的方式来动态改变属性值。
它是这个样子滴~
注意:这里的constructor其实应该是沿着原型链往上搜寻到的,并非真的是设置在构造函数中的属性~~~
可以说:我们利用构造函数构造了以 构造函数原型 为 原型 的对象。(有点拗口~)简言之,我们通过构造函数构造新对象,prototype用于分类,实例传参与个性定义实现多态。
3.实例
在使用new创建新对象(对象实例化)时,会为新对象创建独立空间并初始化,其中的__proto__属性指向原型对象(prototype),
实现对原型对象属性与方法的继承。
它是这个样子滴~
注意:这里的constructor其实应该是从原型中搜寻到的,并非真的是设置在实例对象中的属性~~~
综上所述可以看出构造函数,实例对象的基本属性都汇聚(指向)于原型,看得出JS自身是多么惜存如金啊~所以编程更应该注意内存的合理使用~
4.这三者的沟通方式
对象实例的_proto_属性指向原型对象(prototype),而原型对象(prototype)的constructor属性指向对象的构造函数,构造函数通过new运算符创建并初始化实例对象,使得对象原型与实例对象可以交流。构造函数中的属性与方法也是所有实例共享的。
5.原型链——继承原理
刚刚提到了,原型对象(prototype)中的__proto__属性指向该原型对象的原型,假设将上述 “原型对象”,“构造函数”,“实例对象”这三者看做一个层级,那么原型对象(prototype)中的__proto__属性便承担了沟通上级层级的使者,这样一层一层相连接,就形成了原型链。原型链的顶端为Object,它的__proto__指向null。每一个对象都有自身的原型链,在确定某一对象时,也会类似于确定变量值一样按就近原则一层一层查找,找到则返回属性值或方法,若是直到顶端还未找到则返回undefined。
原型链是这样子的:实例->实例原型->实例原型的原型->实例的原型的原型的原型->...->Object.prototype->null;
6.方法与属性的设置
我们可以在“原型对象”,“构造函数”,“实例对象”设置方法和属性。
(1).在原型对象中设置时,设置的属性与方法可以被所有实例共享,且在用new运算符创建实例时不会开辟新的空间存放这些属性和方法,仅仅只是存放了原型的引用(__proto__);
(2).在构造函数中设置属性与对象是通过灵活的this来设置每次传进来的不同对象的相同方法与属性,因此使用new运算符来创建实例时,会为每一个实例开辟相应的空间来存放相同的属性与方法,这种做法十分耗内存,一般我们提倡将实例的共享属性加到实例的原型中去,那构造函数难道就没其他作用了吗,其实它有一个作用,就是可以用来扩展JS原有的内建对象的方法与属性,这样,与团队的其他成员合作时,不会因为改动了内建对象的原有属性而使其他成员在未知情况下使用内建对象而出现意想不到的结果,同时也减少了代码量,因为这种情况下每次在不同实例上添加共同方法时都必须使用 " [实例].constructor.*** " 来设置,这样会加大代码量,影响性能。内建对象有Array,Math,Date,Function,RegExp......
(3).为实例添加属性与方法时,如果与原型对象中的属性方法重名时,实例会屏蔽原型中的同名属性与方法;如果不重名,则会为实例添加相应的属性与方法;一般用来设置独有属性与方法。
(4).对象的__proto__可以赋值重新指定原型
eg.var a = function () {};
var b = new a ();
b.__proto__ == a.prototype;
b instanceof a; //true (判断b是否是a的实例)
b.__proto__ == Function.prototype;
b instanceof a; //false
7.自我脑补完整版
函数与构造器并没有明显界限,当一个函数被定义时,会自动创建并初始化prototype值(存放原型的引用,即指向原型),此时的函数其实是隐式构造自Function(),即其__proto__属性指向Function.prototype,而其prototype属性指向该函数的原型对象(?分配的?),此时的原型对象(prototype)中只有继承到的属性与方法(即__proto__)与constructor属性,通过new运算符把该函数的prototype属性值赋给实例的__proto__属性,此时实例就继承了原型的所有属性与方法。
我们一般通过 “ [构造函数].prototype.*** ” 或 “ [实例].__proto__.*** ” 来访问添加修改原型的属性与方法,用delete来删除指定的属性与方法。
要确定实例属性,使用实例属性与方法时就沿着原型链查找。
注意:给 “ [构造函数].prototype ” 重新定义时会重写整个原型对象,必须加上 “ constructor:[构造函数] ” ,否则将会使构造函数与原型之间的引用断开。
正确的姿态:JS的特点是没有类,而是靠prototype来区分不同类别的对象,一般我们把对象属性定义在构造函数中,这样就可以通过传参来动态设置相应属性值,而方法则通过 “ [构造函数].prototype.***=function(){}; " 将其添加到原型中。
我的想法:我把原型链看成一层一层的,每一层的构造函数本身有指向本层原型对象的prototype属性,在构造对象时将原型对象的引用赋予实例的__proto__属性,层与层之间的连接通过原型的_proto_属性指向上层原型来实现,使用new运算符相当于将this放置于原型赋给实例。
(1).
function a (){};
alert(a instanceof Function); //true(函数是Function的实例)
alert(a.prototype); //object Object(函数有指向原型对象的prototype属性)
alert(a.prototype.constructor);//function a(){};
(2).
var a = function (){};
alert(a instanceof Function); //true(函数是Function的实例)
alert(a.prototype); //object Object (赋值式函数也有指向原型对象的prototype属性)
alert(a.prototype.constructor);//function (){};
(3).
var a = function (name){};
var b = new a();
alert(b.constructor == a); //true (a是b的构造函数)
alert(b.__proto__ == a.prototype); //true (a在构造b对象时将原型对象的引用赋予b的__proto__属性)
alert(a.__proto__ == Function.prototype); //true (层与层之间的连接通过原型的_proto_属性指向上层原型来实现)
alert(a.__proto__ == Function.prototype); //true
alert(a.constructor == Function); //true
继承:
暂时撇开构造函数看,可以发现实例与原型的__proto__都是往上指的,这样就可以清晰的看到原型链了吧~~O(∩_∩)O~
嘿~再加回构造函数,看没看到一棵树呢?
8.实现继承啦——原型复制
先看代码:
function a (){}
alert( a.prototype.constructor == a); //true 函数原型的构造函数指向本身
delete a.prototype.constructor;
alert( a.prototype.constructor == Object); //true 删除函数原型的构造函数的指向后,函数指向父级原型中的constructor值
由于实例与原型的constructor皆指向构造函数,再结合以上两点可以得出此时: a.prototype.constructor == new Object().constructor;
此时 a.prototype与new Object没实质区别。
那么,我们可以这样进行原型复制以实现继承:
function MyObject1(){}
function MyObject2(){}
MyObject2.prototype = new MyObject1(); //即MyObject2的原型是MyObject1的实例,其__proto__指向MyObject1的原型,constructor指向MyObject1,从而实现继承,原型链形成,还记得前面提到的吗,为函数重设原型时如没有设置constructor指回原值,构造函数与原型之间的引用会断开
var obj1 = new MyObject1();
var obj2 = new MyObject2();
alert(obj2.constructor == MyObject2); //false
alert(obj2.constructor == MyObject1); //true
alert(MyObject2.prototype.constructor == MyObject1); //true (那MyObject2咋办捏~或者说希望修改obj2原型,且属性可与obj2的同类共享,而不希望改动obj1,咋办~)
还记得上文提到的实例对象与构造函数的属性皆指向原型吗?这样的耦合性太大,一般我们会使用临时构造器(f())进行解耦
function MyObject1 (){}
function MyObject2 (){}
var f = function (){};
f.prototype = MyObject1.prototype; //将f中的prototype属性指向MyObject1的原型,此时f就可以获得MyObject1的构造器了
MyObject2.prototype = new f(); //取得f的prototype赋给MyObject2.prototype的__proto__属性
MyObject2.prototype.constructor = MyObject2; //这样通过 MyObject2 构造函数修改原型,不会影响父级啦~
说白了就是人为的给原型设置__proto__与constructor属性。
9.相关属性
instance of:判断实例 eg. a instanceof b; ——判断a是否是b的实例
hasOwnPrototype:判断是否是自身独有而非继承的属性
isProtypeOf:判断原型 eg. a isProtypeOf b; ——判断a是否是b的原型
(Object)getPrototypeOf(obj):获取obj原型
prototypeIsEnumerable(prototype); 判断prototype是否可用for...in...枚举
......