构造函数、原型与实例之间的关系
每创建一个函数,该函数就会自动带有一个 prototype 属性。该属性是个指针,指向了一个对象,我们称之为 原型对象。什么是指针?指针就好比学生的学号,原型对象则是那个学生。我们通过学号找到唯一的那个学生。假设突然,指针设置 null, 学号重置空了,不要慌,对象还存在,学生也没消失。只是不好找了。
原型对象上默认有一个属性 constructor,该属性也是一个指针,指向其相关联的构造函数。
通过调用构造函数产生的实例,都有一个内部属性,指向了原型对象。所以实例能够访问原型对象上的所有属性和方法。
所以三者的关系是,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。通俗点说就是,实例通过内部指针可以访问到原型对象,原型对象通过constructor指针,又可以找到构造函数。
function Dog (name) {
this.name = name;
this.type = 'Dog';
}
Dog.prototype.speak = function () {
console.log('wang');
}
var doggie = new Dog('jiwawa');
doggie.speak(); //wang
以上代码定义了一个构造函数 Dog(), Dog.prototype 指向的原型对象,其自带的属性construtor又指回了 Dog,即 Dog.prototype.constructor == Dog. 实例doggie由于其内部指针指向了该原型对象,所以可以访问到 speak方法。
Dog.prototype 只是一个指针,指向的是原型对象,但是这个原型对象并不特别,它也只是一个普通对象。假设说,这时候,我们让 Dog.protptype 不再指向最初的原型对象,而是另一个类 (Animal)的实例,情况会怎样呢?
原型链
面我们说到,所有的实例有一个内部指针,指向它的原型对象,并且可以访问原型对象上的所有属性和方法。doggie实例指向了Dog的原型对象,可以访问Dog原型对象上的所有属性和方法;如果Dog原型对象变成了某一个类的实例 aaa,这个实例又会指向一个新的原型对象 AAA,那么 doggie 此时就能访问 aaa 的实例属性和 AA A原型对象上的所有属性和方法了。同理,新的原型对象AAA碰巧又是另外一个对象的实例bbb,这个实例bbb又会指向新的原型对象 BBB,那么doggie此时就能访问 bbb 的实例属性和 BBB 原型对象上的所有属性和方法了。
这就是JS通过原型链实现继承的方法了。看下面一个例子:
/定义一个 Animal 构造函数,作为 Dog 的父类
function Animal () {
this.superType = 'Animal';
}
Animal.prototype.superSpeak = function () {
alert(this.superType);
}
function Dog (name) {
this.name = name;
this.type = 'Dog';
}
//改变Dog的prototype指针,指向一个 Animal 实例
Dog.prototype = new Animal();
//手动挂载构造器,指向自己的构造函数
Dog.prototype.constructor = Dog
//上面那行就相当于这么写
Dog.prototype.speak = function () {
alert(this.type);
}
var doggie = new Dog('jiwawa');
doggie.superSpeak(); //Animal
解释一下。以上代码,首先定义了一个 Animal 构造函数,通过new Animal()得到实例,会包含一个实例属性 superType 和一个原型属性 superSpeak。另外又定义了一个Dog构造函数。然后情况发生变化,代码中加粗那一行,将Dog的原型对象覆盖成了 animal 实例。当 doggie 去访问superSpeak属性时,js会先在doggie的实例属性中查找,发现找不到,然后,js就会去doggie 的原型对象上去找,doggie的原型对象已经被我们改成了一个animal实例,那就是去animal实例上去找。先找animal的实例属性,发现还是没有 superSpeack, 最后去 animal 的原型对象上去找,诶,这才找到。
这就说明,我们可以通过原型链的方式,实现 Dog 继承 Animal 的所有属性和方法。
总结来说:就是当重写了Dog.prototype指向的原型对象后,实例的内部指针也发生了改变,指向了新的原型对象,然后就能实现类与类之间的继承了。
js常见的6种继承方式
原型链继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。
function inherit1(){
this.name = "冰墩墩"
this.arr = [1,2,3,4]
}
function inherit2(){
this.age = "男"
}
inherit2.prototype = new inherit1()
let new1 = new inherit2()
let new2 = new inherit2()
new1.arr.push(5)
console.log(new1.arr) // [1,2,3,4,5]
console.log(new2.arr) // [1,2,3,4,5]
明明我只改变了new1的play属性,为什么new2也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。
构造函数继承
function inherit1(){
this.name = "冰墩墩"
this.arr = [1,2,3,4]
}
inherit1.prototype.getName = function(){
console.log(this.name)
}
function inherit2(){
inherit1.call(this)
this.age = "男"
}
let new1 = new inherit2()
let new2 = new inherit2()
new1.arr.push(5)
console.log(new1.arr) // [1,2,3,4,5]
console.log(new2.arr) // [1,2,3,4]
console.log(new1.name2) // undefined
console.log(new1.getName()) // 报错
可以看到我改变了new1的arr属性,new2的arr属性没有发生改变,解决了原型链继承的弊端。但他也有一个弊端就是,只能继承父类的实例属性和方法,不能继承原型属性或者方法
组合继承(结合原型链继承和构造函数继承)
function inherit1(){
this.name = "冰墩墩"
this.arr = [1,2,3,4]
}
inherit1.prototype.getName = function(){
return this.name
}
inherit1.prototype.name2 = "雪融融"
// 使用构造函数继承
function inherit2(){
inherit1.call(this)
this.age = "男"
}
// 使用原型链继承 解决原型属性和方法无法继承的问题
inherit2.prototype = new inherit1()
inherit1.prototype.constructor = inherit2
let new1 = new inherit2()
let new2 = new inherit2()
new1.arr.push(5)
console.log(new1.arr) // [1,2,3,4,5]
console.log(new2.arr) // [1,2,3,4]
console.log(new1.name2) // undefined
console.log(new1.getName()) // 报错
可以看到这次解决了前两种方式产生的问题,但是inherit1被执行了两次,第一次是改变inherit2的prototype的时候,第二十是通过call方法调用inherit1的时候,所以就多进行了一次性能开销。
原型式继承
这里不得不提到的就是ES5里面的Object.create方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。
let inherit1 = {
name: "冰墩墩",
date: "2022",
arr: [1,2,3,4]
}
let new1 = Object.create(inherit1)
new1.name = "冰墩墩可爱"
new1.arr.push(5)
let new2 = Object.create(inherit1)
new2.name1 = "雪融融可爱"
new2.arr.push(6)
console.log(new1.name) //冰墩墩可爱
console.log(new1.arr) //[1,2,3,4,5,6]
console.log(new2.name1) //雪融融可爱
console.log(new2.arr) //[1,2,3,4,5,6]
可以看到使用原型式继承出现了类似于前拷贝的问题
寄生式继承
let inherit1 = {
name: "冰墩墩",
date: "2022",
arr: [1, 2, 3, 4],
getName: function () {
console.log(this.name)
}
}
function clone(inherit) {
let inherit2 = Object.create(inherit)
inherit2.getDate = function () {
console.log(this.date)
}
return inherit2
}
let inherit2 = clone(inherit1)
inherit2.name = "冰墩墩可爱"
inherit2.arr.push(5)
let inherit3 = clone(inherit1)
inherit3.name1 = "雪融融可爱"
inherit3.arr.push(6)
console.log(inherit2.name) //冰墩墩可爱
console.log(inherit2.arr) //[1,2,3,4,5,6]
console.log(inherit3.name1) //雪融融可爱
console.log(inherit3.arr) //[1,2,3,4,5,6]
寄生式组合继承
结合第四种中提及的继承方式,解决普通对象的继承问题的Object.create方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式,代码如下。
function clone(inherit, child) {
child.prototype = Object.create(inherit.prototype)
child.prototype.constructor = child
}
function inherit1() {
this.name = "张三"
this.sex = "男"
this.arr = [1,2,3,4]
}
inherit1.prototype.seeName = function () {
console.log(this.name)
}
function inherit2() {
inherit1.call(this)
this.age = 18
}
clone(inherit1, inherit2)
inherit2.prototype.seeAge = function () {
console.log(this.age)
}
let new1 = new inherit2()
new1.arr.push(5)
let new2 = new inherit2()
new2.arr.push(6)
console.log(new1.arr) // [1,2,3,4,5]
console.log(new2.arr) // [1,2,3,4,6]
new1.seeAge()
new1.seeName()
通过这段代码可以看出来,这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销,我们来看一下上面这一段代码的执行结果。
ES6的extends关键字实现逻辑
ES6的extends关键字实现逻辑
我们可以利用ES6里的extends 的语法糖,使用关键词很容易直接实现JavaScript的继承,但是如果想深入了解extends 语法糖是怎么实现的,就得深入研究extends的底层逻辑(就是使用寄生式组合继承实现的)
class animal {
constructor (name) {
this.name = name
}
getName(){
return this.name
}
}
class dog extends animal {
constructor(name,age){
super(name)
this.age = age
}
}
let example = new dog('狗',20)
console.log(example.getName())