一、继承
js并没有继承这一个现有的机制,所以我们所说的继承是通过JS本身的机制去实现的。
1、类式继承
1 // 类式继承 2 3 // 父类 4 function SuperClass () { 5 this.superValue = true 6 } 7 8 // 为父类原型添加方法 9 SuperClass.prototype.getSuperValue = function (arguments) { 10 return this.superValue 11 } 12 13 // 声明子类 14 function SubClass () { 15 this.subValue = false 16 } 17 18 // 继承父类 19 SubClass.prototype = new SuperClass() 20 21 // 为子类原型添加方法 22 SubClass.prototype.getSubValue = function (arguments) { 23 return this.subValue 24 }
这里之所以要将SubClass的原型赋值为SuperClass的实例,是因为如果我们将父类的实例赋值给子类的原型,那么子类的原型就可以访问到父类的原型上的属性和方法与从父类的构造函数中赋值的属性和方法,这正是类式继承的原理。
另外,我们可以通过instanceof操作符来检测某个对象是否是某个类的实例,或者说某个对象是否继承了某个类。instanceof通过判断对象的prototype链来确定这个对象是否是某个类的实例,而不关心对象与类的自身结构。
1 var instance = new SubClass() 2 console.log(instance instanceof SuperClass) // true 3 console.log(instance instanceof SubClass) // true 4 console.log(instance instanceof SuperClass) // false
最后一个为什么是false?明明SubClass继承SuperClass,可是为什么SubClass instanceof SuperClass却是false?
之前说过,instanceof是判断前面的对象是否是后面类的对象的实例,并不表示两者之间的继承关系,我们在实现SubClass继承SuperClass时,是通过将SuperClass的实例赋值给SubClass的原型prototype,所以说SubClass.prototypr继承了SuperClass。
1 console.log(instance.prototype instanceof SuperClass) // true
而上面的类式继承有两个严重的缺陷:
1、由于子类通过起原型prototype对父类实例化,继承了父类。所以说父类中的共有属性如果是引用类型,那么就会导致子类中的所有实例共享这同一个引用类型的属性,后果可想而知,A实例一旦修改了某个引用类型的属性,其他实例都会受到影响。
1 function SuperClass () { 2 this.color = [‘red‘, ‘green‘, ‘black‘] 3 } 4 5 function SubClass () { 6 // ... 7 } 8 9 SubClass.prototype = new SuperClass() 10 var instance1 = new SubClass() 11 var instance2 = new SubClass() 12 console.log(instance2.color) // [‘red‘, ‘green‘, ‘black‘] 13 instance2.color.push(‘gray‘) 14 console.log(instance1.color); // [‘red‘, ‘green‘, ‘black‘, ‘gray‘]
从上面代码可以看出,instance2修改了color属性,导致instance1的color属性也被修改了。这正是问题所在
2、由于子类实现的继承是靠其原型prototype被赋值为父类的实例实现的,因此在创建父类的时候,是无法向父类传递参数的,这里所说的不能传递参数,并不是说真的传递参数了就会出错,而是这样做不符合面向对象编程的规则:对象(实例)才是属性的拥有者。如果在子类定义时就将属性赋了值,对象实例就不能再更改自己的属性了。这样就变成了类拥有属性,而不是对象拥有属性了。 举个例子,子类 Children 继承父类 Parents,Parents 构造函数:function Parents(name){ this.name=name; }使用原型链并给父类构造函数传参数:Children.prototype=new Parents("Hello");那么此时,Children 类就拥有了 name=“Hello” 属性,而 Children 类的实例对象 c1、c2、c3 等等只能被迫接受这个 name 属性。Children 是 "Hello" 的拥有者而 c1、 c2、c3不是。所以说,在创建父类是,是无法向父类传递参数的。
2、构造函数继承
正因为类式继承本身存在着一定的缺陷,因此有了构造函数继承
1 // 构造函数式继承 2 // 声明父类 3 function SuperClass (value) { 4 // 引用类型属性 5 this.color = [‘red‘, ‘green‘, ‘black‘] 6 this.value = value 7 } 8 9 // 父类声明原型方法 10 SuperClass.prototype.showColor = function () { 11 console.log(this.color) 12 } 13 14 // 声明子类 15 function SubClass (value) { 16 // 继承父类 17 SuperClass.call(this, value) 18 } 19 20 // 创建第一个子类实例 21 var instance1 = new SubClass(1) 22 // 创建第二个子类实例 23 var instance2 = new SubClass(2) 24 25 instance1.books.push(‘gray‘) 26 console.log(instance1.color) // [‘red‘, ‘green‘, ‘black‘, ‘gray‘] 27 console.log(instance2.color) // [‘red‘, ‘green‘, ‘black‘] 28 instance1.showColor() // TypeError
这里在子类中使用了call,并将子类的this传递了进去,call会导致SuperClass再执行一次,而this确实指向了子类,因此子类自然也就继承了父类的共有属性,但由于这种类型的继承没有涉及到原型prototype,所以父类的原型方法自然不会被子类所继承,而如果想要被子类继承,那么方法或者属性就必须放在构造函数中,但是这样创建出来的每个实例都会单独拥有一份副本而不会共用,这有违代码复用的原则。
3、组合继承
组合继承,顾名思义,综合了以上两种继承方式的思想,所以名为组合
1 // 组合式继承 2 // 声明父类 3 function SuperClass (name) { 4 // 值类型共有属性 5 this.name = name 6 // 引用类型共有属性 7 this.color = [‘red‘, ‘green‘, ‘black‘] 8 } 9 10 // 父类原型 11 SuperClass.prototype.getName = function () { 12 console.log(this.name) 13 } 14 15 // 声明子类 16 function SubClass (name, time) { 17 // 构造函数式继承父类name属性 18 SuperClass.call(this, name) 19 this.time = time 20 } 21 22 // 类式继承 子类原型继承父类 23 SubClass.prototype = new SuperClass () 24 // 子类原型方法 25 SubClass.prototype.getTime = function () { 26 console.log(this.time) 27 }
组合继承,子类的实例中更改父类继承下来的引用类型属性如color,不会影响到其他实例,并且子类实例化过程中又能将参数传递到父类的构造函数中。
至此,组合继承应该算是比较完美的解决方案了,不过,仔细研究代码还是可以发现,我们在使用构造函数继承时,执行了一次父类的构造函数,而在实现子类原型的类式继承时,再次调用了一次父类的构造函数。因此,父类的构造函数执行了两边,所以,也不算完美。
4、原型式继承
2006年道格拉斯-克罗克福发表的《JavaScript中原型式继承》中提到了他的观点:借助原型prototype可以根据已有的对象创建一个新的对象,同时不必创建新的自定义对象。这段话可能不好理解,我们直接看代码:
1 // 原型式继承 2 function inheritobject(o) { 3 // 声明一个过渡函数对象 4 function F () {} 5 // 过渡对象的原型继承父对象 6 F.prototype = o 7 // 返回过渡对象的一个实例,该实例的原型继承了父对象 8 return new F() 9 }
这个方法其实与类式继承相似,它是对类式继承的一个封装。所以类式继承中出现的问题,这里也会出现,但是由于过渡类F的构造函数中并无其他的内容,所以使用起来较为方便。在原型式继承的基础之上,该大牛推出了寄生式继承。
5、寄生式继承
1 // 寄生式继承 2 // 声明基对象 3 var book = { 4 name: ‘book‘, 5 color: [‘red‘, ‘green‘] 6 } 7 function createBook(obj) { 8 // 通过原型继承方式创建新对象 9 var o = new inheritObject(obj) 10 // 扩展新对象 11 o.getName = function () { 12 console.log(this.name) 13 } 14 // 返回拓展后的新对象 15 return o 16 }
其实寄生式继承是对原型继承的二次封装,并且在二次封装的过程中对继承的对象进行了拓展,这样新创建的对象不仅仅有父类中的属性和方法,也添加了新的属性和方法.其缺点也与原型式继承相同。
6、寄生组合式继承
1 function inheritPrototype (SubClass, SuperClass) { 2 // 复制一份父类的原型副本保存在变量中 3 var temp = inheritObject(SuperClass.prototype) 4 // 修正因为重写子类原型导致自留constructor指向改变 5 temp.constructor = SubClass 6 // 设置子类的原型 7 SubClass.prototype = temp 8 } 9 10 // 定义父类 11 function SuperClass (name) { 12 this.name = name 13 this.color = [‘red‘, ‘green‘, ‘black‘] 14 } 15 //定义父类原型方法 16 SuperClass.prototype.getName = function () { 17 console.log(this.name) 18 } 19 20 // 定义子类 21 function SubClass (name, time) { 22 // 构造函数式继承 23 SuperClass.call(this, name) 24 // 子类属性 25 this.time = time 26 } 27 28 // 寄生式继承父类原型 29 inheritPrototype(SubClass, SuperClass) 30 // 子类新增原型方法 31 SubClass.prototype.getTime = function () { 32 console.log(this.time) 33 } 34 35 // 测试 36 var instance1 = new SubClass(‘a‘, 1) 37 var instance2 = new SubClass(‘b‘, 2)