手撕js中的继承(一)

知识铺垫

构造函数

构造函数是用来批量创建对象的函数,本质上也是函数,区别于其他函数的方式就是调用的方式。
简单点说:如果没有使用new来调用的,就是普通函数。如果使用了new来调用的,就是构造函数。
话不多说直接上代码康康:

function Person(uname, age){
  this.uname = uname;
  this.age = age;
}
let IronMan = new Person('Tony stark', 30);
Person('Tony stark', 40);

console.log(IronMan);
console.log(uname);
console.log(age);

打印结果如下:

let IronMan = new Person('Tony stark', 30);中,通过new调用了函数Person,并且生成了IronMan,这里的Person就成了构造函数,IronMan就成了Person的一个实例。

原型对象

当我们每次创建一个函数的时候,函数对象都会有一个prototype属性,这个属性是一个指针,指向它的原型对象原型对象的本质也是一个对象。看代码:

function Person(uname, age){
  this.uname = uname;
  this.age = age;
}
console.log(Person.prototype);

打印如下:

我们可以看到Person.prototype指向一个对象,就是Person的原型对象,在这个原型对象中有一个constructor属性又指向了Person构造函数。

构造函数,原型对象和实例的关系

直接上图!

从上图可以看到:

  • 函数对象prototype指向原型对象,原型对象的constructor指向函数对象。
  • 实例对象的__proto__指向原型对象,__proto__的作用是允许实例通过该属性访问原型对象中的属性和方法
function Person(uname, age) {
  this.uname = uname;
  this.age = age;
}
Person.prototype.sex = 'man';
let IronMan = new Person('Tony stark', 28);
let BlackWidow = new Person('Natalia', 24);
BlackWidow.sex = 'woman'
console.log(IronMan.sex);
console.log(BlackWidow.sex);

打印结果如下:

可以看出,我们并没有给IronMan实例设置sex属性,但因为__proto__会访问原型对象中对应的属性,所以输出man;同时,我们给BlackWidow设置sex属性后输出的是woman,两者比较说明实例本身不存在对应的属性和方法时,才会去原型对象上查找对应的属性和方法
补充:在这里我们打印一下这句话:

console.log(IronMan.constructor);

打印结果:

通过实例的constructor可以访问构造函数,但是constructor本质上却是原型对象的属性而不是实例对象的,是实例对象通过__proto__找到原型对象prototype然后通过原型对象的constructor指回的构造函数。如果难以理解,我们不妨打印一下IronMan这个实例:
看,实例对象IronMan上并没有constructor属性,而是通过__proto__找到了原型对象,原型对象上拥有constructor属性并且指向了Person构造函数。

继承

原型链

在js中继承的主要思路就是利用了原型链。
原型链的原理是:让一个引用类型继承另一个引用类型的属性和方法。
既然我们知道了原型对象通过constructor指向构造函数,实例通过__proto__指向原型对象,那我们不妨想一想:如果让原型对象等于另一个构造函数的实例会怎么样?

function Father() {

}
Father.prototype.sayF = function () {
  console.log('from Father');
}

function Son() {

}
Son.prototype = new Father();
Son.prototype.sayS = function () {
  console.log('from Son');
}

let father = new Father();
let son = new Son();

son.sayS();
son.sayF();

结果:

上面过程发生了什么呢?我们再画一张图:
  • 首先我们创建了Father和Son两个函数对象,同时也就生成了他们的原型对象。
  • 接着我们给Father的原型对象添加了sayF()方法
  • Son.prototype = new Father(),这一步我们让函数对象Son的prototype指针指向了Father的实例,这也就是为什么Son原型对象里面不再有constructor属性,其实Son本来有一个真正的原型对象可以通过Son.prototype访问,结果我们手动修改了这个指针的指向,所以Son真正的原型对象现在没有办法被访问了,取而代之的是Father的一个实例,所以没有constructor这个属性。(把之前的例子带进来看,其实father这个实例通过__proto__可以访问到Father的原型对象,Father的原型对象上是有constructor这个属性的,所以我们通过打印Son.prototype.constructor,还是可以打印出结果,只不过这个结果却是Father这个构造函数,我们在写的时候通常还需要手动为Son.prototype添加constructor属性将它指回Son构造函数:Son.prototype.constructor = Son;)
  • 我们给Son.prototype指向的对象,增加一个sayS方法
  • 生成一个实例son,调用了son的sayS方法,可以执行:这是因为,son.__proto__可以访问到Son原型对象上的方法。
  • 调用了son的sayF方法,也可以执行:这是因为,son沿着__proto__属性,可以访问Son的原型对象,这时并没有sayF这个方法,Son的原型对象继续沿着__proto__属性访问Father的原型对象,最终在Father.prototype上找到了sayF方法。
    所以现在就相当于son继承了Father的属性和方法,这种由__proto__不断把实例和原型对象联系起来的结构其实就是原型链。这也是es6之前继承的主要方式。

你可能感兴趣的:(手撕js中的继承(一))