上一篇介绍面向对象JS的实现原理以及封装特性的实现
JavaScript设计模式(2)-设计模式的基础-面向对象JS-封装
这一篇主要介绍JS模拟面向对象继承特性的实现
JS并没有继承机制,要想实现继承需要对其进行模拟
也因此也少了很多限制,使JS具有了一定的灵活性
对于继承的方式也有多种实现方法
最常见的继承方式就是类式继承
// 声明父类
function SuperClass(){
this.superValue = true;
}
// 为父类原型添加公有方法,可供所有实例调用
SuperClass.prototype.getSuperValue = function(){
return this.superValue;
}
// 声明子类
function SubClass(){
this.subValue = false;
}
// 子类继承父类,子类prototype原型为父类实例
SubClass.prototype = new SuperClass();
// 为子类原型添加公有方法,可供所有实例调用
SubClass.prototype.getSubValue = function(){
return this.subValue;
}
类式继承:
类式继承:将父类实例赋值给子类原型
类式继承的原理:
通过类的原型对象可以为类添加公有方法
创建一个父类实例,会复制一套父类构造函数中的属性与方法,
并将实例的__proto__指向父类原型对象
因此新创建的父类实例拥有了父类原型对象上的属性和方法,
并且能访问到父类原型上的属性和方法
将这个父类实例赋值给子类原型,
子类原型就能访问到父类原型上的属性,方法
以及从父类的构造函数中复制的属性和方法
这就是类式继承的原理
var instance = new SubClass();
instance.getSuperValue(); // true
instance.getSubValue(); // false
instanceof作用:
检测对象是否为某个类的实例(是否继承了某个类)
instanceof原理:
通过判断对象的prototype链来确认对象是否为某个类的实例
console.log(instance instanceof SuperClass);// true prototype链存在SuperClass
console.log(instance instanceof SubClass); // true prototype链存在SubClass
console.log(SubClass instanceof SuperClass); // false
console.log(SubClass.prototype instanceof SuperClass); // true
console.log(instance.prototype instanceof Object);//true所有对象都是Object实例
子类通过prototype原型对父类实例化,实现子类继承父类
所有子类实例的prototype原型都指向同一个父类对象,
此时,如果父类中的公有属性为引用类型,就会被所有子类实例所共用
所以,任何一个子类实例修改从父类构造函数中继承来的公有属性就会影响到其他子类实例
// 创建父类
function SuperClass(){
// 父类公有属性-数组为引用类型
this.books = ['js', 'html', 'css']
}
// 创建子类
function SubClass(){}
// 子类继承父类
SubClass.prototype = new SuperClass();
//创建2个子类实例
var instance1 = new SubClass();
var instance2 = new SubClass();
console.log(instance1.books);// ['js', 'html', 'css']
instance2.books.push('js设计模式');
console.log(instance1.books);// ['js', 'html', 'css', 'js设计模式']
由于2个子类原型指向同一个父类实例,共享了数组引用类型的属性,导致相互影响
子类依靠prototype原型对父类实例化实现继承
因此创建父类时无法向父类传递参数,也无法对构造函数中的属性进行初始化
针对于类式继承的缺点,灵活的js还有其他的继承方法,如构造函数继承
// 声明父类
function SuperClass(id){
// 公有属性-数组为引用类型
this.books = ['js', 'html', 'css'];
// 公有属性-值类型
this.id = id;
}
// 父类原型方法
SuperClass.prototype.showBooks = function(){
console.log(this.books);
}
// 声明子类
function SubClass(id){
SuperClass.call(this, id);
}
// 创建两个子类实例
var instance1 = new SubClass(1);
var instance2 = new SubClass(2);
// 修改instance1的books数组
instance1.books.push("js设计模式");
// 打印修改后的结果
console.log(instance1.id); // 1
console.log(instance1.books); // ['js', 'html', 'css', 'js设计模式']
console.log(instance2.id); // 2
console.log(instance2.books); // ['js', 'html', 'css']
instance1.showBooks(); // TypeError 父类原型方法未被子类继承
从运行结果可以看出,对实例1的引用类型属性修改并没有对实例2的属性造成影响
每个实例都拥有属于自己的一份属性,而非公用同一引用
对子类实例调用父类原型方法报错,子类未继承父类原型方法
这句代码是关键,call方法可以更改函数的作用环境,this指代当前环境
在子类(SubClass)中对父类(SuperClass)调用call方法,让父类在子类环境中执行
父类执行时会给this绑定属性,而this是子类环境,所以子类就继承了父类的公有属性
也正是因此,这种继承方式并没有涉及到prototype原型,
父类的原型方法不会被子类继承,想要被子类继承就必须要放到构造函数中才可以,
但这样创建创建出来的实例都会单独拥有一份属性而不能共用,违背了代码复用原则
优点:
相比于类式继承,构造函数继承克服了实例共享引用类型属性和不能初始化属性的缺点
缺点:
但由于没有使用prototype原型,导致子类不能继承父类原型
类式继承:通过子类原型对父类实例化(共享引用,无法实例化)
构造函数继承:在子类作用环境中执行父类构造函数(解决了共享引用,无法实例化,但不能继承父类原型)
所以,只要子类能够继承父类原型就没有问题了
// 声明父类
function SuperClass(name){
// 公有属性-数组为引用类型
this.books = ['js', 'html', 'css'];
// 公有属性-值类型
this.name = name;
}
// 父类原型公有方法
SuperClass.prototype.getName = function(){
console.log(this.name);
}
// 声明子类
function SubClass(name, createTime){
// 构造函数式继承
SuperClass.call(this, name);
// 子类新增公有属性
this.createTime = createTime;
}
//类式继承-子类原型继承父类实例
SubClass.prototype = new SuperClass();
// 子类原型新增公有方法
SubClass.prototype.getCreateTime(){
console.log(this.time);
}
// 实例化两个子类对象并对其进行操作
var instance1 = new SubClass("js设计模式", 2018);
instance1.books.push("js设计模式");
console.log(this.books); // ['js', 'html', 'css', 'js设计模式']
instance1.getName(); // js设计模式
instance1.getCreateTime(); // 2018
var instance2 = new SubClass("javascrpit", 2017);
console.log(instance2.books); // ['js', 'html', 'css']
instance2.getName(); // javascrpit
instance2.getCreateTime(); // 2017
可以看到,对子类实例1的数组(引用类型属性)进行了修改,并没有影响子类实例2
子类实例1和实例2均可以调用子类原型方法getCreateTime和父类原型方法getName获得自身实例的值
所以这种组合方式,既克服了公有属性指向同一引用的问题,也解决了不能初始化属性的问题
同时子类还可以继承父类原型方法
在子类构造函数中执行父类构造函数
在子类原型上实例化父类,
这就是组合继承,
包含了类式继承和构造函数继承的优点,克服了他们的不足
尽管组合继承的方式客服了之前类式继承和构造函数继承的不足,但仍不完美
因为在使用构造函数继承时执行了一遍父类构造函数:
SuperClass.call(this, name);
而为了实现子类继承父类原型所使用的类式继承中,又调用了一遍父类的构造函数:
SubClass.prototype = new SuperClass();
因此父类的构造函数被调用了两次,造成了资源的浪费
借助原型prototype可以根据已有的对象创建一个新的对象,同时不必创建新自定义对象类型
//原型式继承
function inheritObject(o) {
// 声明一个过渡函数对象
function F() {}
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡独享的一个实例,该实例的原型继承了父对象
return new F();
}
这是对类式继承的一种封装,其中的过渡对象(过渡类)F()相当于类式继承中的子类,
在原型式继承中作为过渡对象,目的是为了创建要返回的新的实例化对象
这种方式由于F过渡类的构造函数中无内容,所以开销比较小,使用起来也比较方便
如果有必要可以将D过渡类缓存起来,不必每次创建一个新的过渡类F
var book = {
name:"js book",
books : ["css book", "html book"]
}
var newBook = inheritObject(book);
newBook.name = "ajax book";
newBook.books.push("xml book")
var otherBook = inheritObject(book);
otherBook.name = "flash book";
otherBook.books.push("as book")
console.log(newBook.name) // ajax book
console.log(newBook.books); // ["css book", "html book", "xml book", "as book"]
console.log(otherBook.name) // flash book
console.log(otherBook.books); // ["css book", "html book", "xml book", "as book"]
console.log(book.name) // js book
console.log(newBook.books); // ["css book", "html book", "xml book", "as book"]
由于原型式继承是类式继承的一种封装形式,
所以和类式继承一样,多个实例会共享原型引用
然而,寄生式继承在原型式继承对类式继承的基础上对其进行了二次封装,并对其进行了拓展
寄生式继承就是对原型继承的第二次封装,并且对继承的对象进行了拓展
//原型式继承
function inheritObject(o) {
// 声明一个过渡函数对象
function F() {}
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡独享的一个实例,该实例的原型继承了父对象
return new F();
}
var book = {
name:"js book",
books:["css book","html book"]
}
function createBook(obj){
// 通过原型继承方式创建新对象
var o = new inheritObject(obj);
// 拓展新对象
o.getName = function(){
console.log(name)
}
// 返回拓展后的新对象
return o;
}
新创建的对象不仅有父类中的属性和方法,还为其添加新的属性和方法
就和名字一样:
像寄生虫一样寄托于某个对象内部生长
而这个对象,指的就是通过原型式继承得到的对象
这种思想的作用也是为了寄生组合式继承的实现
组合式继承:将类式继承和构造函数式继承组合使用
问题:子类不是父类的实例,而子类的原型是父类的实例
为了克服这个问题,有了寄生组合式继承:
寄生组合式继承结合了寄生式继承和构造函数式继承
寄生式继承,依托于原型式继承,原型式继承又依托于类式继承
(原型式继承是对类式继承的一次封装,而寄生式继承是对原型式继承的又一次封装)
子类不是父类实例的问题是由于类式继承引起的
解决方法:只需要继承父类的原型即可,不需要调用父类的构造方法
// 父类
function SuperClass(name) {
this.name = name;
this.colors = ["red", "blue", "yellow"];
}
// 父类原型
SuperClass.prototype.getName = function () {
console.log(this.name)
return this.name;
}
// 子类
function SubClass(name, time) {
// 构造函数继承
SuperClass.call(this, name);
// 子类新增属性
this.time = time;
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass);
// 添加子类原型方法
SubClass.prototype.getTime = function () {
console.log(this.time)
return this.time;
}
// 测试:创建两个子类实例
var instance1 = new SubClass("java script", '2018');
var instance2 = new SubClass("css", '2017');
instance1.colors.push("black");
console.log(instance1.colors);// ["red", "blue", "yellow", "black"]
console.log(instance2.colors);// ["red", "blue", "yellow"]
console.log(instance2.getName());// css
console.log(instance2.getTime());// 2017
相比于组合式继承, 子类原型被赋予了父类原型的引用
解决了子类原型被赋值为父类实例导致的父类构造函数多执行一次的问题
在子类原型引用父类原型后,重新赋值construct指向子类,修正了construct的指向错误
我们知道,有一些面向对象语言是支持多继承的
javaScript的继承是依赖prototype原型链实现的,
由于原型链只有一条,所以理论上不能支持继承多个父类,
但js有一定的灵活性,可以通过继承多个对象的属性来模拟多继承的实现
常用的extend方法:
var extend = function (target, source) {
// 遍历源对象属性
for (var property in source){
// 将源对象属性复制到目标对象中
target[property] = source[property];
}
// 返回目标对象
return target;
}
extend方法的实现,实际是对对象属性的一个(浅)复制过程
测试extend方法:
// book对象
var book = {
name:"js设计模式",
books:['css', 'js', 'html']
}
var anotherBook = {
color:'blue'
}
// anotherBook对象继承book对象的属性
extend(anotherBook, book);
//console.log(anotherBook.name); // js设计模式
//console.log(anotherBook.books);// ["css", "js", "html"]
// 修改引用类型属性
anotherBook.books.push('vue');
anotherBook.name = 'Vue.js';
console.log(anotherBook.name); // Vue.js
console.log(anotherBook.books); // ["css", "js", "html", "vue"]
console.log(book.name); // js设计模式
console.log(book.books); // ["css", "js", "html", "vue"]
浅复制:
extend方法是一个浅复制过程,只能复制值类型属性,对于引用类型属性只会使用其引用
深复制:
jQuery等一些框架实现了深复制,可以将源对象中的引用类型属性再执行一遍extend(递归)
传入多个对象,第一个对象为目标对象,其他对象为需要被继承的对象
遍历需要被继承的对象,将他们的属性全部赋值给目标对象,实现多继承效果
// 多继承
var mix = function () {
var i = 1, // 从第二个参数起未被继承的对象
len = arguments.length, // 获取参数长度
target = arguments[0], // 第一个对象为目标
arg; // 保存循环中当前正在处理的对象
// 遍历被继承的对象,从1(第二个对象)开始,第一个对象时目标对象
for(; i < len; i++){
arg = arguments[i];
//遍历窗前对象中的属性,赋值给目标对象
for (var property in arg){
// 将源对象属性复制到目标对象中
target[property] = arg[property];
}
}
return target;
}
也可以将这个逻辑绑定到原生对象Object的prototype原型上,使所有对象都具备此特性
同时这样做还有一个好处,就是不需要传入目标对象,在内部使用this即可
Object.prototype.mix = function () {
var len = arguments.length,
arg;
// 遍历被继承的对象,从1(第二个对象)开始,第一个对象时目标对象
for(var i = 0; i < len; i++){
arg = arguments[i];
//遍历窗前对象中的属性,赋值给目标对象
for (var property in arg){
// 将源对象属性复制到目标对象中
this[property] = arg[property];
}
}
return this;
}
// 测试
var obj1 = {
name:"js设计模式"
}
var obj2 = {
desc:"javaScript"
}
anotherBook.mix(obj1, obj2);
console.log(anotherBook);
//Object :
//{color: "blue"
// desc: "javaScript"
// mix: ƒ ()
// name: "js设计模式"
// __proto__: Object
//}