构造函数借用构造函数继承

上次博客跟大家分享了自己对原型链继承的理解,想看的同学欢迎猛击这里,上次说到原型链继承有一些问题,主要是两方面的。我们举个栗子来说明下:

Q1:共享的超类属性能被任何实例改写,这个是很危险的!看下面一段代码:


     
     
     
     
  1. function Person(name){
  2. this.name=name;
  3. this.countries=[ “America”, “China”, “Canada”];
  4. }
  5. Person.prototype.sayName= function(){
  6. return this.name;
  7. }
  8. function Student(age){
  9. this.age=age;
  10. }
  11. Student.prototype= new Person();
  12. var s1= new Student();
  13. s1.countries.push( “india”);
  14. console.log(s1.countries); //[“America”, “China”, “Canada”, “india”]
  15. var s2= new Student();
  16. console.log(s2.countries); //[“America”, “China”, “Canada”, “india”]

大家看到,我对s1对象的countries熟悉进行了修改,但是当我new出另外一个Studnet实例的时候,你会发现它的counties属性也被修改了。这段代码说明,从表面上看,任何对实例属性的修改,都会反映到其他实例上。

那么怎么解释这样的现象呢?用一张图来简要的说明下
构造函数借用构造函数继承_第1张图片

大家可以看到,其实当把Person的实例赋给Student的原型后,countries属性其实已经存在了Student的原型对象中。而此时s1,s2实例均是指向Student的原型对象的,所以不论你怎么更改countries,都会反映到原型对象中的countries值中。这样就解释了为什么s2的countries属性也会被修改。

接下来,我们来看原型链继承的第二个问题:

Q2:在创建子类型的实例时,不能向超类型的构造函数中传递参数;看下面的一段代码:


     
     
     
     
  1. function Person(name){
  2. this.name=name;
  3. this.countries=[ "America", "China", "Canada"];
  4. }
  5. Person.prototype.sayName= function(){
  6. return this.name;
  7. }
  8. function Student(name,age){
  9. this.age=age;
  10. }
  11. Student.prototype= new Person();
  12. var s1= new Student( "bob", 25);
  13. console.log(s1.name); //undefined

当我打印s1的name属性时,是undefined,这就是第二个问题的描述!好了,既然有问题,我们就来解决!

一,借用构造函数

在解决问题的过程中,借用构造函数也可以成为:“伪造对象”,“经典继承”。这种技术也就说在子类型构造函数的内部调用超类型构造函数, 其实函数只不过是在特定环境中执行代码的对象(这句话很经典,在《高程》上面看到的,初学js的同学可以反复揣摩这句话,相信你每一遍对这句话的思考,都能有新的启发,说不定你之前有些不能理解的问题,这里能下子豁然开朗!)
好了,有了上面这些铺垫,我们只需要一种工具,能够在子类型的构造函数中调用超类型的构造函数。这里apply()和call()就派上用场了。
说到这里,之前也看了很多关于call和apply的介绍,这个函数看起来很神秘,其实说起来就一句话,在特定的函数环境里执行另外一个函数!
看下面一段代码:

      
      
      
      
  1. function Person(name){
  2. this.name=name;
  3. this.countries=[ “America”, “China”, “Canada”];
  4. }
  5. function Student(name,age){
  6. Person.call( this,name); //在这里借用了Person的构造函数
  7. this.age=age;
  8. }
  9. var s1= new Student( “bob”, 25);
  10. s1.countries.push( “india”);
  11. console.log(s1.countries); //[“America”, “China”, “Canada”, “india”]
  12. console.log(s1.name); //bob
  13. var s2= new Student( “finnya”, 23);
  14. console.log(s2.countries); //[“America”, “China”, “Canada”]
  15. console.log(s2.name); //finnya
从上面的代码中可以看到,通过是用call()方法(或者apply()方法),我们就是在Student实例的环境下调用了Person构造函数。这样一来,新的Student对象中就有了Person构造函数里面的初始化代码。结果就是Student的每个实例当中都有了countries属性的副本。
但是这里有个坑,就是子类的构造函数中新增的属性,一定要放在借用构造函数之后,否则,就会被超类的构造函数重写,什么意思呢,看下面一段代码:

      
      
      
      
  1. function Person(name){
  2. this.city= “北京”;
  3. this.name=name;
  4. this.countries=[ “America”, “China”, “Canada”];
  5. }
  6. function Student(name,age){
  7. this.city= “上海”;
  8. Person.call( this,name);
  9. this.age=age;
  10. //this.city=”上海”;//如果将新增属性放在借用函数以后,就不会出现这个问题
  11. }
  12. var s1= new Student( “bob”, 25);
  13. s1.countries.push( “india”);
  14. console.log(s1.city); //这里输出的是北京,前面新增的Student实例属性并没有体现出来

到此为止,关于借用构造函数的继承方法已经完成了。但是,细心的同学会发现,借用构造函数方法也有它自己弊端,因为每一个子类的对象实例都拥有自己的实例属性,这样一来函数复用就无从谈起了。而且在超类型的原型中定义的方法,对子类型而言也是不可见的。所以,其实我们一般在实现继承的过程中,都不会单纯的使用这种继承模式。那么这个问题怎么解决呢?请大家接着看下面一部分:

二,组合式继承

其实,这里的组合式继承,说白了,就是将原型链继承和借用构造函数继承的技术组合到一块,让他们各自发挥自己的优点,同时又可以弥补各自存在的缺点。组合继承实现的思路其实很简单,我们利用原型链继承实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样一来,即实现了函数的复用,又保证了每个实例都有自己的属性。看下面的代码:

       
       
       
       
  1. function Person(name){
  2. this.name=name;
  3. this.countries=[ “America”, “China”, “Canada”];
  4. }
  5. Person.prototype.sayName= function(){
  6. return this.name;
  7. }
  8. function Student(name,age){
  9. Person.call( this,name);
  10. this.age=age;
  11. }
  12. Student.prototype= new Person();
  13. Student.prototype.sayAge= function(){
  14. return this.age;
  15. }
  16. var s1= new Student( “bob”, 25);
  17. s1.countries.push( “india”);
  18. console.log(s1.countries); //[“America”, “China”, “Canada”, “india”]
  19. console.log(s1.name); //bob
  20. console.log(s1.sayAge()) //25
  21. var s2= new Student( “finnya”, 23);
  22. console.log( “————————”);
  23. console.log(s2.countries); //[“America”, “China”, “Canada”]
  24. console.log(s2.name); //finnya
  25. console.log(s2.sayAge()) //23
通过上面的例子,可以看到,当我们把两种继承的方式融合到一起以后,Student实例分别拥有了各自的name,countries属性,又共享了Person的原型方法sayName。所以说,组合继承避免了原型链和借用构造函数的缺点,融合了各自的优点。当然,这里我们仍然可以使用之前谈到的两种方法去检测实例和原型的关系,instanceof和isPrototypeOf(),具体怎么检测和其中的问题,请大家参考我之前一篇博客—-原型链式继承。

好了,到这里,关于继承的事情基本谈完了,下面说一下一个知识扩充:

三,call和apply的用法

之前,到网上查询这两个方法的使用,看了很多的文章,给大家列举下一些不太常见(或者说特殊)的用法:

第一种,大家知道,调用一个方法时,我们可以使用:

       
       
       
       
  1. function foo(){
  2. alert( 1);
  3. }
  4. foo();
  5. //同时,也可以这么调用
  6. foo.call();
那么这里怎么解释这样的写法呢?前面我用红色的字体标记过一句话”函数只不过是在特定的环境中执行代码的对象“,我们使用call调用时,call函数的本质是需要执行这个函数的(这里可以联想到另外一个方法bind,它只是绑定函数并生成一个新的函数,并不去执行,大家一定要注意,有时间,我会将这个函数进行扩展!),那么其实这里相当于在window域里面去调用foo函数,所以这个问题就迎刃而解了,只是调用函数的一种方式而已。

第二种用法,
({}).toString.apply(‘str’);//这里返回的是[object String]
       
       
       
       
上面的这种用法,一般是用来检测一个对象的类型, 通常,
({}).toString.call([Class]);//返回的是[object Class],传入的参数不一样,出现的Class就不一样
       
       
       
       
说白了,我们前面的一段代码可以这样理解,我下面只是一个伪代码,可以这样去理解,但真正的结果还是有区别的!!!!

       
       
       
       
  1. var str= “str”;
  2. str.toStirng();
关于更多的理解,本文篇幅有限,有时间我会再去补充,大家有什么疑问,可以给我留言!

第三种用法,大家看一下下面的一段代码,如何实现两个数组拼接,组成新的数组:

       
       
       
       
  1. var arr1=[ “a”, “b”, “c”];
  2. var arr2=[ “d”, “e”, “f”];
  3. // arr1.push(arr2)
  4. // console.log(arr1);//[“a”, “b”, “c”, Array[3]]
  5. Array.prototype.push.apply(arr1,arr2);
  6. console.log(arr1); //[“a”, “b”, “c”, “d”, “e”, “f”]
上面的第一种调用方法中只是把arr2整个数组加到了arr1中,但是第二种调用方式,能够出现正常的结果!什么原因呢?因为大家知道,apply方法的第二个参数是一个数组,这里在方法内部,肯定是是实现了一个数组循环,然后把数组中每个值分开进行push操作,这里正是利用了apply方法的这个特性,将arr2自动循环解析。

第四种用法,我们来让Math.max能够从数组中取出最大的元素

       
       
       
       
  1. var arr1=[ “1”, “2”, “3”];
  2. console.log( Math.max(arr1)); //NaN
  3. console.log( Math.max.apply(arr1)); //-Infinity
  4. console.log( Math.max.apply( null,arr1)); //3
大家看上面的第一个调用,可以知道,其实Math.max()只能接受(1,,2,3)这样一个一个的值,传入数组时,它是没法工作的。这里我们想要Math.max()能够处理数组,那么只能请回apply来处理了,它能够将传入的数组,变成一个接一个的参数的形式,这样Math.max()就能够返回正常的最大值了。
说到这里,顺便提一下apply/call的入参问题,大家要注意,当apply只有一个入参的时候,相当于传入的参数缺省,也就是说唯一的入参就是obj,这也就能够解释为什么上面的第二种调用方式会出现没法判断的问题,因为相当于直接调用Math.max(),没有给入参,这时,返回的就是-Infinity。
然而,如果传入的第一个入参是null,或者undefined时,this默认指向window或者global,这里是需要注意的地方!

【致谢】依旧感谢《高程》的陪伴,给我了许多的参考。本人前端的菜鸟,文中肯定有理解不对的地方,希望各位前辈给我留言指导!时间仓促,有表述不对的地方,我会后续进行修改!望各位理解!








转自: crowds123的专栏

你可能感兴趣的:(js)