浅析构造函数,原型对象和实例对象的关系
原文地址:JS面向对象-原型对象,实例对象,构造函数的关系(http://blog.csdn.net/u014205965/article/details/45798861)
因为最根上的object拥有一个prototype属性,而js中所有的对象又都继承自object,所以js中所有的对象都拥有一个prototype属性,而在js中函数也是对象,所以js中每个函数也都有一个prototype属性。
例如:function Person(){...} 和function Dog(){...}
而每一个prototype属性又会获得一个constructor属性
该constructor属性有一个隐含的指针,指向了prototype属性所在的函数。
这里就说明了,当用alert函数弹出 Person.prototype.constructor 的时候,弹出的结果是
function Person{....}
当通过new Person() 创建一个对象实例后,该实例包含一个隐含的指针,指向了Person.prototype
结合之前的例子,通过图示说明一下 构造函数,实例对象,原型对象之间的关系
Person 的每个实例都包含一个隐形指针,指向Person.prototype,换句话说,它们与构造函数没有直接关系。
可以用isPrototypeof( ) 来判定一个“实例”是否指向某个“原型对象”;也可以理解为某个实例是否源于某个原型对象。
alert(Person.prototype.isPrototypeof(p1 )) 返回true,说明p1指向Person.prototype,
Person.prototype 是实例p1的原型对象。
详解构造函数和原型对象
原文:深入理解javascript构造函数和原型对象(http://www.jb51.net/article/55539.htm)
对象,是javascript中非常重要的一个梗,是否能透彻的理解它直接关系到你对整个javascript体系的基础理解,说白了,javascript就是一群对象在搅。。(哔!)。
常用的几种对象创建模式
使用new关键字创建
最基础的对象创建方式,无非就是和其他多数语言一样说的一样:没对象,你new一个呀!
1 var gf = new Object(); 2 gf.name = "tangwei"; 3 gf.bar = "c++"; 4 gf.sayWhat = function() { 5 console.log(this.name + "said:love you forever"); 6 }
使用字面量创建
这样似乎妥妥的了,但是宅寂的geek们岂能喜欢如此复杂和low土的定义变量的方式,作为一门脚本语言那应该有和其他兄弟们一样的范儿,于是出现了对象字面量的定义方式:
1 var gf = new Object(); 2 gf.name = "tangwei"; 3 gf.bar = "c++"; 4 gf.sayWhat = function() { 5 console.log(this.name + "said:love you forever"); 6 }
工厂模式
实际上这是我们在实际中最常用的对象定义方式,但是我要有好多拥有相似属性的对象(想想都让人激动。。。)怎么办呢?那要是一个个的定义,就会产生大量的代码,何不建个工厂,批量的生产出我们的对象呢,于是,javascript世界中第一个充气娃。。。不,“工厂模式”诞生了!
1 function createGf(name, bar) { 2 var o = new Object(); 3 o.name = name; 4 o.bar = bar; 5 o.sayWhat = function() { 6 alert(this.name + "said:love you forever"); 7 } 8 return o; 9 } 10 var gf1 = createGf("bingbing","d"); 11 var gf2 = createGf("mimi","a");
构造函数
工厂模式解决了多个相似对象的创建问题,但是问题又来了,这些对象都是Object整出来的,怎么区分它们的对象具体类型呢?这时候我们就需要切换到另一种模式了,构造函数模式:
1 function Gf(name,bar){ 2 this.name = name; 3 this.bar = bar; 4 this.sayWhat = function(){ 5 alert(this.name + "said:love you forever"); 6 } 7 } 8 var gf1 = new Gf("vivian","f"); 9 var gf2 = new Gf("vivian2","f");
上面的方式似乎没什么不妥,但是我们可以发现,两个实例中调用的构造函数中的sayWhat方法不是同一个Function实例:
console.log(gf1.sayWhat == gf2.sayWhat); //false
调用同一个方法,却声明了不同的实例,实在浪费资源。我们可以优化一下将sayWhat函数放到构造函数外面声明:
1 function Gf(name,bar){ 2 this.name = name; 3 this.bar = bar; 4 this.sayWhat = sayWhat 5 } 6 function sayWhat(){ 7 alert(this.name + "said:love you forever"); 8 }
这样解决了,多个实例多次定义同一个方法实例的问题,但是新问题又来了,我们定义的sayWhat是一个全局作用域的方法,但这个方法其实是没法直接调用的,这就有点矛盾了。如何更优雅的定义一个具备一定封装性的对象呢?我们来看一下javascript原型对象模式。
原型对象模式
理解原型对象
当我们创建一个函数时,该函数就会具备一个prototype属性,这个属性指向通过构造函数创建的那个函数的原型对象。通俗点讲原型对象就是内存中为其他对象提供共享属性和方法的对象。
在原型模式中,不必再构造函数中定义实例属性,可以将属性信息直接赋予原型对象:
1 function Gf(){ 2 Gf.prototype.name = "vivian"; 3 Gf.prototype.bar = "c++"; 4 Gf.prototype.sayWhat = function(){ 5 alert(this.name + "said:love you forever"); 6 } 7 } 8 var gf1 = new Gf(); 9 gf1.sayWhat(); 10 var gf2 = new Gf();
和构造函数不同的是这里新对象的属性和方法是所有实例都可以共享的,换句话说gf1和gf2访问的是同一份属性和方法。原型对象中除了我们赋予的属性外,还有一些内置的属性,所有原型对象都具备一个constructor属性,这个属性是一个指向包含prototype属性函数的一个指针(敢不敢再绕点!)。通过一幅图我们来清楚的理一下这个绕口的流程:
所有的对象都有一个原型对象(prototype),原型对象中有一个constructor属性指向包含prototype属性的函数,Gf的实例gf1和gf2都包含一个内部属性指向原型对象(在firefox浏览器中表现为私有属性proto),当我们访问一个对象中的属性时,首先会询问实例对象中有没有该属性,如果没有则继续查找原型对象。
使用原型对象
在前面的示例中,我们注意到在为原型对象添加属性时,需要每个都增加Gf.prototype,这个工作很重复,在上面对象的创建模式中,我们知道可以通过字面量的形式创建一个对象,这里我们也可以改进一下:
1 function Gf(){} 2 Gf.prototype = { 3 name : "vivian", 4 bar : "c++", 5 sayWhat : function(){ 6 alert(this.name + "said:love you forever"); 7 } 8 }
这里有一个地方需要特别注意下,constructor属性不再指向对象Gf,因为每定义一个函数,就会同时为其创建一个prototype对象,这个对象也会自动获取一个新的constructor属性,这个地方我们使用Gf.prototype本质上覆写了原有的prototype对象,因此constructor也变成了新对象的constructor属性,不再指向Gf,而是Object:
1 var gf1 = new Gf(); 2 console.log(gf1.constructor == Gf);//false 3 console.log(gf1.constructor == Object)//true
一般情况下,这个微妙的改变是不会对我们造成影响的,但如果你对constructor有特殊的需求,我们也可以显式的指定下Gf.prototype的constructor属性:
1 Gf.prototype = { 2 constructor : Gf, 3 name : "vivian", 4 bar : "c++", 5 sayWhat : function() { 6 alert(this.name + "said:love you forever"); 7 } 8 } 9 var gf1 = new Gf(); 10 console.log(gf1.constructor == Gf);//true
通过对原型对象模式的初步了解,我们发现所有的实例对象都共享相同的属性,这是原型模式的基本特点,但往往对于开发者来说这是把“双刃剑”,在实际开发中,我们希望的实例应该是具备自己的属性,这也是在实际开发中很少有人单独使用原型模式的主要原因。
构造函数和原型组合模式
在实际开发中,我们可以使用构造函数来定义对象的属性,使用原型来定义共享的属性和方法,这样我们就可以传递不同的参数来创建出不同的对象,同时又拥有了共享的方法和属性。
1 function Gf(name,bar){ 2 this.name = name; 3 this.bar = bar; 4 } 5 Gf.prototype = { 6 constructor : Gf, 7 sayWhat : function() { 8 alert(this.name + "said:love you forever"); 9 } 10 } 11 var gf1 = new Gf("vivian", "f"); 12 var gf2 = new Gf("vivian1", "c");
在这个例子中,我们再构造函数中定义了对象各自的属性值,在原型对象中定义了constructor属性和sayWhat函数,这样gf1和gf2属性之间就不会产生影响了。这种模式也是实际开发中最常用的对象定义方式,包括很多js库(bootstrap等)默认的采用的模式。
浅析原型链与继承
原文:构造函数、实例和原型的概念和关系(http://blog.csdn.net/beautifull001/article/details/53695641)
每个函数都属于对象,都会有一个属性叫prototype。这个属性指向一个对象,我们把他叫做当前函数的原型对象。原型对象下面有个属性叫constructor.这个属性指向当前函数。函数又分为普通函数和构造函数。这里我们说一下构造函数。定义一个函数 :
1 function Person(x, y) 2 { 3 this.age = x; 4 this.name = y; 5 } 6 var xiaoming = new Person(12, "xiaoming");
这里创建实例对象 xiaoming的时候就是调用了Person构造函数,使xiaoming有了自己的属性和方法,之后xiaoming和Person也就没有什么直接交集了(可以理解为小明分手了,哎程序员好难╥..╥)但是每个实例对象都会有一个隐藏属性[[prototype]],这个属性在chrome/firefox下叫proto,仅仅供学习调试用.它指向的就是构造函数的原型对象。
原型对象的深入理解
对于这个原型对象,我们就要重点理解下了。这个对象的作用就是为了让所有的实例对象都能共享这个对象的属性和方法(当然实例本身的属性和方法优先级是高于原型的)。每个构造函数都会有一个默认的原型对象。我们只要在改原型对象上做文章就可以实现很多功能。
● 共享属性和方法:
1 Person.prototype.eyes = 2; 2 Person.prototype.walk = function () 3 { 4 alert(this.name + ":go"); 5 }; 6 var xiaoming = new Person(12, "xiaoming"); 7 var xiaohong = new Person(12, "xiaohong"); 8 console.log(xiaoming.eyes);//2 9 console.log(xiaohong.eyes); //2 小明和小红都有2只眼 10 console.log(xiaoming.walk());//xiaoming:go 11 console.log(xiaohong.walk()); //xiaohong:go 小明和小红都会走路
● 原型链:
我们先做一个假设,假如我们把一个函数对象Man的原型直接给换成另一个函数对象Person的实例对象xiaoming会怎么样呢?
前面说了,通过实例对象是可以找到函数对象Person的原型。那我们现在Man对象的实例xiaoming是不是也就可以访问到Person对象的原型对象了呢。
1 function Man() 2 { 3 this.beard = "xxx "; 4 } 5 console.log(Man.prototype.constructor === Man);//true 6 Man.prototype = new Person(23, "xiaoming "); 7 console.log(Man.prototype.constructor === Person);//true
这里我们相当于把默认的那个原型给重写了,给参数其实就是给原型添加属性和方法
1 var xiaohui = new Man(); 2 console.log(xiaohui.constructor === Person);//true ,构造函数,返回创建该对象的函数的引用 3 console.log(xiaohui.prototype === undefined);//true ,实例对象都会有一个隐藏属性[[prototype]],这个属性在chrome/firefox下叫proto,仅仅供学习调试用 4 console.log(xiaohui.beard); //xxx 这里实例xiaohui自己的属性(小明有胡子) 5 console.log(xiaohui.age); //23 6 console.log(xiaohui.name); //xiaohui 这两个属性是实例的原型上面的属性(其实这个属性是Person实例的属性,但是现在的原型不就是Person实例吗) 7 console.log(xiaohui.eyes); //2 这个属性呢,是Person的原型对象上面的了
这里我们基本上都可以访问到,是不是有点继承的味道了。如果我们再这样搞一个对象,也这么干,这里是不是就感觉像条链一样。最顶端的对象是Object,也就是说到最后了。我们把这条链接方式叫做原型链。这也是继承的依据。
继承
和传统的OOP语言来说,JavaScript语言比较蛋疼的是它没有类这个机制。所以说我们事先js的继承就从对象角度下手了。我们重点说一下依据原型链继承的。(其他的继承我就不说了,比如借用父对象的构造函数等,实用性不强)
1.上面所说的实现原型链的方法虽然有点继承的味道了,但是你有没有发现 实例化xiaohui这个对象的时候调用了Man这个构造函数,但是xiaohui自己的age和name都没能进行构造,只不过是原型上的属性而已(其实是Person自己构造的,new Person( 23, “xiaoming” ))。我们其实可以这样用call和apply这个object原型下面给我们定义好的方法改进下(call和apply方法自己看api说明吧)
1 function Man(x, y) 2 { 3 Person.call(this, x, y); //这里你可以这样理解,this指的是Man,这样其实就是借用Person构造函数this.age = x;this.name = y;; 4 this.beard = "xxx "; 5 }
我们把Man的构造函数这样一改,实例化的时候传参,这样age和name这两个属性就是Man自己构造出来的了,并不会被共享
1 Man.prototype = new Person(); 2 Man.prototype.constructor = Man; 3 var xiaohui = new Man(23, "xiaohui"); 4 console.log(xiaohui.constructor === Man);//true
这里只是让Man的原型的构造函数变成原有的构造函数,如果不加这一句的话,那么Man原型的构造函数就变成undefied,因为实例和构造函数并没有直接关系。这样一来,原型找不到构造函数,这是非常蛋疼的事情,违反了原型链的定义啊。 (原文)
上句经测试发现:
1.即使不加Man.prototype.constructor = Man,Man的原型的构造函数也不会是undefined,因为此时Man的原型被重写为Person的实例,所以自然此时Man的原型的构造函数===Person原型的构造函数(上代码可证)。
2.虽然Man的原型的构造函数变为Person,但是执行new Man();时函数入口仍为function Man(){...},并非新的构造函数function Person(){...},对面向对象的C#等语言的类创建对象new时调用构造函数模拟有出入。
3.加上Man.prototype.constructor = Man,Man的原型的构造函数还原为function Man(){...},与构造函数的语义相符。(个人理解)
这边可能会有人问了,我为什么不自己像胡子beard 那个属性一样直接构造呢。
大哥,我这是举例子,你以为实际的项目中就会有这么两个属性吗。而且这样不正是继承的目的吗
可以少写很多代码啊。(说多了都是泪)
但是也是有缺点的:两次调用父类构造函数(第一次是在创建子类原型的时候,第二次是在子类构造函数内部);子类继承父类的属性,一组在子类实例上,一组在子类原型上(在子类原型上创建不必要的多余的属性,实例上的屏蔽原型上的同名属性,是不是感觉有点多余了 ,效率低。
2.为了改进这种方法,下面说的这种继承方式是借助我们伟大的道爷(这个人很厉害,自行百度)的灵感 。这种就是利用一个空函数对象来做一个桥梁.具体实现方式如下:
1 function inherits(Child, Parent) 2 { 3 var F = function () { }; 4 F.prototype = Parent.prototype; 5 Child.prototype = new F(); 6 Child.prototype.constructor = Child; 7 }
另外在子对象的构造函数中别忘了借用父对象的构造函数哦。(就是那个call或者apply方法)
这里和上面的区别是,子对象的原型现在不是父对象的实例了,变成了空函数对象的实例(父对象不用再创建两次了,并且子对象的原型上也不会有啥属性和方法了)。而空函数对象的原型变成了父对象的原型。前面我们说过,有了实例就能找到原型。所以现在子对象原型和父对象原型是就建立关系了。这种方式现在是最稳的方法,也已经被很多框架给写到源码里面了。这里我们就用google closure 关于继承的两个api,这边简单举个例子:
Child = function( ){ goog.base(this); this.height = 12; } goog.inherits(Child, Parent);
详解原型链与继承
原文:详解JS原型链与继承(http://louiszhai.github.io/2015/12/15/prototypeChain/)
摘自JavaScript高级程序设计:
继承是OO语言中的一个最为人津津乐道的概念.许多OO语言都支持两种继承方式: 接口继承 和实现继承 .接口继承只继承方法签名,而实现继承则继承实际的方法.由于js中方法没有签名,在ECMAScript中无法实现接口继承.ECMAScript只支持实现继承,而且其 实现继承
主要是依靠原型链来实现的.
概念
简单回顾下构造函数,原型和实例的关系:
每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针.
JS对象的圈子里有这么个游戏规则:
如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性.
如果让原型对象指向另一个类型的实例.....有趣的事情便发生了.
即: constructor1.prototype = instance2
鉴于上述游戏规则生效,如果试图引用constructor1构造的实例instance1的某个属性p1:
1).首先会在instance1内部属性中找一遍;
2).接着会在instance1.__proto__(constructor1.prototype)中找一遍,而constructor1.prototype 实际上是instance2, 也就是说在instance2中寻找该属性p1;
3).如果instance2中还是没有,此时程序不会灰心,它会继续在instance2.__proto__(constructor2.prototype)中寻找...直至Object的原型对象
搜索轨迹: instance1--> instance2 --> constructor2.prototype…-->Object.prototype
这种搜索的轨迹,形似一条长链, 又因prototype在这个游戏规则中充当链接的作用,于是我们把这种实例与原型的链条称作 原型链 . 下面有个例子
function Father(){
this.property = true; } Father.prototype.getFatherValue = function(){ return this.property; } function Son(){ this.sonProperty = false; } //继承 Father Son.prototype = new Father();//Son.prototype被重写,导致Son.prototype.constructor也一同被重写 Son.prototype.getSonVaule = function(){ return this.sonProperty; } var instance = new Son(); alert(instance.getFatherValue());//true
instance实例通过原型链找到了Father原型中的getFatherValue方法.
注意: 此时instance.constructor指向的是Father,这是因为Son.prototype中的constructor被重写的缘故.
以上我们弄清楚了何为原型链,如有不清楚请尽量在下方给我留言
确定原型和实例的关系
使用原型链后, 我们怎么去判断原型和实例的这种继承关系呢? 方法一般有两种.
第一种是使用 instanceof 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true. 以下几行代码就说明了这点.
alert(instance instanceof Object);//true
alert(instance instanceof Father);//true alert(instance instanceof Son);//true
由于原型链的关系, 我们可以说instance 是 Object, Father 或 Son中任何一个类型的实例. 因此, 这三个构造函数的结果都返回了true.
第二种是使用 isPrototypeOf() 方法, 同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true, 如下所示.
alert(Object.prototype.isPrototypeOf(instance));//true
alert(Father.prototype.isPrototypeOf(instance));//true
alert(Son.prototype.isPrototypeOf(instance));//true
原理同上.
原型链的问题
原型链并非十分完美, 它包含如下两个问题.
问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.
有鉴于此, 实践中很少会单独使用原型链.
为此,下面将有一些尝试以弥补原型链的不足.
借用构造函数
为解决原型链中上述两个问题, 我们开始使用一种叫做借用构造函数(constructor stealing)的技术(也叫经典继承).
基本思想:即在子类型构造函数的内部调用超类型构造函数.
function Father(){
this.colors = ["red","blue","green"]; } function Son(){ Father.call(this);//继承了Father,且向父类型传递参数 } var instance1 = new Son(); instance1.colors.push("black"); console.log(instance1.colors);//"red,blue,green,black" var instance2 = new Son(); console.log(instance2.colors);//"red,blue,green" 可见引用类型值是独立的
很明显,借用构造函数一举解决了原型链的两大问题:
其一, 保证了原型链中引用类型值的独立,不再被所有实例共享;
其二, 子类型创建时也能够向父类型传递参数.
随之而来的是, 如果仅仅借用构造函数,那么将无法避免构造函数模式存在的问题--方法都在构造函数中定义, 因此函数复用也就不可用了.而且超类型(如Father)中定义的方法,对子类型而言也是不可见的. 考虑此,借用构造函数的技术也很少单独使用.
组合继承
组合继承, 有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式.
基本思路: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.
这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性. 如下所示.
function Father(name){
this.name = name; this.colors = ["red","blue","green"]; } Father.prototype.sayName = function(){ alert(this.name); }; function Son(name,age){ Father.call(this,name);//继承实例属性,第一次调用Father() this.age = age; } Son.prototype = new Father();//继承父类方法,第二次调用Father() Son.prototype.sayAge = function(){ alert(this.age); } var instance1 = new Son("louis",5); instance1.colors.push("black"); console.log(instance1.colors);//"red,blue,green,black" instance1.sayName();//louis instance1.sayAge();//5 var instance1 = new Son("zhai",10); console.log(instance1.colors);//"red,blue,green" instance1.sayName();//zhai instance1.sayAge();//10
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式. 而且, instanceof 和 isPrototypeOf( )也能用于识别基于组合继承创建的对象.
同时我们还注意到组合继承其实调用了两次父类构造函数, 造成了不必要的消耗, 那么怎样才能避免这种不必要的消耗呢, 这个我们将在后面讲到.
原型继承
该方法最初由道格拉斯·克罗克福德于2006年在一篇题为 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式继承) 的文章中提出. 他的想法是借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型. 大意如下:
在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.
function object(o){
function F(){} F.prototype = o; return new F(); }
从本质上讲, object() 对传入其中的对象执行了一次浅复制. 下面我们来看看为什么是浅复制.
var person = {
friends : ["Van","Louis","Nick"] }; var anotherPerson = object(person); anotherPerson.friends.push("Rob"); var yetAnotherPerson = object(person); yetAnotherPerson.friends.push("Style"); alert(person.friends);//"Van,Louis,Nick,Rob,Style"
在这个例子中,可以作为另一个对象基础的是person对象,于是我们把它传入到object()函数中,然后该函数就会返回一个新对象. 这个新对象将person作为原型,因此它的原型中就包含引用类型值属性. 这意味着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享.
在 ECMAScript5 中,通过新增 object.create() 方法规范化了上面的原型式继承.
object.create() 接收两个参数:
- 一个用作新对象原型的对象
- (可选的)一个为新对象定义额外属性的对象
var person = {
friends : ["Van","Louis","Nick"] }; var anotherPerson = Object.create(person); anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.friends.push("Style"); alert(person.friends);//"Van,Louis,Nick,Rob,Style"
object.create() 只有一个参数时功能与上述object方法相同, 它的第二个参数与Object.defineProperties()方法的第二个参数格式相同: 每个属性都是通过自己的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性.例如:
var person = {
name : "Van"
};
var anotherPerson = Object.create(person, { name : { value : "Louis" } }); alert(anotherPerson.name);//"Louis"
目前支持 Object.create() 的浏览器有 IE9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome.
提醒: 原型式继承中, 包含引用类型值的属性始终都会共享相应的值, 就像使用原型模式一样.
寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路, 同样是克罗克福德推而广之.
寄生式继承的思路与(寄生)构造函数和工厂模式类似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象. 如下.
function createAnother(original){
var clone = object(original);//通过调用object函数创建一个新对象 clone.sayHi = function(){//以某种方式来增强这个对象 alert("hi"); }; return clone;//返回这个对象 }
这个例子中的代码基于person返回了一个新对象--anotherPerson. 新对象不仅具有 person 的所有属性和方法, 而且还被增强了, 拥有了sayH()方法.
注意: 使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.
寄生组合式继承
前面讲过,组合继承是 JavaScript 最常用的继承模式; 不过, 它也有自己的不足. 组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部. 寄生组合式继承就是为了降低调用父类构造函数的开销而出现的 .
其背后的基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数
function extend(subClass,superClass){
var prototype = object(superClass.prototype);//创建对象 prototype.constructor = subClass;//增强对象 subClass.prototype = prototype;//指定对象 }
extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要,多余的属性. 于此同时,原型链还能保持不变; 因此还能正常使用 instanceof 和 isPrototypeOf() 方法.
以上,寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方法.
下面我们来看下extend的另一种更为有效的扩展.
function extend(subClass, superClass) {
var F = function() {}; F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructor = subClass; subClass.superclass = superClass.prototype; if(superClass.prototype.constructor == Object.prototype.constructor) { superClass.prototype.constructor = superClass; } }
我一直不太明白的是为什么要 "new F()", 既然extend的目的是将子类型的 prototype 指向超类型的 prototype,为什么不直接做如下操作呢?
subClass.prototype = superClass.prototype;//直接指向超类型prototype
显然, 基于如上操作, 子类型原型将与超类型原型共用, 根本就没有继承关系.
new 运算符
为了追本溯源, 我顺便研究了new运算符具体干了什么?发现其实很简单,就干了三件事情.
var obj = {};
obj.__proto__ = F.prototype;
F.call(obj);
第一行,我们创建了一个空对象obj;
第二行,我们将这个空对象的proto成员指向了F函数对象prototype成员对象;
第三行,我们将F函数对象的this指针替换成obj,然后再调用F函数.
我们可以这么理解: 以 new 操作符调用构造函数的时候,函数内部实际上发生以下变化:
1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
2、属性和方法被加入到 this 引用的对象中。
3、新创建的对象由 this 所引用,并且最后隐式的返回 this.
__proto__ 属性是指定原型的关键
以上, 通过设置 __proto__ 属性继承了父类, 如果去掉new 操作, 直接参考如下写法
subClass.prototype = superClass.prototype;//直接指向超类型prototype
那么, 使用 instanceof 方法判断对象是否是构造器的实例时, 将会出现紊乱.
假如参考如上写法, 那么extend代码应该为
function extend(subClass, superClass) {
subClass.prototype = superClass.prototype;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) { superClass.prototype.constructor = superClass; } }
此时, 请看如下测试:
function a(){}
function b(){} extend(b,a); var c = new a(){}; console.log(c instanceof a);//true console.log(c instanceof b);//true
c被认为是a的实例可以理解, 也是对的; 但c却被认为也是b的实例, 这就不对了. 究其原因, instanceof 操作符比较的应该是 c.__proto__ 与 构造器.prototype(即 b.prototype 或 a.prototype) 这两者是否相等, 又extend(b,a); 则b.prototype === a.prototype, 故这才打印出上述不合理的输出.
那么最终,原型链继承可以这么实现,例如:
function extend(subClass, superClass) {
var F = function() {}; F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructor = subClass; subClass.superclass = superClass.prototype; if(superClass.prototype.constructor == Object.prototype.constructor) { superClass.prototype.constructor = superClass; } }
function Father(name){
this.name = name; this.colors = ["red","blue","green"]; } Father.prototype.sayName = function(){ alert(this.name); }; function Son(name,age){ Father.call(this,name);//继承实例属性,第一次调用Father() this.age = age; } extend(Son,Father)//继承父类方法,此处并不会第二次调用Father() Son.prototype.sayAge = function(){ alert(this.age); } var instance1 = new Son("louis",5); instance1.colors.push("black"); console.log(instance1.colors);//"red,blue,green,black" instance1.sayName();//louis instance1.sayAge();//5 var instance1 = new Son("zhai",10); console.log(instance1.colors);//"red,blue,green" instance1.sayName();//zhai instance1.sayAge();//10
扩展:
属性查找
使用了原型链后, 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined. 此时若想避免原型链查找, 建议使用 hasOwnProperty 方法. 因为hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数. 如:
console.log(instance1.hasOwnProperty('age'));//true
对比: isPrototypeOf 则是用来判断该方法所属的对象是不是参数的原型对象,是则返回true,否则返回false。如:
console.log(Father.prototype.isPrototypeOf(instance1));//true
instanceof && typeof
上面提到几次提到 instanceof 运算符. 那么到底它是怎么玩的呢? 下面让我们来趴一趴它的使用场景.
instanceof 运算符是用来在运行时指出对象是否是构造器的一个实例, 例如漏写了new运算符去调用某个构造器, 此时构造器内部可以通过 instanceof 来判断.(java中功能类似)
function f(){
if(this instanceof arguments.callee) console.log('此处作为构造函数被调用'); else console.log('此处作为普通函数被调用'); } f();//此处作为构造函数被调用 new f();//此处作为普通函数被调用
以上, this instanceof arguments
.callee 的值如果为 true 表示是作为构造函数被调用的,如果为 false 则表示是作为普通函数被调用的。
对比: typeof 则用以获取一个变量或者表达式的类型, 一般只能返回如下几个结果:
number,boolean,string,function(函数),object(NULL,数组,对象),undefined。
new运算符
此处引用 艾伦的 JS 对象机制深剖——new 运算符
接着上述对new运算符的研究, 我们来考察 ECMAScript 语言规范中 new 运算符的定义:
The new Operator
The production NewExpression : new NewExpression is evaluated as follows:Evaluate NewExpression.Call GetValue(Result(1)).If Type(Result(2)) is not Object, throw a TypeError exception.If Result(2) does not implement the internal [[Construc]] method, throw a TypeError exception.Call the [[Construct]] method on Result(2), providing no arguments (that is, an empty list of arguments).Return Result(5).
其大意是,new 后必须跟一个对象并且此对象必须有一个名为 [[Construct]] 的内部方法(其实这种对象就是构造器),否则会抛出异常
根据这些内容,我们完全可以构造一个伪 [[Construct]] 方法来模拟此流程
function MyObject(age) {
this.age = age; } MyObject.construct = function() { var o = {}, Constructor = MyObject; o.__proto__ = Constructor.prototype; // FF 支持用户引用内部属性 [[Prototype]] Constructor.apply(o, arguments); return o; }; var obj1 = new MyObject(10); var obj2 = MyObject.construct(10); alert(obj2 instanceof MyObject);// true