JavaScript原型链及对象继承的方法演变

297 阅读8分钟

一、原型和原型链概念

创建了一个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