JavaScript面向对象(4)——最佳继承模式(深拷贝、多重继承、构造器借用、组合寄生式继承)

来源:互联网 发布:ic卡读写器单片机 编辑:程序博客网 时间:2024/07/24 02:48

       很多同学甚至在相当长的时间里,都忽略了JavaScript也可以进行面向对象编程这个事实。一方面是因为,在入门阶段我们所实现的各种页面交互功能,都非常顺理成章地使用过程式程序设计解决了,我们只需要写一些方法,然后将事件绑定在页面中的DOM节点上便可以完成。尤其像我这类一开始C++这类语言没好好学,第一门主力语言就是JavaScript的同学来说,过程化程序设计的思维似乎更加根深蒂固。另一方面,就算是对于Java、C++等语言的程序员来说,JavaScript的面向对象也是一个异类:JavaScript中没有class的概念(在ES5及之前版本中没有,ES6会单独介绍),其基于prototype的继承模式也与传统面向对象语言不同,而JavaScript的弱类型特性更会令这里面的很多人抓狂。当然,在熟悉了之后,这种灵活性也会带来很多好处。总之,封装、继承、多态、聚合这些面向对象的基本特性JavaScript都有其自己的实现方式,这些知识的学习是从入门级JS程序员进阶的必经之路。

JavaScript面向对象(1)——谈谈对象

JavaScript面向对象(2)——谈谈函数(函数、对象、闭包)

JavaScript面向对象(3)——原型与基于构造函数的继承模式(原型链)

JavaScript面向对象(4)——最佳继承模式(深拷贝、多重继承、构造器借用、组合寄生式继承)



一、基于对象工作模式的继承

        我们知道,JavaScript中创建对象主要有两种方式:构造函数与对象直接量。上一篇中介绍的三种继承方法也都是基于构造函数进行工作的,这种方法更类似与Java式的继承方式,构造函数和原型对象就相当于Java中的类了。 然而,JavaScript中终究是没有类的概念的,一切的核心还是对象。下面介绍的就是这类方法:

        1、浅拷贝

//浅拷贝function extend(p){var obj = {};for(var i in p) obj[i] = p[i];obj.father = p;return obj;}var fatherObj = {name: 'father',toString: function(){return this.name;}}var a = extend(fatherObj);a.name = 'aaa';a.toString();  // 'aaa'

        这个继承函数的唯一参数是父对象(注意这里接受的是父对象,也就是父类的实例对象。上一节的拷贝法中接受的是父类的构造函数对象),将父对象的全部属性拷贝至子对象中,并在子对象中添加father属性以方便引用父对象。当然了,由于是直接拷贝,父对象中值为对象的属性依然是以引用的方式拷贝的,在子对象中修改此类属性会影响到父对象。 下面是这种方法得到的a对象的结构



        2、深拷贝

//深拷贝function deepCopy(p, c){var c = c || {};for( var i in p){if(typeof p[i] === 'object') {c[i] = (p[i].constructor === Array) ? [] : {};deepCopy(p[i], c[i]);}else if(typeof p[i] === 'function'){c[i] = p[i].prototype.constructor;}else c[i] = p[i];}return c;}var fatherObj = {name: 'father',hobby: ['football','basketball'],toString: function(){return this.hobby}}//测试var a = deepCopy(fatherObj);console.log(a.toString());  // ['football','basketball']console.log(a.hobby === fatherObj.hobby); //falseconsole.log(a.toString === fatherObj.toString); //false

        相对于之前的浅拷贝,深拷贝则是对于对象做了特殊的处理:在遍历父对象属性是,一旦发现该对象为对象属性,递归调用自身将该对象进行复制。另外,由于函数对象无法直接通过属性遍历的方法进行深拷贝,这里通过访问方法对象的原型对象的constructor属性并将其进行赋值这个小技巧,完成了属性的深拷贝。这个方法由于在处理对象深拷贝时需要递归调用,没有在方法内添加父对象的引用,在使用的时候可以手动进行添加或者对这个方法进行二次封装。

        拷贝与深拷贝其实也是聚合的实现了,将其他对象的属性拿过来扩展自身对象。若是两对象为父级子级关系,则为继承;若是两对象同级扩展,则可以视作聚合。其核心点就是深拷贝。


         3、通过直接设置原型对象进行继承

//直接设置原型对象function extend(p) {function F(){};F.prototype = p;var c = new F();c.father = p;return c;}
        这个方法接受父对象为唯一参数,并将父对象设置为临时构造器的原型对象,构造出子对象,完成继承。Object对象中包含了create方法,功能与这个大概一致,都是接受一个对象作为参数,返回以该对象为原型对象的新对象,MDN中有详细的解释:MDN:Object.create()


        4、多重继承

        显然,JavaScript不可能为多重继承提供语法单元。但是对于JavaScript这类语言来说,模拟出多重继承也是非常容易的。这里提供了一种基于对象拷贝的多重继承实现:

//多重继承function multiple(){var c = {},stuff,len = arguments.length;c['father'] = [];for(var j = 0;j < len;j++){stuff = arguments[j];for(var i in stuff) c[i] = stuff[i];c['father'].push(stuff);}return c;}
         JavaScript中实参的个数可以多于形参,利用这个特性我们可以方便的处理任意数量个参数。这里的方法就可以从任意个对象中继承属性,将这些属性拷贝至新对象中,并将父对象的引用添加值father属性中,将构造完成的子对象返回。 同样的,可以轻松地将这个方法改写成现有对象之间的继承:

//多重继承2function multiple(/*第一个参数为子对象,其余为父对象*/){var c = arguments[0],stuff,len = arguments.length;c['father'] = [];for(var j = 1;j < len;j++){stuff = arguments[j];for(var i in stuff) c[i] = stuff[i];c['father'].push(stuff);}return c;}
          当然了,若遇到同名属性,会按照先后次序覆盖。

二、构造函数借用

        还有一类很重要的继承实现方式,称为构造器借用(构造函数借用)。这里是利用了call()或apply()方法在子对象构造函数中调用父对象的构造函数。

//构造器借用function Animal(age){this.age = age;}Animal.prototype.getAge = function(){ return this.age + ' years old.'};function Bird(){Animal.apply(this, arguments);}Bird.prototype.className = 'Bird';var a = new Bird(10);console.log( a.className ); // 'Bird'console.log( a.age); // 10console.log( a.getAge ); // undefined
          在这种继承模式中,子对象不会继承父对象的原型属性,只会将父对象在构造函数中定义的属性重建在自身属性中。并且遇到值为对象的属性时,也会获得一个新值,而不是父级该值的引用。同时,对子对象所做的任何修改都不会影响父对象。  


三、找出最佳的继承方法

        1、寄生式继承

        这个方法其实是对原型对象法的升级,将继承后对象的扩展也封装进方法中。“这样在创建对象的函数中直接吸收其他对象的功能,进行扩展并返回,好像所有工作都是自己做的”,便是寄生式继承名字的由来了。这里直接使用了Object.create()方法,也可以用上文中给出的方法。

//寄生式继承function extend(p){var c = Object.create(p);//在此对c进行扩展,添加子对象的自有属性和方法//......//......return c;}

        2、组合继承

        这个方法是构造器借用法的延伸。由于构造器借用法无法继承原型属性,无法实现函数复用。便在该方法上做了简单改动: BIrd.prototype = new Animal()

//组合继承function Animal(age){this.age = age;}Animal.prototype.getAge = function(){ return this.age + ' years old.'};function Bird(){Animal.apply(this, arguments);}Bird.prototype.className = 'Bird';var a = new Bird(10);console.log( a.className ); // 'Bird'console.log( a.age ); // 10console.log( a.getAge ); // '10 years old.'

        然而,这种方式也有个明显的缺点。在继承的过程中,父对象的构造函数会被调用两次:apply方法会调用一次,随后调用子对象构造函数时又会调用一次。父对象的自身属性实际上被继承了两次:

function Animal(age){this.age = age;}function Bird(){Animal.apply(this, arguments);}Bird.prototype = new Animal(100)Bird.prototype.className = 'Bird';var a = new Bird(200);console.log(a.age); // 200console.log(a.__proto__.age); // 100delete a.age;console.log(a.age); // 100

        从a对象的结构中可以清晰的看出,其自身重建了父级属性age,又从原型中继承了age,该属性被继承了两次。

        将继承原型的方式从本例中的方法替换为 JavaScript面向对象(3)——原型与基于构造函数的继承模式(原型链) 中的最后一种方法可以更正双重继承的问题。然而由于该方法本身的问题与局限性,这还不是最佳的方案。


        3、最佳继承方法: 组合寄生式继承

        说了这么多,终于该引出最佳方法了:组合寄生式继承法。这里直接搬出红宝书里的经典源码:

function inherit(subType, superType){    var protoType = Object.create(superType.prototype);     protoType.constructor = subType;         subType.prototype = protoType; }
        该方法接受子类和父类构造函数作为参数,构造出子类构造函数的原型对象,完成原型的继承,再配合组合式继承法的其余部分:

function inherit(subType, superType){    var protoType = Object.create(superType.prototype);     protoType.constructor = subType;         subType.prototype = protoType; }function Animal(age){this.age = age;}Animal.prototype.getAge = function (){ return '11'};function Bird(){Animal.apply(this, arguments);}inherit(Bird, Animal);Bird.prototype.className = 'Bird';Bird.prototype.getName = function(){ return '22'};


        这便是目前公认最佳的JavaScript继承的实现模式了。

阅读全文
0 0
原创粉丝点击