2018.5.27html
今天本人又在查关于继承的问题,从新温习了一遍书,发现以前举的例子实际上不太清晰,故作调整。app
个人上一篇文章介绍了,原型链继承模式。原型链继承虽然很强大,可是单纯的原型链模式并不能很好地实现继承。函数
先看一个例子:
this
//父类:人 function Person () { this.head = '脑壳瓜子'; } //子类:学生,继承了“人”这个类 function Student(studentID) { this.studentID = studentID; } Student.prototype = new Person(); var stu1 = new Student(1001); console.log(stu1.head); //脑壳瓜子 stu1.head = '聪明的脑壳瓜子'; console.log(stu1.head); //聪明的脑壳瓜子 var stu2 = new Student(1002); console.log(stu2.head); //脑壳瓜子
以上例子,咱们经过重写 Student.prototype 的值为 Person 类的一个实例,实现了 Student 类对 Person 类的继承。因此 ,stu1 能访问到父类 Person 上定义的 head 属性,打印值为“脑壳瓜子”。咱们知道,全部的 Student 实例都共享着原型对象上的属性。那么,若是我在 stu1 上改变了 head 属性值,是否是会影响原型对象上的 head 值呢?看我上面的代码就知道,确定是不会。stu1 的 head 值确实是改变了,可是我从新实例化的对象 stu2 的 head 值仍旧不变。spa
这是由于,当实例中存在和原型对象上同名的属性时,会自动屏蔽原型对象上的同名属性。stu1.head = "聪明的脑壳瓜子" 实际上只是给 stu1 添加了一个本地属性 head 并设置了相关值。因此当咱们打印 stu1.head 时,访问的是该实例的本地属性,而不是其原型对象上的 head 属性(它因和本地属性名同名已经被屏蔽了)。prototype
刚才咱们讨论的这个 head 属性是一个基本类型的值,可若是它是一个引用类型呢?这其中又会有一堆小九九。设计
其实原型对象上任何类型的值,都不会被实例所重写/覆盖。在实例上设置与原型对象上同名属性的值,只会在实例上建立一个同名的本地属性。code
可是,原型对象上引用类型的值能够经过实例进行修改,导致全部实例共享着的该引用类型的值也会随之改变。htm
再看下面这个例子:对象
//父类:人 function Person () { this.head = '脑壳瓜子'; this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐 } //子类:学生,继承了“人”这个类 function Student(studentID) { this.studentID = studentID; } Student.prototype = new Person(); var stu1 = new Student(1001); console.log(stu1.emotion); //['喜', '怒', '哀', '乐'] stu1.emotion.push('愁'); console.log(stu1.emotion); //["喜", "怒", "哀", "乐", "愁"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜", "怒", "哀", "乐", "愁"]
咱们在刚才的 Person 类中又添加了一个 emotion 情绪属性,人都有喜怒哀乐嘛。尤为须要注意的是,这是一个引用类型的值。这时,stu1 认为他还很“愁”,因此就经过 stu1.emotion.push ( ) 方法在原来的基础上增长了一项情绪,嗯,打印出来“喜怒哀乐愁”,没毛病。但是 stu2 是个乐天派,他咋也跟着一块儿愁了呢?!确定不对嘛~
这就是单纯的原型链继承的缺点,若是一个实例不当心修改了原型对象上引用类型的值,会致使其它实例也跟着受影响。
所以,咱们得出结论,原型上任何类型的属性值都不会经过实例被重写,可是引用类型的属性值会受到实例的影响而修改。
在解决原型对象中包含引用类型值所带来问题的过程当中,开发人员开始使用一种叫作借用构造函数的技术。实现原理是,在子类的构造函数中,经过 apply ( ) 或 call ( )的形式,调用父类构造函数,以实现继承。
//父类:人 function Person () { this.head = '脑壳瓜子'; this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐 } //子类:学生,继承了“人”这个类 function Student(studentID) { this.studentID = studentID; Person.call(this); } //Student.prototype = new Person(); var stu1 = new Student(1001); console.log(stu1.emotion); //['喜', '怒', '哀', '乐'] stu1.emotion.push('愁'); console.log(stu1.emotion); //["喜", "怒", "哀", "乐", "愁"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜", "怒", "哀", "乐"]
细心的同窗可能已经发现了,该例子与上面的例子很是类似,只是去掉了以前经过 prototype 继承的方法,而采用了 Person.call (this) 的形式实现继承。别忘了,函数只不过是一段能够在特定做用域执行代码的特殊对象,咱们能够经过 call 方法指定函数的做用域。
(题外话:也许有的同窗对 this 的指向还不彻底清楚,我是这么理解的:谁调用它,它就指向谁。)
在 stu1 = new Student ( ) 构造函数时,是 stu1 调用 Student 方法,因此其内部 this 的值指向的是 stu1, 因此 Person.call ( this ) 就至关于Person.call ( stu1 ),就至关于 stu1.Person( )。最后,stu1 去调用 Person 方法时,Person 内部的 this 指向就指向了 stu1。那么Person 内部this 上的全部属性和方法,都被拷贝到了 stu1 上。stu2 也是同理,因此实际上是,每一个实例都具备本身的 emotion 属性副本。他们互不影响。说到这里,你们应该清楚一点点了吧。
总之,在子类函数中,经过call ( ) 方法调用父类函数后,子类实例 stu1, 能够访问到 Student 构造函数和 Person 构造函数里的全部属性和方法。这样就实现了子类向父类的继承,并且还解决了原型对象上对引用类型值的误修改操做。
这种形式的继承,每一个子类实例都会拷贝一份父类构造函数中的方法,做为实例本身的方法,好比 eat()。这样作,有几个缺点:
1. 每一个实例都拷贝一份,占用内存大,尤为是方法过多的时候。(函数复用又无从谈起了,原本咱们用 prototype 就是解决复用问题的)
2. 方法都做为了实例本身的方法,当需求改变,要改动其中的一个方法时,以前全部的实例,他们的该方法都不能及时做出更新。只有后面的实例才能访问到新方法。
//父类:人 function Person () { this.head = '脑壳瓜子'; this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐 this.eat = function () { console.log('吃吃喝喝'); } this.sleep = function () { console.log('睡觉'); } this.run = function () { console.log('快跑'); } }
因此,不管是单独使用原型链继承仍是借用构造函数继承都有本身很大的缺点,最好的办法是,将二者结合一块儿使用,发挥各自的优点。我将在下一篇文章做出解释。js 继承之组合继承
若是你以为文章解决了你的疑惑的话,还请赏我一个推荐哦~ :)
做者不才,文中如有错误,望请指正,避免误人子弟。
文章内容全都参考于《JAVASCRIPT 高级程序设计》)