一、原型和原型链概念
创建了一个Person类,它的原型链是怎样?
1、原型对象:每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象就是函数的原型对象。
2、构造函数也是一个函数,构造函数可以创建对象实例。
3、构造函数创建的对象实例会有一个__proto__属性([[prototype]]属性),这个属性也是一个指针,指向构造函数的原型对象。
也就是下图的左半部分。
4、函数也是一个对象实例,它是内置的Function构造函数创建出来的实例对象。因此,构造函数(比如Person、Function、Object的构造函数)也会有__proto__属性,指向Function构造函数的原型对象。
5、原型对象也是一个对象实例,它没有明确的构造函数实例化,它就认为是内置的Object构造函数创建出来的实例对象。因此,它也会有__proto__属性,指向Object构造函数的原型对象。
6、Object构造函数的原型对象是顶级原型对象,__proto__是null。
也就是下图的右半部分。
7、原型链:__proto__链接起来的链式结构就是原型链,下图的原型链是:Person实例对象->Person构造函数的原型对象->Object构造函数的原型对象。
二、通过原型链实现继承
继承:一个引用类型可以引用另一个引用类型的属性和方法。基于原型搜索机制,可以实现JavaScript的对象继承。
通过原型链继承,继承的属性和方法在重写的Student.prototype(Person实例)身上。
代码如下:
function Person(uname){
this.uname=uname;
}
var p=new Person("Alice")
function Student(grade){
this.grade=grade;
}
Student.prototype=p;
//可以给Student.prototype添加一些对象共用的属性和方法
Student.prototype.teacher="Miss Lee"
var s=new Student("primary school");
console.log(s.uname); //"Alice"
console.log(s.grade); //"primary school"
console.log(s.teacher); //"Miss Lee"
输出的对象s如下:
Student {grade: "primary school"}
grade: "primary school"
[[Prototype]]: Person
teacher: "Miss Lee"
uname: "Alice"
[[Prototype]]: Object
constructor: ƒ Person(uname)
[[Prototype]]: Object
可以看到上面有一个很明显的缺点是,创建的所有Student的实例,实例的uname属性值都是一样的,也就是共享了原型对象上的uname属性值,却没法传参改变这个原型对象上的属性值。同理。原型对象上的引用类型、函数也是被所有Student的实例共享。
三、借助构造函数实现继承——解决给父类型构造函数传参及引用类型共享问题
(无论对象创建还是继承,仅用原型都会有共享属性的问题,仅用构造函数都会有代码复用问题。)
借助构造函数继承,继承的属性和方法都在子类型Student对象实例身上。
function Person(uname){
this.uname=uname;
this.hobby=["singing","painting"];
}
function Student(uname,grade){
Person.call(this,uname);
this.grade=grade;
}
var s1=new Student("Alice","middel school"); //给父类型构造函数传参,每个实例的uname都不同
s1.hobby.push("hiking");
console.log(s1);
//输出Student {uname: "Alice", hobby: Array(3), grade: "middel school"}
var s2=new Student("Cindy","high school");
console.log(s2); //s1的hobby属性改变,不会影响s2
//输出Student {uname: "Cindy", hobby: Array(2), grade: "high school"}
但是这样的方式实现的继承,只继承了父亲节点,却没能继承祖父及其他祖先节点。因为原型链还是:Student对象->Student构造函数的原型对象->Object构造函数的原型对象。并且存存在固有的代码复用问题,如果想定义一个共有的方法,定义在Person构造函数中,n个对象就有n个函数,浪费了内存。
Student {uname: "Alice", hobby: Array(3), grade: "middel school"}
grade: "middel school"
hobby: (3) ["singing", "painting", "hiking"]
uname: "Alice"
[[Prototype]]: Object
constructor: ƒ Student(uname,grade)
[[Prototype]]: Object
四、组合式继承:组合使用构造函数和原型链实现继承——最常用的继承模式
使用组合式继承,把继承的属性放在子类型Student对象实例身上,而把要继承的共有的方法,按需放在重写的Student.prototype或是Person.prototype身上。
function Person(uname){
this.uname=uname;
this.hobby=["singing","painting"];
}
//复用的被继承方法应该定义在Person的原型上
Person.prototype.sayName=function(){
console.log(this.uname)
}
function Student(uname,grade){
Person.call(this,uname); //第二次调用Person,创建Student对象时调用
this.grade=grade;
//至此,创对象后,uname、hobby、grade都是对象实例自己的属性,前两者是继承的
}
//第一次调用Person
Student.prototype=new Person() //只改写原型对象,不传参,给父类型传参的工作交给call()
//可以给Student.prototype添加一些对象共用的属性和方法
var s1=new Student("Alice","middel school");
s1.hobby.push("hiking");
console.log(s1);
var s2=new Student("Cindy","high school");
console.log(s2);
因为原型链还是:Student对象->Person对象->Person构造函数的原型对象->Object构造函数的原型对象。一个对象的输出如下:
Student {uname: "Alice", hobby: Array(3), grade: "middel school"}
grade: "middel school"
hobby: (3) ["singing", "painting", "hiking"]
uname: "Alice" //Student实例的属性覆盖了原型对象Person实例的属性
[[Prototype]]: Person
hobby: (2) ["singing", "painting"]
uname: undefined
[[Prototype]]: Object
sayName: ƒ ()
constructor: ƒ Person(uname)
[[Prototype]]: Object
五、寄生组合式继承——最理想的继承模式
组合式继承的缺点是父类的构造函数(Person)调用了两次,看上面(四)的代码标注。
寄生组合式继承:使得父类的构造函数只调用一次,原型链是这样的:
Student对象->一个简单的对象(定义constructor为Student)-Person构造函数的原型对象->Object构造函数的原型对象。
function Person(uname){
this.uname=uname;
this.hobby=["singing","painting"];
}
//复用的被继承方法应该定义在Person的原型上
Person.prototype.sayName=function(){
console.log(this.uname)
}
function Student(uname,grade){
Person.call(this,uname);
this.grade=grade;
//至此,创对象后,uname、hobby、grade都是对象实例自己的属性,前两者是继承的
}
//调用Person
Student.prototype=Object.create(Person.prototype); //注意这两行代码的差异
//重写Student构造函数的原型,指向新对象A,而新对象A的原型是Person构造函数的原型。
Student.prototype.constructor=Student; //注意这两行代码的差异
输出的student对象如下:看到Student实例的原型对象(第一个[[Prototype]]的地方,不再是(四)的Person实例,是一个除了添加上的constructor属性,没有多余的属性的对象。
Student {uname: "Alice", hobby: Array(3), grade: "middel school"}
grade: "middel school"
hobby: (3) ["singing", "painting", "hiking"]
uname: "Alice"
[[Prototype]]: Person
constructor: ƒ Student(uname,grade)
[[Prototype]]: Object
sayName: ƒ ()
constructor: ƒ Person(uname)
[[Prototype]]: Object
这个模式也是,把继承的属性放在子类型Student对象实例身上,共有的方法,可以定义在Student.prototype上,也可以定义在Person.prototype上。
六、ES6 extends继承——寄生组合式继承语法糖
class Person{
constructor(name,age){
this.name=name
this.age=age
}
hobby=["hiking","singing"];
sayContry(){
console.log(this.country);
} //这里是添加在Person.prototype上的方法
}
class Student extends Person{
constructor(name,age,grade){
super(name,age);
this.grade=grade;
}
sayGrade(){
console.log(this.grade);
} //这里是添加在Sudent.prototype上的方法
}
let s=new Student("Alice",22,"primary")
输出的Student对象s如下:
Student {hobby: Array(2), name: "Alice", age: 22, grade: "primary"}
age: 22
grade: "primary"
hobby: (2) ["hiking", "singing"]
name: "Alice"
[[Prototype]]: Person
constructor: class Student
sayGrade: ƒ sayGrade()
[[Prototype]]: Object
constructor: class Person
sayContry: ƒ sayContry()
[[Prototype]]: Object
对比(五)的输出可以看出,他们的原型链是一样的,所以我认为这个是寄生组合式继承的语法糖。下面列举两个注意点
1、子类中的constructor不是必须的。
父类不变的情况下,定义这样的子类也是可以的。
class Student extends Person{
grade="middle"
sayGrade(){
console.log(this.grade);
}
}
let s=new Student("Cindy",20,"primarry")
输出的Student对象s如下,可以看到和上面别无二致,也是可以继承父类左右的属性的,而且还可以传参,就像是自动调用了super(name,age)一样。但是有一个问题就是,grade是写死的,创建Student对象没法传参改变grade。
Student {hobby: Array(2), name: "Cindy", age: 20, grade: "middle"}
age: 20
grade: "middle" //注意这里
hobby: (2) ["hiking", "singing"]
name: "Cindy"
[[Prototype]]: Person
constructor: class Student
sayGrade: ƒ sayGrade()
[[Prototype]]: Object
constructor: class Person
sayContry: ƒ sayContry()
[[Prototype]]: Object
2、但如果子类中有constructor并且想添加一写子类的属性,则必须在这个构造器最开头(this关键字之前)调用super()调用父函数的构造函数初始化,否则报错。
class Student extends Person{
constructor(grade){
this.grade=grade;
}
sayGrade(){
console.log(this.grade);
}
}
let s=new Student("middle")
上面的代码抛出错误:VM250:14 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
七:扩展篇
(一)原型式继承——捷径的继承方法
1、Object.create()函数:Object.create()方法创建一个新对象,使用现有的对象(函数的第一个参数)来提供新创建的对象的__proto__。就是创建对象的同时改变了它的__proto__属性所指向的原型对象,一步到位。相比于“原型链实现继承方式”,这种方式就少了个对象的构造函数,而且很简单。
function Person(uname){
this.uname=uname;
this.hobby=["singing","painting"];
}
var p=new Person("Alice")
var student=Object.create(p);
student.grade="middle school";
student对象的原型链是student实例对象->Person实例对象->Person构造函数的原型对象->Object构造函数的原型对象。这个方法可以不用写构造函数,可以很方便的实现了继承,但也有仅用原型链带来的原型中引用类型共享的问题。在只想让一个对象和另一个对象相似的情况下,这种方式很适用。
输出的student对象如下:
Person {grade: "middle school"}
grade: "middle school"
[[Prototype]]: Person
hobby: (2) ["singing", "painting"]
uname: "Alice"
[[Prototype]]: Object
constructor: ƒ Person(uname)
[[Prototype]]: Object
(二)寄生式继承
相对于原型式继承继承而言,只是将创建相似对象,并设置其独有属性的部分封装在函数内。
function Person(uname){
this.uname=uname;
this.hobby=["singing","painting"];
}
var p=new Person("Alice")
function createStudent(original){
var student=Object.create(original);
student.grade="middle school"
return student;
}
var student=createStudent(p)
输出的student对象如下,和上面原型式继承是一样的。
Person {grade: "middle school"}
grade: "middle school"
[[Prototype]]: Person
hobby: (2) ["singing", "painting"]
uname: "Alice"
[[Prototype]]: Object
constructor: ƒ Person(uname)
[[Prototype]]: Object