JavaScript中的继承实现(1)
前言:学习过面向对象语言(java、c++)的童鞋都了解面向对象的概念,也肯定知道面向对象语言的特征:封装、继承和多态,但JavaScript并非面向对象,而是基于对象,这些概念我们无法直接应用到JavaScript的对象机制中,这节我们主要学习JavaScript开发者是如何曲线救国实现面向对象语言的继承特性。
1.类的概述?
类:类是面向对象语言的基础。类好比模型,比如说动物是一个类别很空泛不具体,拥有很多的特征,但是我们具体不知道它是会飞、爬、游。
对象:那么对象好比是类的一个具体体现,比如狗这个动物就是真实存在的,所有的特征是固定的,会跑、会汪汪叫等。
面向对象:面向对象更多强调的是数据和操作数据的行为是互相关联的,具体的实现就是将数据和关联的行为封装为一个类,基于类完成面向对象语言的开发。
举例:表示单词或短语的字符通常称为字符串。但我们往往关心的不是数据本身,而是和数据关联的操作。所以就将数据(字符)和行为(计算长度、增删数据等)封装成了统一的String类,所有的字符串就是String类的一个实例。
2.何为继承?继承有什么好处?
在现实生活中我们常会使用继承一词,比如:小鸟继承了妈妈的特征。可以继承妈妈的属性(羽毛、四肢等),也可以继承妈妈的行为(鸣叫、飞等),程序中的继承与其类似。
继承:子类在不需要重写父类属性和方法的前提下继承了父类的属性和方法,并且可以直接调用它们。
好处:提高了代码的复用性。
3.JavaScript中继承的实现
在其他语言(java)中,子类继承父类得到只是父类的一个副本,类的继承实质上就是类的复制。但是在JavaScript中只有对象,对象之间不存在类之间复制的功能。那么JavaScript开发者又是如何实现面向对象语言的继承行为的。
1>构造函数继承
在子类中通过call/apply方法完成子类对父类的继承。
// 定义名为Person的构造函数,构造函数只绑定属性name function Person(name) { this.name = name this.getName = function() { return this.name; } } // 定义构造函数Bar(),定义属性name和label function Student(name, hobby){ // 通过call改变this的指向,继承Person中的属性name和方法getName() Person.call(this, name) this.hobby = hobby; this.getHobby = function () { return this.hobby } } var a = new Student("小明", "basketBall"); console.log(a.getName()); console.log(a.getHobby());
上例中子类Student通过Person.call(this.name)继承了父类Person的name属性和getName()方法,我们在Student的实例中可以直接调用name属性和getName()方法。
分析:<1>Person所有实例中的getName()方法行为都是一样的,每实例化一个对象都会定义一个行为相同的getName()方法,但我们希望所有实例都共享同一个getName()方法,所有getName()最好的方式是定义在原型上。
缺点:
<1>.每个实例都有相同行为的getName()方法造成内存的浪费。
<2>.做不到代码的复用,最佳做法getName()方法所有实例共用同一个。
<3>.造成父类多余属性的继承
2>原型prototype模式继承
函数都有prototype属性,在设计构造函数时,我们将不需要共享的属性和方法,定义在构造函数里面,需要共享的属性和方法,都放在prototype中;
// 将不共享的name属性定义在Person函数中 function Person(name) { this.name = name } // 将需要共享的getName方法定义在原型上 Person.prototype.getName = function(){ return this.name } // 定义构造函数Student(),定义属性name和label function Student(name, hobby){ // 通过call改变this的指向,继承Person中的属性name Person.call(this, name) this.hobby = hobby; } // Student继承了Person中的方法 Student.prototype = new Person() // 修正Student的构造函数的指向 Student.prototype.constructor = Student // Student定义getHobby的方法 Student.prototype.getHobby = function() { return this.hobby } var stu = new Student("小明", "basketBall"); console.log(stu.getName()); console.log(stu.getHobby());
分析:
1>.代码中Student.prototype = new Person(),Person实例赋值给Student.prototype,Student.prototype原先的值被删除掉。
2>.Student.prototype.constructor = Student做了什么呢?每个Prototype对象都有一个Constructor属性,如果没有Student.prototype = new Person(),
Student.prototype的constructor是指向Student的,执行这行后constructor指向了Person,更重要的是每个实例有一个constructor属性,默认调用prototype中的constructor属性。
调用Student.prototype = new Person()后,stu.constructor也指向了Person。
console.log(Student.prototype.constructor === Person) // true console.log(stu.constructor === Person) // true console.log(stu.constructor === Student.prototype.constructor) //true
这就导致了继承链的混乱,因为stu是Student的实例,而不是Person的实例,所以我们需要手动修正Student.prototype.constructor = Student。
3>.上例中对于Student.prototype = new Person(),我们可以使用Student.prototype = Person.prototype替换,与第一种相比不需要执行Person函数(提高性能)也不需要创建实例(节省内存),同时缩短了原型链的调用。
缺点:导致Student.prototype和Person.prototype指向同一个对象引用,修改其中任意一个另一个就会被影响。比如:Student.prototype.constructor = Student会导致Person.prototype.constructor也是Student。
console.log(Person.prototype.constructor === Student) // true
3>prototype模式改进
由于Student.prototype = new Person()和Student.prototype = Person.prototype两种方式都存在缺点,这里我们使用空对象作为中介进行优化。
/* 1>F是空函数对于性能的损耗和内存的浪费忽略不计 2>不会导致child.prototype和parent.prototype指向同一个引用 */ function extend (parent, child) { function F (){} F.prototype = parent.prototype child.prototype = new F() child.prototype.constructor = child } // 将不共享的name属性定义在Person函数中 function Person(name) { this.name = name } // 将需要共享的getName方法 Person.prototype.getName = function(){ return this.name } // 定义构造函数Student(),定义属性name和hobby function Student(name, hobby){ // 通过call改变this的指向,继承Person中的属性name和方法getName() Person.call(this, name) this.hobby = hobby; } extend(Person, Student) Student.prototype.getHobby = function() { return this.hobby } var stu = new Student("小明", "basketBall"); console.log(stu.getName()); //小明 console.log(stu.getHobby()); //basketball console.log(Student.prototype.constructor) //Student console.log(Person.prototype.constructor) //Person
分析:
1>上例中我们封装了extend方法,extend中我们还是用空构造函数F作为中介,避免了之前实现方式中的缺点。
1>F是空函数对于性能的损耗和内存的浪费忽略不计
2>不会导致child.prototype和parent.prototype指向同一个引用
2>对于我们自己封装的extend方法,我们其实可以使用ES5中提供的Object.create(proto,properties)代替,实现的效果是一样的。
Student.prototype = Object.create(Person.prototype)
注意:Object.create()方法的内部实现和我们自己封装的extend方法类似。