javascript中继承的几种方式
为什么需要继承?
在实际编码的过程中,如果有很多类似的方法都存放于构造函数中,这样会导致内存的浪费(内存泄漏),在这种情况下我们就需要用到继承。
继承是什么?
所谓继承就是通过某种方式让一个对象可以访问到另一个对象中的属性和方法。
在JavaScript中常用的几种继承方式
- 原型链继承
- 借用构造函数继承
- 组合模式继承
- 共享原型继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
- ES6中class的继承(新)
一、原型链继承
通过实例化一个函数使子类的原型指向父类的实例,子类就可以调用到父类的属性和方法。代码实现如下:
function Parent() { this.parentName = '父类';}Parent.prototype.getParentName = function() { return this.parentName;};function Child() { this.childName = '子类';}Child.prototype = new Parent();Child.prototype.getChildName = function() { return this.childName};var c = new Child();console.log(c.getParentName()); // '父类'
要注意的问题
1.不要忘记默认的类型:
在js中所有的引用类型都继承了Object,而继承也是通过原型链实现的。所有存在于Object原型对象上的方法同样也存在。比如:hasOwnProperty()、toString()、valueOf()等;
2.确定原型和实例的关系:
- 第一种:使用instanceof 操作符,这个操作符用于测试实例于原型链中的构造函数,如果存在则返会true反之返回false;
- 第二种:使用isPrototyoeOf()方法。该方法测试原型链中的原型,只要是原型链中出现过的原型,都可以说是该原型链派生出的实例的原型,因此只要是出现在原型链中的方法用该方法都会返回true反之返回false
用代码打印上述代码的对比关系:
console.log(c instanceof Object); //trueconsole.log(c instanceof Parent); //trueconsole.log(c instanceof Child); //trueconsole.log(Object.prototype.isPrototypeOf(c)); //trueconsole.log(Parent.prototype.isPrototypeOf(c)); //trueconsole.log(Child.prototype.isPrototypeOf(c)); //true
3.子类要在继承后定义新方法:
因为,原型链的实质使重写原型对象。若在继承前为子类的prototype上定义方法,这些方法将在继承后被覆盖。
4.不能够使用对象字面量创建原型方法
如上所说,原型链的实质是重写原型对象。当使用字面量创建原型方法的时候,实质上相当于重写了原型链,导致原来的原型链被切断。
function Parent() { this.parentName = '父类';}Parent.prototype.getParentName = function() { return this.parentName;};function Child() { this.childName = '子类';}// 继承 ParentChild.prototype = new Parent();// 使用对象字面量添加新方法,会导致上一行代码无效Child.prototype = { getChildName: function() { return this.childName; }, someOtherMethod: function() { return false; }}var c = new Child()console.log(c.getParentName) // undefined
5.注意父类包含引用类型的情况:
function Parent() { this.name = "父类"; this.hobbies = ["sing", "dance", "rap"];}function Child() {}// 继承 ParentChild.prototype = new Parent();var c1 = new Child();c1.name = "c1";c1.hobbies.push("coding");console.log(c1.name);console.log(c1.hobbies);var c2 = new Child();console.log(c2.name);console.log(c2.hobbies);
上面代码运行结果如下:
"c1"["sing", "dance", "rap", "coding"]"父类"["sing", "dance", "rap", "coding"]
原型链继承的优缺点:
优点:写法方便简洁、容易理解;
缺点:在父类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型原型上变成原型属性被所有子类型实例所共享。同时在创建子类型的实例时,不能向超类型的构造函数中传递参数。
二、借用构造函数继承(对象冒充)
在解决原型链继承中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术。
该技术的思路相当简单——在子类型的构造函数中调用超类型的构造函数。
function Parent(name) { this.name = name; this.hobbies = ["sing", "dance", "rap"];}function Child(name) { Parent.call(this, name); this.age = 24}var c1 = new Child('c1');var c2 = new Child('c2');c1.hobbies.push('coding');console.log(c1.hobbies)console.log(c2.hobbies)console.log(c1 instanceof Parent)console.log(c1 instanceof Child)
上述代码的输出结果如下:
["sing", "dance", "rap", "coding"]["sing", "dance", "rap"]falsetrue
借用构造函数的基本思想就是利用 call 或者 apply 把父类中通过this指定的方法复制到子类创建的实例中。
因为this对象是在运行时基于函数的执行环境绑定的。那也就是说,在全局中,this等于window,而当函数被作为某个对象的方法调用时,this就等于那个对象。 call、apply方法可以替代另一个对象调用一个方法。call、apply方法可以将一个函数的对象上下文从初始的上下文改变为thisObj指定的新对象。
所以,这个借用构造函数就是,new 对象的时候,创建了一个新的实例对象,并且执行Child里面的代码,而Child里面用 call 调用了Parent,也就是说把this指向改成了指向新的实例,所以就会把Parent里面的this相关属性及方法赋值到新的实例上,而不是赋值到Child里面。所有实例中就拥有了父类定义的这些this里面的方法。
借用构造函数继承的优点:
- 解决了引用类型的值被实例共享的问题
- 可以向超类传递参数
- 可以是按多继承(call若干个超类)
借用构造函数继承的缺点
- 不能继承超类原型上的属性和方法
- 无法实现函数复用,由于call有很多个父类实例的副本,性能损耗。
- 原型链丢失
三、组合模式继承
组合式继承是将原型链继承和构造函数继承二者取其长处组合到一起而产生的继承模式。
function Parent(name){ this.name = name; this.hobbies = ["sing", "dance", "rap"];}Parent.prototype.getName = function(){ return this.name}function Child(name){ Parent.call(this, name); this.age = 24}Child.prototype = new Parent('父类')var c1 = new Child('c1');var c2 = new Child('c2');console.log(c1.hasOwnProperty('name')); // trueconsole.log(c1.getName()); // "c1"c1.hobbies.push('coding');console.log(c1.hobbies); // ["sing", "dance", "rap", "coding"]console.log(c2.hobbies); // ["sing", "dance", "rap"]
组合式继承的优点
- 即通过在原型上定义方法实现了函数复用,又保证每个实例都有自己的属性
- 解决了原型链继承和借用构造函数继承造成的影响。
组合式继承的缺点
导致无论在什么情况下都会调用两次超类型构造函数:一次是在创建子类型原型的时候;一次是在子类型构造函数的内部。
四、共享原型继承
该继承方式使得子类和父类共用一个原型。
function Parent(){}Parent.prototype.hobbies = ["sing", "dance", "rap"];function Child(name, age){ this.name = name; this.age = age;}Child.prototype = Parent.prototype;var c1 = new Child("c1", 20);var c2 = new Child("c2", 24);c1.hobbies.push("coding");console.log(c1.hobbies); // ["sing", "dance", "rap", "coding"]console.log(c2.hobbies); // ["sing", "dance", "rap", "coding"]console.log(c1.name); // "c1"console.log(c2.name); // "c2"
共享原型继承的优点
- 简单!!!
共享原型继承的缺点
- 只能继承父类原型属性方法,不能继承构造函数属性方法
- 与原型链继承一样0存在引用类型问题
五、原型式继承
该继承方法多用于基于当前已有对象创建新对象。在另一个函数o1内部创建一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上函数o1对传入的对象进行了一次浅拷贝。
ES5通过新增Object.create()方法将原型是继承进行了规范化。该方法接受两个参数:
- 第一个参数:作为新对象原型的对象
- 第二个参数(可选):给新对象定义额外属性的对象。
// 用法一:创建一个纯洁的对象:对象什么属性都没有Object.create(null);// 用法二:创建一个子对象,它继承自某个父对象var o1 = { name: '父对象', say: function() {}}var o2 = Object.create(o1);
原型式继承的优点
- 不需要单独创建构造函数
原型式继承的缺点
- 属性中包含的引用值始终会在相关对象间共享。
六、寄生式继承
寄生式继承是原型式继承的加强版,它结合原型式继承和工厂模式,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
function createAnother(origin) { var clone = Object.create(origin); // 通过调用函数创建一个新对象 clone.sayHi = function() { // 以某种方式来增强这个对象 alert("Hi"); }; return clone; // 返回这个对象}var o1 = { name: "父对象", hobbies: ["sing", "dance", "rap"]};var o2 = createAnother(o1);o2.sayHi();
上述代码中,createAnother函数接受了一个参数,也就是将要被继承的对象。
o2是基于o1创建的一个新对象,新对象不仅具有o1的所有属性和方法,还有自己的方法。简单来说,寄生式继承在产生了这个继承父类的对象之后,为这个对象添加了一些增强方法。
寄生式继承的优点
写法简单,不需要单独创建函数。
寄生式继承的缺点
通过该方式给对象添加函数会导致函数难以复用。
七、寄生组合式继承
通过借用构造函数来继承属性,通过原型链的混成形式来实现继承的方法。
本质上就是使用寄生式继承来继承超类的原型,然后再将结果指定给子类型的原型。
基本写法:
function inheritPrototype(SubType, SuperType) { var prototype = Object.create(SuperType.prototype); prototype.constructor = SubType; SubType.prototype = prototype;}
兼容写法
function object(o) { function W() { } W.prototype = o; return new W;}function inheritPrototype(SubType, SuperType) { var prototype; if (typeof Object.create === 'function') { prototype = Object.create(SuperType.prototype); } else { prototype = object(SuperType.prototype); } prototype.constructor = SubType; SubType.prototype = prototype;}
完美写法:
function Parent(name) { this.name = name; this.hobbies = ["sing", "dance", "rap"];}Parent.prototype.getHobbies = function(){ return this.hobbies}function Child(name) { Parent.call(this, name); this.age = 24}Child.prototype = Object.create(Parent.prototype);Child.prototype.constructor = Child;// 测试结果var c1 = new Child('c1');var c2 = new Child('c2');console.log(c1 instanceof Child); // trueconsole.log(c1 instanceof Parent); // trueconsole.log(c1.constructor); // Childconsole.log(Child.prototype.__proto__ === Parent.prototype); // trueconsole.log(Parent.prototype.__proto__ === Object.prototype); // truec1.hobbies.push('coding');console.log(c1.getHobbies()); // ["sing", "dance", "rap", "coding"]console.log(c2.getHobbies()); // ["sing", "dance", "rap"]
寄生组合式继承的优点
高效率只调用一次父构造函数,并且因此避免了在子原型上面创建的不必要、多余的属性。与此同时,原型链还保持不变。
寄生组合式继承的缺点
代码复杂!!!
八、ES6——class继承
原理:ES5的继承,实质是先创造的子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
ES6的继承机制完全不同,实质就是先将父类实例对象上的方法和属性,加到this上面(所以必须调用super()方法),然后再用子类的构造函数修改this。
需要注意的是:class关键字只是原型的语法糖,js继承依然是基于原型实现的。
class Parent{ constructor(name) { this.name = name; this.hobbies = ["sing", "dance", "rap"]; } getHobbies() { return this.hobbies; } static getCurrent() { console.log(this); }}class Child extends Parent { constructor(name) { super(name); }}var c1 = new Child('c1');var c2 = new Child('c2');console.log(c1 instanceof Child); // trueconsole.log(c1 instanceof Parent); // true
class继承的优点
语法简单易懂,操作更加方便
class继承的缺点
不是每个浏览器都支持class关键字