在js中,原型,原型链及原型继承的重要性不言而喻,它不像java等语言通过class来继承:
以下以java中的继承为例:
// 父类
public class Person{
private String name,
private int age,
public Person(String name,int age){
this.name = name
this.age = age
}
...
}
// 子类继承父类
public class Teacher extents Person{
public Teacher(String name,int age){
super(name,age)
}
}
在es6之前,js是通过原型来继承;es6则借鉴java等语言推出了class语法糖,class只是语法糖,但本质上依旧是基于原型的继承。
既然es6推出了class语法糖,那还是先来看看class语法糖的使用吧,下面结合场景来使用:
有这么一个场景,假设我们有一个People个人类,
class People{
//
constructor(name,age,gender){
this.name = name
this.age = age
this.gender = gender
}
sayHi(){
// 下面console.log中为模板字符串,等同于 "hello,"+this.name+',性别'+this.gender+",年龄"+this.age
console.log(`hello,我是${this.name},性别${this.gender},年龄${this.age}`)
}
}
// 通过上面的People类模板,我们可以实例化一个个people实例
let p1 = new People('李四',18,'男')
p1.sayHi()
let p2 = new People('张三',18,'女')
p2.sayHi()
输入结果如下:
hello,我是李四,性别男,年龄18
hello,我是张三,性别女,年龄18
假设现在,People类下又分为Teacher教师类,Student学生类,教师类有一个独有的subject学科属性,学生类有一个独有的studentId学生id号属性,那我们是不是还要给Teacher类,Teacher类再把重复的name,age属性再写一遍呢? 当然不是,我们可以抽取公共的属性和方法建立父类,子类直接继承父类的属性和方法,而在子类中去写独有的部分或者是重写父类的方法,如下:
// People父类,包含子类中都存在的name,age,gender属性
class People{
constructor(name,age,gender){
this.name = name
this.age = age
this.gender = gender
}
sayHi(){
console.log(`hello,我是${this.name},年龄${this.age},性别:${this.gender}`)
}
}
class Teacher extends People{
constructor(name,age,gender,subject){
// super()调用父类的构造函数,而避免我们在这里还要this.age = age,虽然这里调用的是父类People的构造函数,但是this指向的依旧是当前子类Teacher类的实例对象
super(name,age,gender)
this.subject = subject // subject是当前Teacher类独有的,所以需要自行构造, this.subject = subject
}
// Teacher类独有的方法,给学生上课,其他子类是不具有的
attendClass(){
console.log(`${this.name}给学生们上:${this.subject}`)
}
// 重写父类People的sayHi方法
sayHi(){
console.log(`{this.name}老师对学生们说:上课`)
}
}
//学生类
class Student extends People{
constructor(name,age,gender,studentId){
super(name,age,gender)
// Student子类独有的studentId属性
this.studentId = studentId
}
sayHi(){
console.log(`名字:${this.name},性别:${this.gender},年龄:${this.age},学号:$this.studentId`)
}
}
// 实例化
let p = new People('王老头',60,'男')
p.sayHi()
let wangFang = new Teacher('王芳',30,'女','英语课')
wangFang.attendClass()
wangFang.sayHi()
let liHua = new Student('李华',18,'男',1003)
liHua.sayHi()
输入结果如下:
如上图,根据不同的构造函数创建出的实例对象,互不干扰,而且Teacher类的实例对象wangFang和Student类的实例对象liHua分别重写了父类People的sayHi方法。
注意:虽然es6语法提出了class相关规范,但还是基于原型的继承,class只是语法糖,目的是为了更容易理解继承关系。
如下图,虽然是上面的例子是通过class关键字创建类和继承,实际上还是基于原型的继承,这里仅列出Student构造出的实例对象liHua的原型链
以上,是es6新语法中的class面向对象思想和继承的使用。
那么,在es6之前,如何定义构造对象模板,想要继承该如何实现呢?
es6之前创建类的模板有几种方法,这里以构造函数为例:
function People(name,age,gender){
// 属性可以照常构建
this.name = name
this.age = age
this.gender = gender
}
// 但是要定义构造函数(对象)的方法,就不能直接在People中写,而是需要把方法挂载到对象的原型对象上,如下:
People.prototype.sayHi = function (){
console.log(`姓名:${this.name},年龄:${this.age},性别:${this.gender}`)
}
let p1 = new People('lisi',18,'男')
let p2 = new People('wf',20,'女')
p1.sayHi()
p2.sayHi()
代码运行结果:
姓名:lisi,年龄:10,性别:男
姓名:wf,年龄:20,性别:女
分析上述代码,可画出一个原型链图,如下:
People构造函数通过prototype获取的对象就是原型对象,简称原型,而通过People构造函数构造出的实例对象通过.__proto__就可以获取到这个原型对象。
显式原型:即通过prototype获取的原型
隐式原型:即通过.__proto__获取的原型
现在原型的概念知道了,原型链就是如上图所示,由一个个原型连起来的链式原型称为原型链。
// 构造函数、实例对象,原型对象之间的关系
let p2 = new People('lisi',20,'男')
console.log(p2.__proto === People.prototype) // true
console.log(p2.__proto__.__proto__ === People.prototype.__proto__) // true
console.log(p2.__proto__.proto__ === Object.prototype) //true
console.log(People.prototype.__proto__.__proto__) //null
// 查找规则
console.log(p2.age) // 20,当查找p2的age属性时,首先会在p本身寻找,如果找到就不会在继续查找;而如果没找到则会去p的原型上找age属性,如果没有,继续往p原型的上一级原型中查找,知道顶级object原型上没找到age属性,则返回undefined
// 简单来说,会沿着原型链一级级往上
instanceof作用也很明显了:判断某个引用类型的具体类型
let p2 = new People('lisi',20,'男')
console.log(p2 instanceof People) // true
console.log(p2 instanceof Object) // true
console.log([] instanceof People) //false ,因为[]不在该原型链上
hasOwnProperty的作用?
for(let key in p2){
// 循环p2实例对象中的每一个属性,并且判断该属性是不是在p2自身对象上
console.log(`${key} + ${p2.hasOwnProperty(key)}`)
}
输出结果如下
通过代码分析可知:hasOwnProperty的作用是判断对象自身属性中是否有指定的属性
如果想要实现继承,怎么做呢?
以代码为例:
function People(name,age,gender){
this.name = name
this.age = age
this.gender = gender
}
// 往People的原型上绑定一个方法
People.prototype.sayHi = function (){
console.log(`hello,我是${this.name},性别${this.gender},年龄${this.age}`)
}
// Student类中也有name,age,gender,还有一个独有的studentId属性
function Student(name,age,gender,studentId){
// 将People构造函数中的this指向为Student(准确说是通过Student构造的这个对象),相当于People构造函数中的this.name = name 就实际上转变成Teacher.name = name
People.call(this,name,age,gender)
// 将Student的显示原型对象的隐式原型指向People的原型对象,即在原型链上加了一个People原型
Student.prototype.__proto__ = People.prototype
this.sudentId = studentId
}
// 通过People构造出的实例对象
let p1 = new People('李四',18,'男')
// 通过Student构造出的实例对象
let s1 = new Student('王芳',18,'女',1003)
console.log(p1)
console.log(s1)
p1.sayHi()
s1.sayHi()
打印结果如下:
基于原型的继承,其实就是在原型链上加了一环,如下图:
注意:class是es6语法规范,是ECMA指定的语法规范,实现方式是v8引擎的实现方式,也是主流的。
关于原型继承,参考可见:基于原型的继承实现