JavaScript 继承
原型链继承
JavaScript原型链详细版见这里
多说无益,直接上代码
// 1st
function Super() {
this.colors = ['red', 'blue']
}
var test = new Super()
// 1st
function Super() {
this.colors = ['red', 'blue']
}
var test = new Super()
// 2nd
function Sub() {}
// 1st
function Super() {
this.colors = ['red', 'blue']
}
var test = new Super()
// 2nd
function Sub() {}
// 3rd
Sub.prototype = test
Sub.prototype.constructor = Sub
// 1st
function Super() {
this.colors = ['red', 'blue']
}
var test = new Super()
// 2nd
function Sub() {}
// 3rd
Sub.prototype = test
Sub.prototype.constructor = Sub
// 4th
let instance1 = new Sub()
let instance2 = new Sub()
现在执行如下代码:
console.log(instance1.colors) // (2) ['red', 'blue']
console.log(instance2.colors) // (2) ['red', 'blue']
首先会从实例上寻找对应的属性,找到则返回,否则会一直顺着原型链向上找,即从实例出发,以__proto__为链。知道找到位置,否则返回undefined(原型链尽头是Object.prototype,指向null)。
所以,在寻找colors属性时,顺着原型链找,在test实例,即新的Sub.prototype中找到colors属性并返回。
但是,由于所有实例往上部分的原型链都相同,因此,若原型链上超类属性为引用类型时,修改可能造成错误。
instance1.colors.push('green')
console.log(instance1.colors) // (3) ['red', 'blue', 'green']
console.log(instance2.colors) // (3) ['red', 'blue', 'green']
总结:
优点:理解简单
缺点:
- 包含引用类型的原型属性会被所有实例属性共享,容易造成属性的修改混乱
- 创建子类型的时候不能向超类型传递参数
借用父类构造函数继承
使用JavaScript中的call()方法,它可以用来调用所有者对象作为参数的方法。通过 call(),能够让实例使用属于父类的属性和方法。
function Person() {
this.emotion = ['喜', '怒', '哀', '乐']
}
function Student(type) {
this.type = type
Person.call(this) // 在这里使用父类Person.call(this),使Student类的实例能够使用Person类中的属性与方法
}
let stu1 = new Student('正常人')
console.log(stu1.type + '情绪有:' + stu1.emotion.toString())
// 正常人情绪有:喜,怒,哀,乐
通过这种方式继承其实是将父类的属性与方法深拷贝了一份放在子类的实例中,因此,当拥有多个实例,且父类的属性中有引用数据类型时,不会出现原型链继承中的修改混乱问题。
function Person() {
this.emotion = ['喜', '怒', '哀', '乐']
}
function Student(type) {
this.type = type
Person.call(this)
}
let stu1 = new Student('正常人')
let stu2 = new Student('有愁的人')
stu2.emotion.push('愁')
console.log(stu1.type + '情绪有:' + stu1.emotion.toString())
// 正常人情绪有:喜,怒,哀,乐
console.log(stu2.type + '情绪有:' + stu2.emotion.toString())
// 有愁的人情绪有:喜,怒,哀,乐,愁
总结:
优点:解决了多个子类实例中某个实例修改父类的引用类型变量后导致的其他实例同步修改的问题。
缺点:由于本质上是对父类内容的深拷贝,当实例很多时内存占用会很高,而且,这种借用父类构造函数继承的方法虽然能够传递参数并保证每个实例的属性之间是独立的,但是无法访问到父类的prototype属性,因此无法函数复用,大大限制了继承的功能。
因此,可以将原型链继承与调用父类构造函数继承这两种继承的优点结合起来一起使用。
组合继承(伪经典继承)
因为需要能够函数复用,所以必须使用原型链的方式将子类的原型指向父类的实例,这样多个实例才可以同时访问父类原型上的方法。
然而,又为了防止多个实例对父类实例引用类型变量的修改错误,因此,需要在子类中调用父类.call()方法将父类实例内部的属性与方法深拷贝过来,这样每个子类实例之间的属性与方法就是独立的,不会相互影响。
代码如下:
function Super(id) {
this.id = id
this.colors = ['red', 'blue']
}
// 这个函数就是子类实例将要共享的方法,放在父类的原型上
Super.prototype.sayHi = function() {
console.log('Hi!')
}
function Sub(id) {
// 子类通过call方法将参数传递到父类,并将父类的属性深拷贝到子类
Super.call(this, id)
}
// 原型链继承
Sub.prototype = new Super()
Sub.prototype.constructor = Sub
// 创建两个子类实例
let sub1 = new Sub(1)
let sub2 = new Sub(2)
console.log(sub1.id + ':' + sub1.colors.toString()) // 1:red,blue
console.log(sub2.id + ':' + sub2.colors.toString()) // 2:red,blue
sub1.colors.push('green')
console.log(sub1.id + ':' + sub1.colors.toString()) // 1:red,blue,green
console.log(sub2.id + ':' + sub2.colors.toString()) // 2:red,blue
sub1.sayHi() // Hi!
sub2.sayHi() // Hi!
这样既可以保证参数能传递给父类,并且每个实例的属性都是独立的不会相互影响,而且也能够复用一些方法(通过在父类原型上定义),综合了两种继承的优点。
。。。待续