「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
哈喽,我是刘十一,今天我们一起来看看JavaScript中有哪些继承以及它们各自的优缺点吧!!
一、原型链继承
1、关键
子类型的原型 为 父类型的 一个 实例对象。
2、代码示例
//父类型
function Person(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () { }
}
Person.prototype.setAge = function () {
console.log("111")
}
//子类型
function Student(price) {
this.price = price
this.setScore = function () { }
}
// Student.prototype.sayHello = function () { }
//在这里写子类的原型方法和属性是无效的,
//因为会改变原型的指向,所以应该放到重新指定之后
Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
//Student.prototype.sayHello = function () { }
//放在此处
var s1 = new Student(15000)
var s2 = new Student(14000)
console.log(s1,s2)
3、分析
本质是通过 将子类的原型 指向 父类的实例,故,子类的实例就可通过 proto 访问到 Student.prototype,即 Person的实例,如此就可以访问到父类的私有方法,随后再通过 proto 指向父类的prototype获取到父类原型上的方法。 即,实现了将 父类的私有、共有方法和属性都当做子类的共有属性
4、优缺点
1)优点
(1)父类新增原型方法/原型属性,子类都能访问到
(2)简单,易于实现
2)缺点
(1)无法实现多继承
(2)来自原型对象的所有属性被所有实例共享,父类的私有属性为引用类型时,子类s1操作这个属性的时候,就会影响到子类s2
(3)创建子类实例时,无法向父类构造函数传参
(4)要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中(因为会改变原型的指向,所以应该放到重新指定之后)
二、构造函数继承
1、关键
在 子类型构造函数中 通过 call () 调用 父类型构造函数
2、代码示例
function Person(name, age) {
this.name = name,
this.age = age,
this.setName = function () {}
}
Person.prototype.setAge = function () {}
function Student(name, age, price) {
Person.call(this, name, age) // 相当于: this.Person(name, age)
// this.name = name
// this.age = age
this.price = price
}
var s1 = new Student('Tom', 20, 15000)
console.log(s1.setAge()) // Uncaught TypeError: s1.setAge is not a function
3、分析
这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些在父类原型上的方法和属性的。
4、优缺点
1)优点
(1)解决了原型链继承中 子类实例 共享 父类属性的问题
(2)创建子类实例时,可以向父类传递参数
(3)可以实现多继承(call 多个父类dui)
2)缺点
(1)实例并不是父类是实例,只是子类的实例
(2)只能继承父类的实例的属性和方法,不能继承原型属性和方法
(3)无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
三、原型式继承
1、关键
原型式继承要求必须有一个对象可以作为另一个对象的基础
2、代码示例
function object(o){
function F(){}
F.prototype = 0;
return new F();
}
//在object函数内部,先创建了一个临时的构造函数,
//然后将传入的对象作为该构造函数的原型,
//最后返回了这个临时类型的新实例。
//从本质上讲,object() 对传入其中的对象执行了一次浅复制。
var person = {
name: 'Alvin',
friends: ['Yannis','Ylu']
}
var p1 = object(person);
p1.name = 'Bob';
p1.friends.push('Lucy');
var p2 = object(person);
p2.name = 'Lilei';
p2.friends.push('Hanmeimei');
console.log(person.friends);//Yannis, Ylu, Lucy, Hanmeimei
3、分析
在这个例子中可以作为另一个对象基础的是person对象, 于是我们把它传入到object函数中, 然后该函数就会返回一个新对象,这个新对象将person作为原型, 所以它的原型中就包含了一个基本类型属性name和一个引用类型属性friends。 这就意味着person.friends不仅属于person所有,而且也会被p1和p2共享。 实际上就相当于创建了person对象的两个副本。
4、拓展
在ECMAScript5中新增了Object.create()方法,该方法规范了原型式继承。
这个方法接收两个参数:一个用作新对象原型的对象,另一个是可选的,用于新对象定义额外的属性的对象。
在只传入一个参数的情况下,Object.create()与上面的object()方法行为相同。看下面示例:
var person = {
name: 'Alvin',
friends: ['Yannis','Ylu']
}
var p1 = Object.create(person);
p1.name = 'Bob';
p1.friends.push('Lucy');
var p2 = Object.create(person);
p2.name = 'Lilei';
p2.friends.push('Hanmeimei');
console.log(person.friends);//Yannis, Ylu, Lucy, Hanmeimei
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义。
以 这种方式指定任何属性都会覆盖原型对象上的同名属性。如:
var person = {
name: 'Alvin',
friends: ['Yannis','Ylu']
}
var p1 = Object.create(person,{
name:{
value:'Lucy'
}
})
console.log(p1.name);//Lucy
5、优缺点
1)优点
ECMAScript5通过新增 Object.create()方法规范了原型式继承。
2)缺点
包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
四、寄生式继承
1、关键
构造一个实例,让实例(instance)的prototype 代理 父类的prototype。
2、代码示例
//父类型
function Person(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () { }
}
Person.prototype.setAge = function () {
console.log("111")
}
//子类型
function Student(price) {
Person.call(this, name,age); // 相当于: this.Person(name,age)
this.price = price
this.setScore = function () { }
}
// 关键的三步
var F = function () {};
F.prototype = Person.prototype;
Student.prototype = new F();
3、分析
有个问题:一定需要中间变量吗,直接把Student的prototype指向Person不可以吗?
Child.prototype = Parent.prototype
其实这样做是有效果的,但是两个prototype指向了同一个对象的引用,子类的独立性就没了。
这就是为什么处理原型链时必须new一次。
4、寄生式继承的思路
我们期望得到的结果是, 能让子类的原型间接和父类联系起来。 如果能有一个原型, prototype是父类的, 然后让 子类的prototype 指向 这个原型,不就联系上了吗?
Obj.prototype = Parent.prototype
Child.prototype = new Obj()
可以发现,prototype被添加到了原型链中,而且并没有调用构造函数。
5、优缺点
1)优点
(1)使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。
2)缺点
(1)虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。
(2)跟构造函数继承类似,调用一次函数就得创建一遍方法,无法实现函数复用,效率较低
五、组合继承(原型链+构造函数)
1、关键
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
2、代码示例
function Person(name, age) {
this.name = name,
this.age = age,
this.setAge = function () { }
}
Person.prototype.setAge = function () {
console.log("111")
}
function Student(name, age, price) {
Person.call(this,name,age)
this.price = price
this.setScore = function () { }
}
Student.prototype = new Person()
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Jack', 22, 14000)
console.log(s1)
console.log(s1.constructor) //Student
console.log(Person.prototype.constructorr) //Person
3、分析
融合了 原型链继承 和 构造函数的优点,是JavaScript 中常用的继承模式。 存在的问题是:无论在何种情况下,构造函数都会调用两次。 一次是在 创建子类原型的时候; 另一次是在 子类型构造函数的内部。 子类型最终会包含父类型对象的全部实例属性,可我们又不得不在调用子类构造函数时重写这些属性。
4、优缺点
1)优点
(1)可以继承实例属性/方法,也可以继承原型属性/方法
(2)不存在引用属性共享问题
(3)可传参
(4)函数可复用
2)缺点
(1)调用了两次父类构造函数,生成了两份实例
六、组合继承(优化一)
1、关键
通过 父类原型和子类原型 指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点
2、代码示例
function Person(name, age) {
this.name = name,
this.age = age,
this.setAge = function () { }
}
Person.prototype.setAge = function () {
console.log("111")
}
function Student(name, age, price) {
Person.call(this,name,age)
this.price = price
this.setScore = function () { }
}
Student.prototype = Person.prototype
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
console.log(s1)
3、分析
但这种方式没办法辨别是对象是子类还是父类实例化
4、优缺点
1)优点
(1)避免了组合继承的缺点,不会初始化两次 实例方法/属性
2)缺点
(1)没办法辨别实例是 子类 还是 父类 创造的,子类与父类的构造函数 指向了同一个
七、组合继承(优化二)
1、关键
借助原型 可以基于 已有的对象 来创建对象。 var B = Object.create(A)以A对象为原型,生成了B对象。 B继承了A的所有属性和方法。
2、代码示例
function Person(name, age) {
this.name = name,
this.age = age
}
Person.prototype.setAge = function () {
console.log("111")
}
function Student(name, age, price) {
Person.call(this,name,age)
this.price = price
this.setScore = function () { }
}
Student.prototype = Object。create(Person.prototype)//核心代码
Student.prototype.constructor = Student //核心代码
var s1 = new Student('Tom', 20, 15000)
console.log(s1 instanceof Student, s1 instanceof Person) // true true
console.log(s1.constructor) //Student
console.log(s1)
3、分析
Student 继承了所以 Person 原型对象的属性和方法。 目前来说,是最完美的继承方法。
4、优缺点
优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
八、寄生组合式继承
1、关键
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
2、代码示例
下面代码展示了 寄生组合式继承的 基本模式:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(son,father){
var prototype = object(father.prototype);//创建对象
prototype.constructor = son;//增强对象
son.prototype = prototype;//指定对象
}
//在这个例子中inheritPrototype()函数 实现了寄生组合式继承的最简单形式。
//这个函数接收两个参数:子类型构造函数和父类型的构造函数。
//在函数内部,第一步是创建父类型原型的一个副本。
//第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。
//最后一步就是将新创建的副本赋值给子类型的原型。
这样我们就可以调用inheritPrototype()函数来替换上面例子中为子类型原型赋值的语句了,如下:
function Father(name){
this.name = name;
this.friends = ['Yannis','Lucy']
}
Father.prototype.sayName = function(){
console.log(this.name);
}
function Son(name, age){
Father.call(this,name);//第二次调用Father()
this.age = age;
}
//Son.prototype = new Father();//第一次调用Father()
inheritPrototype(Son,Father); //新语句
//或者直接用Object.create()也可以实现,效果是一样的
//Son.prototype = Object.create(Father.prototype);
//Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log(this.age)
}
3、分析
这个例子的高效率体现在它只调用了一次Father构造函数,并且因此避免了在Son.prototype上创建不必要的多余的属性。与此同时,原型链还能保持不变。
这就是寄生式组合继承,也是目前相对来说比较好的继承方式.
4、寄生组合式继承的思路
不必为了子类型的原型而调用父类型的构造函数,我们所需的无非是要父类型原型的一个副本而已。 本质上就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
5、优缺点
组合继承优点、寄生继承的优点,目前JS继承中使用的都是这个继承方法
九、ES 6 中 class继承
ES6 中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字 定义类的 静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。 ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面( Parent.apply(this) )。 ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
1、关键
class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的
2、代码示例
class Person {
//调用类的构造方法
constructor(name, age) {
this.name = name
this.age = age
}
//定义一般的方法
showName() {
console.log("调用父类的方法")
console.log(this.name, this.age);
}
}
let p1 = new Person('kobe', 39)
console.log(p1)
//定义一个子类
class Student extends Person {
constructor(name, age, salary) {
super(name, age)
//通过super调用父类的构造方法
this.salary = salary
}
showName() {//在子类自身定义方法
console.log("调用子类的方法")
console.log(this.name, this.age, this.salary);
}
}
let s1 = new Student('wade', 38, 1000000000)
console.log(s1)
s1.showName()
3、优缺点
1)优点
(1)语法简单易懂,操作更方便
2)缺点
(1)并不是所有的浏览器都支持class关键字