①类式继承(构造函数)
JS中其实是没有类的概念的,所谓的类也是模拟出来的。特别是当我们是用new 关键字的时候,就使得“类”的概念就越像其他语言中的类了。类式继承是在函数对象内调用父类的构造函数,使得自身获得父类的方法和属性。call和apply方法为类式继承提供了支持。通过改变this的作用环境,使得子类本身具有父类的各种属性。
var father = function() {this.age = 52;this.say = function() {alert('hello i am '+ this.name ' and i am '+this.age + 'years old');}}var child = function() {this.name = 'bill';father.call(this);}var man = new child();man.say();
②原型继承
原型继承在开发中经常用到。它有别于类继承是因为继承不在对象本身,而在对象的原型上(prototype)。每一个对象都有原型,在浏览器中它体现在一个隐藏的__proto__属性上。在一些现代浏览器中你可以更改它们。比如在zepto中,就是通过添加zepto的fn对象到一个空的数组的__proto__属性上去,从而使得该数组成为一个zepto对象并且拥有所有的方法。话说回来,当一个对象需要调用某个方法时,它回去最近的原型上查找该方法,如果没有找到,它会再次往下继续查找。这样逐级查找,一直找到了要找的方法。 这些查找的原型构成了该对象的原型链条。原型最后指向的是null。我们说的原型继承,就是将父对像的方法给子类的原型。子类的构造函数中不拥有这些方法和属性。
var father = function() {}father.prototype.a = function() {}var child = function(){}//开始继承child.prototype = new father();var man = new child();man.a();可以看到第七行实现了原型继承。很多人并不陌生这种方式。通过在浏览器中打印man我们就可以查看各个原型的继承关系。
可以看到逐级的关系child->object(father实例化的对象)->father。child是通过中间层继承了father的原型上的东西的。但是为什么中间还有一层object呢,为什么不把child.prototype = father.prototype。答案是如果这样做child和father就没有区别了。大家应该还记得在prototype中有个constructor属性,指向的是构造函数。按照正常的情况我们要把constructor的值改回来指向child的构造函数。但如果直接把father.prototype赋值给child.prototype,那么constructor应该指向谁呢?所以很显然只能通过中间层才能使得child和father保持为独立的对象。
类式继承和原型继承的对比
构造函数(类)式继承
首先,构造函数继承的方法都会存在父对象之中,每一次实例,都会将funciton保存在内存中,这样的做法毫无以为会带来性能上的问题。
其次,类式继承是不可变的。无法复用,在运行时,无法修改或者添加新的方法,这种方式是一种固步自封的死方法。实践中很少单纯使用。
原型继承
优点:
原型链可改变:原型链上的父类可替换可扩展
可以通过改变原型链接而对子类进行修改的。另外就是类式继承不支持多重继承,而对于原型继承来说,你只需要写好extend对对象进行扩展即可。
但是原型链继承也有2个问题。
第一,包含引用类型值的原型属性会被所有实例共享(可以这样理解:执行sub1.arr.push(2);先对sub1进行属性查找,找遍了实例属性(在本例中没有实例属性),没找到,就开始顺着原型链向上找,拿到了sub1的原型对象,一搜身,发现有arr属性。于是给arr末尾插入了2,所以sub2.arr也变了)。
第二,在创建子类型的实例时,不能向超类型的构造函数中传递参数。(实际上,应该说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数)实践中很少单纯使用原型链。
function Super(){this.val = 1;this.arr = [1];}function Sub(){// ...}Sub.prototype = new Super(); // 核心var sub1 = new Sub();var sub2 = new Sub();sub1.val = 2;sub1.arr.push(2);alert(sub1.val); // 2alert(sub2.val); // 1alert(sub1.arr); // 1, 2alert(sub2.arr); // 1, 2
总结:
类式继承在实例化时,父类可传参,不能复用(父类不可变,每一次实例都会将父类内容保存在内存中)
原型继承在实例化时,父类不可传参,可以复用(原型链可改变(父类可替换可扩展),父类不会保存在内存中,而是顺着原型链查找,但是结果是原型属性会被所有实例共享(尤其影响引用类型值))
③组合继承(最常用)
组合继承将原型链和借用构造函数的技术结合到一起,发挥两者之长的一种继承模式。思路是使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。
function SuperType(name){this.name = name;this.numbers = [1,2,3];}SuperType.prototype.sayName = function(){console.log(this.name);}function SubType(name,age){SuperType.call(this,name);this.age = age;}SubType.prototype = new SuperType();SubType.prototype.sayAge = function(){console.log(this.age);}var instance1 = new SubType('aaa',21);instance1.numbers.push(666);console.log(instance1.numbers);instance1.sayName();instance1.sayAge();var instance2 = new SubType('bbb',22);console.log(instance2.numbers);instance2.sayName();instance2.sayAge();
把实例函数都放在原型对象上,通过Sub.prototype = new Super();继承父类函数,以实现函数复用。
保留借用构造函数方式的优点,通过Super.call(this);继承父类的基本属性和引用属性,以实现传参;
优缺点
优点:
- 可传参
- 函数可复用
- 不存在引用属性共享问题(图纸)
缺点:
子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,生成了两份,而子类实例上的那一份屏蔽了子类原型上的造成内存浪费。(下面会有方法解决这个问题)
④原型式继承
思想:在一个函数内部创建一个临时性的构造函数,将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新的实例。
function object(o){ function F(){} F.prototype = o; return new F();}var person = { name:'wang', friends:['cc','jj','gg']}var anotherPerson = object(person);anotherPerson.name = 'lee';anotherPerson.friends.push('hh');var anotherPerson2 = object(person);anotherPerson2.name = 'joke';anotherPerson2.friends.push('mm');console.log(person.friends);//cc jj gg hh mm
es5中Object.create()方法规范了原型式继承,传入一个参数情况下是和object()方法行为相同,传入第二个参数可以为新对象添加新的属性,但会覆盖原型对象上的同名属性。
下面是Object.create方法的Polyfill
if (typeof Object.create != 'function') { // Production steps of ECMA-262, Edition 5, 15.2.3.5 // Reference: http://es5.github.io/#x15.2.3.5 Object.create = (function() { //为了节省内存,使用一个共享的构造器 function Temp() {} // 使用 Object.prototype.hasOwnProperty 更安全的引用 var hasOwn = Object.prototype.hasOwnProperty; return function (O) { // 1. 如果 O 不是 Object 或 null,抛出一个 TypeError 异常。 if (typeof O != 'object') { throw TypeError('Object prototype may only be an Object or null'); } // 2. 使创建的一个新的对象为 obj ,就和通过 // new Object() 表达式创建一个新对象一样, // Object是标准内置的构造器名 // 3. 设置 obj 的内部属性 [[Prototype]] 为 O。 Temp.prototype = O; var obj = new Temp(); Temp.prototype = null; // 不要保持一个 O 的杂散引用(a stray reference)... // 4. 如果存在参数 Properties ,而不是 undefined , // 那么就把参数的自身属性添加到 obj 上,就像调用 // 携带obj ,Properties两个参数的标准内置函数 // Object.defineProperties() 一样。 if (arguments.length > 1) { // Object.defineProperties does ToObject on its first argument. var Properties = Object(arguments[1]); for (var prop in Properties) { if (hasOwn.call(Properties, prop)) { obj[prop] = Properties[prop]; } } } // 5. 返回 obj return obj; }; })();}
如果使用Object.create方法的话,那么代码就可以变为
function object(o){ function F(){} F.prototype = o; return new F();}var person = { name:'wang', friends:['cc','jj','gg']}var anotherPerson = Object.create(person,{ name:{ value:'gre' }});
优点:
引出了使用临时构造函数的这种思路,为后面要介绍的继承方式奠定基础
缺点:
这种继承方式和原型继承十分类似,也存在原型继承中引用类型属性共享的问题。当然,父类也不能传入参数。
⑤寄生式继承
思想:类似工厂模式,创建一个仅用于封装继承过程的函数,在该函数内部增强对象,最后返回对象
一种做法:在原型式继承基础上再封一层函数,在这个函数里面为原型式继承返回的对象进行补强
function createObj(o){ var obj = Object.create(o); obj.say = function(){ console.log('hi'); } return obj;}var person = { name:'wang', friends:['aa','bb']}var subPerson = createObj(person);subPerson.say();//hi
优点:
引出再次封装的思想
缺点:
这个封装的方法里面补强的内容是写死的,整个函数不能复用,降低了效率。
原型式继承和寄生式继承
这两种继承自身都有不少问题,但是介绍这两个只是为了介绍思想,引出下文的寄生式组合式继承,所以对两种继承的优缺点没有做太过详细的分析。这两种继承方式也不会经常用到。
⑥寄生组合式继承(最理想的继承范式)
思想:子类的原型直接指向拥有父类原型的对象(当然subType.prototype = superType.prototype是不行的),这个对象还不会拥有父类的自有属性。
方法:通过寄生式继承中的原型式继承方法获取没有父类自有属性的原型对象并补强对象(重写construct属性),通过构造函数实现属性的继承。
function inheritPrototype(subType,superType){ var prototype = Object.create(superType.prototype); prototype.construct = subType;//(重写原型失去了默认的construct属性) subType.prototype = prototype;}function SuperType(name){ this.name = name; this.colors = ['aa','bb','cc'];}SuperType.prototype.sayName = function(){ console.log(this.name);}function SubType(name,age){ SuperTyep.call(this,name); this.age = age;}inheritPrototype(SubType,SuperType);SubType.prototype.sayAge = function(){ console.log(this.age);}
优点:
既可以复用又可以传参
参考资料:
《JavaScript高级程序设计》
博客:http://blog.csdn.net/liuqiao1123/article/details/51330214
博客:http://www.cnblogs.com/ayqy/p/4471638.html