您的位置:首页 > Web前端 > JavaScript

【六】深入理解javascript继承

2017-03-27 11:52 232 查看
继前一博客 深入理解javascript创建对象的七种方式后,继续总结javascrpit实现继承的六种方式。其实以前也写过一篇博客,是当时刚学习js继承的时候总结的,但是一直没有使用过面向对象编程,对总结的知识点理解不够深入,所以再次写下关于js实现继承的这篇博客,知识点总结自《javascript高级程序设计》。

然而关于js继承这一方面,《你不知道的javascript 上卷》貌似有不同的观点,大意是不太赞同继承这种说法,具体内容后面阐述。

首先递进式的总结一下javascript实现继承的六种方式。

原型链继承

原型链继承的基本思想是让子类型的原型对象等于另一个类型的实例,然后创建的子类型的对象,其指向子类型的原型,子类型的原型又指向父类型的原型,由此实现继承。

function Super() {
this.superName = "super";
}
Super.prototype.getSuperName = function() {
return this.superName;
}

function Sub() {
this.subName = "sub";
}
// 继承了Super
Sub.prototype = new Super();
Sub.prototype.getSubName = function() {
return this.subName;
}

var obj1 = new Sub();
console.log(obj1.getSuperName());
console.log(obj1 instanceof Sub);
console.log(obj1 instanceof Super);
console.log(Super.prototype.isPrototypeOf(obj1));


注意点:

所有函数的默认原型都是Object的实例,所以默认原型对象包含一个指针[[Prototype]]指向Object.prototype。每个实例内部都会存在一个指针[[Prototype]],指向构造函数的原型对象。

原型链继承存在的问题:1.当父类型的属性包含引用类型值时,引用类型值的属性会被所有实例共享。2.没有办法在不影响所有子类型实例的情况下给父类型的构造函数传递参数。

前面在说明创建对象的六种方式里面介绍过,为什么要从原型模式创建对象升级为组合模式(组合使用原型模式和构造函数模式):包含在原型对象中的属性会被所有实例共享,当原型对象存在引用类型的属性如数组时,一个实例对该属性值的改变会影响其他所有实例的该属性值。

通过原型链实现继承时,子类型的原型对象成为父类型的实例,当父类型存在引用类型值时,相当于在子类型的原型对象上添加了一个引用类型值的属性,导致所有子类型的实例会共享该属性。

function Super() {
this.friends = ["a","b"];
}

function Sub() { }
// 继承了Super
Sub.prototype = new Super();

var obj1 = new Sub();
var obj2 = new Sub();

obj1.friends.push("c");
console.log(obj1.friends);//[ 'a', 'b', 'c' ]
console.log(obj2.friends);//[ 'a', 'b', 'c' ]


为了解决上述存在的两个问题,引入了借用构造函数式继承。

借用构造函数式继承

基本思想是在子类型的构造函数内部调用父类型的构造函数,通过apply()和call()来改变父类型构造函数的作用域,使其在新创建的对象上执行。

function Super() {
this.friends = ["a","b"];
}

function Sub() {
// 继承了Super
Super.call(this);
}

var obj1 = new Sub();
var obj2 = new Sub();

obj1.friends.push("c");
console.log(obj1.friends);[ 'a', 'b', 'c' ]
console.log(obj2.friends);[ 'a', 'b' ]


由于没有使用原型对象,直接在子类型的构造函数里面执行父类型的构造函数,因而原型链继承中的存在的两个问题都可以完美解决了。

但是借用构造函数式继承同样存在一定的问题:

1.方法都在构造函数中定义的,无法实现函数复用。

2.因为直接使用了构造函数,没用new Super(),因而在父类型原型中定义的方法对子类型是不可见的。

为解决以上的两个问题,引入了组合继承。

组合继承

基本思想是将原型链继承和构造函数式继承结合使用。通过原型链实现对原型属性和方法的继承,通过构造函数实现对实例属性的继承。

function Super(name) {
this.name = name;
this.friends = ["a","b"];
}
Super.prototype.sayName = function() {
console.log(this.name)
}

function Sub(name, age) {
// 继承了属性
Super.call(this, name);              //1
this.age = age;
}
// 继承方法
Sub.prototype = new Super();             //2
Super.prototype.constructor = Sub;       //3
Sub.prototype.sayAge = function() {
console.log(this.age);
}

var obj1 = new Sub("haha", 20);
obj1.friends.push("c");
console.log(obj1.friends);[ 'a', 'b', 'c' ]
obj1.sayName();
obj1.sayAge();

var obj2 = new Sub("xixi", 19);
console.log(obj2.friends);[ 'a', 'b' ]
obj2.sayName();
obj2.sayAge();


组合继承是最常用的一种继承方式。

原型式继承

基本思想是基于已有的对象创建新的对象,不必因此而创建新的类型,相当于执行一次浅复制。

function object(o) {
function F() {};
F.prototype = o;
return new F();
}

var obj1 = {
name: "obj1",
friends: ["a", "b"]
}

var obj2 = object(obj1);
obj2.name = "obj2";
obj2.friends.push("c");
console.log(obj2.name);
console.log(obj2.friends);//[ 'a', 'b', 'c' ]

var obj3 = object(obj1);
obj3.name = "obj3";
obj3.friends.push("d");
console.log(obj3.name);
console.log(obj3.friends);//[ 'a', 'b', 'c', 'd' ]

console.log(obj1.friends);//[ 'a', 'b', 'c', 'd' ]


ecmascript5新增了Object.create()方法规范化了原型式继承,该方法接受两个参数:作为新对象原型的对象和为新对象定义额外属性的对象。在只传入第一个参数时和前述的object()函数功能一致。

var obj1 = {
name: "obj1",
friends: ["a", "b"]
}
var obj2 = Object.create(obj1, {
name: {
value: "obj2"
}
})

obj2.friends.push("c");
console.log(obj2.name);
console.log(obj2.friends);//[ 'a', 'b', 'c' ]


原型式继承在某些条件下很适用,但是包含引用类型值的属性会共享值。

寄生式继承

基本思想与寄生构造函数和工厂模式类似,创建一个仅用于封装继承过程的函数,在该函数内部不断增强对象,最后返回对象。

function object(o) {
function F() {};
F.prototype = o;
return new F();
}

var obj1 = {
name: "obj1",
friends: ["a", "b"]
}

function createAnother(o) {
var clone = object(o);
clone.sayHi = function() {
console.log("hi")
};
return clone;
}

var obj2 = createAnother(obj1);
obj2.sayHi();


此种模式在主要考虑“对象”而不是自定义类型和构造函数的情况下比较适用。

寄生组合式继承

组合继承是最常用的继承模式,但是也存在一定的不足。组合继承无论在什么情况下都会调用两次父类型的构造函数。

第一次是在Sub.prototype = new Super()时,第二次是在子类型构造函数内部Super.call(this)时。这会导致子类实例上有两组属性,一组在实例中,一组在Sub.prototype中。

寄生组合式继承就是为了解决这个问题而引入的。

基本思想是不再为了指定子类型的原型而调用父类型的构造函数,因为所需要的只是父类型原型的一个副本,所以直接给子类型的原型对象指定一个父类型原型对象的副本,避免调用父类型的构造函数。本质上是使用寄生式继承来继承父类型的原型。

function object(o) {
function F() {};
F.prototype = o;
return new F();
}

function inheritPrototype(sub, super) {
var prototype = object(super.prototype);
prototype.constructor = sub;
sub.prototype = prototype;
}

function Super(name) {
this.name = name;
this.friends = ["a", "b"];
}
Super.prototype.sayName = function() {
console.log(this.name);
}

function Sub(name, age) {
Super.call(this, name);
this.age = age;
}
inheritPrototype(Sub,Super);//替换了Sub.prototype = new Super()
Sub.prototype.sayAge = function() {
console.log(this.age);
}


寄生组合式继承只调用了一次父类型的构造函数,避免在子类型的原型对象上创建不必要的属性。

寄生组合式继承是最理想的一种继承方式。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息