# 说明

原型和继承的关系相辅相成,原型链是Javascript实现继承的核心原理,继承是原型链的主要应用场景。

# 一.原型

# 1.原型的概念

原型是JavaScript对象中的一个隐藏属性[[prototype]] (这个属性可以理解为指向原型对象的指针),其本质上就是一个可以给多个实例对象 (有着共同的原型) 共享属性和方法的对象

# 2.如何获取原型对象

① 通过对象的__proto__获取,用这个方法更加便于理解,但是不推荐。(浏览器提供,本质是原型对象的getter/setter)

const cat = {
	skill(){
     console.log('卖萌');
   	}
}

cat.__proto__={
    eat:true
};

cat.skill(); // 卖萌
console.log(cat.eat);; // true
console.log(cat.__proto__); // {eat: true}

② 通过构造函数的prototype属性(普通属性,例如F.prototype)获取,也可以理解为构造函数的一个指向原型对象的指针属性。

function Person(){}

Person.prototype.name = "张三";

const person1 = new Person();
person1.age = 19;

console.log(person1.name); // 张三 原型属性
console.log(person1.age); // 19 实例属性

const person2 = new Person();
person2.age = 18;
console.log(person2.name);// 张三 原型属性 实例间共享
console.log(person2.age); // 18

person2.name = '李四';
console.log(person2.name); // 李四,原型属性被覆盖

console.log(Person.prototype.name) // 张三 原型属性不受影响

③ 通过Object.getPrototypeOf(obj)获取,通过Object.setPrototypeOf(obj,protoObj)为实例对象设置新的原型对象

# 3.原型链

原型链的概念出现在JavaScript继承关系中,是实现继承的核心原理。其主要原理是,当前实例的构造函数的原型属性Son.prototype的原型指针[[prototype]]指向父构造函数的原型对象Father.prototype。以数组为例:

1.创建一个字面量数组[1,2,3]

const arr = [1,2,3];
console.log(arr.__proto__===Array.prototype); //true

2.Array则继承自Object

console.log(Array.prototype.__proto__ === Object.prototype); // true

3.再往上,Object的原型属性的原型则为null

console.log(Object.prototype.__proto__); // null

这就是最简单的原型链,且在同一原型链内的所有属性和方法,都会共享给末端构造函数的实例对象,例如Array的实例都可以调用Object的原型属性和方法,Object的实例却不能调用Array的原型属性和方法

# 二.继承

# 说明

我们都知道,JavaScript中的继承,不管是原型继承和是类继承,本质上都是利用原型链加上构造函数来实现的,而类也只是构造函数的一个语法糖,所以,这里重点介绍利用原型来实现继承。

# 1.原型继承

利用原型链来实现继承,优点是可以共享所有的属性和方法,缺点也是,例如:

function SuperType (){
	this.colors = ['red','blue','green'];
}

function SubType(){}

// 继承SuperType
SubType.prototype = new SuperType();

let instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']

let instance2 = new SubType();
console.log(instance2.colors); // ['red', 'blue', 'green', 'black']

① 在使用原型实现继承时,原型实际上变成了父类型的一个实例,也就是说,原先的实例属性(这里是colors)摇身一变成为了原型属性,会在所有的实例之间共享。而且因为重定义了原型,所以原型的构造方法constructor也会丢失。

② 因为这里子类型和父类型之间的联系只有原型链,所以子类型在实例化的时候并不能给父类型的构造函数传参

所以这个方法在实际开发中,基本上不会用到

# 2.构造函数继承(也叫做盗用构造函数)

为了解决原型继承的两个缺点,也可以用到构造函数继承,本质上就是利用call()方法或者apply()方法更改this的指向(也叫做上下文),在子类构造函数中调用父类构造函数,从而实现调用父构造的同时也能将继承的属性私有化。优点是可以继承实例属性而不共享,缺点是不能继承原型属性和方法,例如:

function SuperType (){
	this.colors = ['red','blue','green'];
}

function SubType(){
	SuperType.call(this);
}

let instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); //  ['red', 'blue', 'green', 'black']

let instance2 = new SubType();
console.log(instance2.colors); // ['red', 'blue', 'green']

注意:为了确保调用父构造函数时不会覆盖子构造自身定义的属性,父构造函数的调用必须放在最前面

① 因为子类型与父类型之间的联系只有构造函数,所以方法也只能定义在构造函数中,这样就不能实现方法的重用了(需要开辟新的内存空间,每个实例的同名方法其实并不是同一个方法)

所以这个方法在实际开发中也基本上不会单独使用

# 3.组合继承

结合原型继承和构造函数继承各自优点的继承方法,本质上的思路是使用原型链来继承原型上的属性和方法,使用盗用构造函数来继承实例属性。这样就可以把方法定义在原型上来实现方法重用,又可以让每个实例都有自己的属性。例如:

function SuperType (name){
    this.name = name;
    this.colors = ['red','blue','green'];
}

SuperType.prototype.sayName = function(){
	console.log(this.name);
}

function SubType(name,age){
    // 步骤2:继承实例属性,本质上第二次调用父构造,将第一步中的原型属性覆盖掉,也可以理解为将继承的实例属性私有化了
    SuperType.call(this,name);

    // 调用父构造之后,再设置自身的属性
    this.age = age;
}

// 步骤1:继承原型属性和方法,但是同时也继承了实例属性,不过在这里父类实例属性变成了自己的原型属性(共享),直接重设了子类原型对象
SubType.prototype = new SuperType();
// 需要解决constructor丢失的问题
SubType.prototype.constructor = SubType;
// 定义自身的原型方法
SubType.prototype.sayAge = function(){
	console.log(this.age);
}

let instance1 = new SubType('小明',18);
instance1.colors.push('black');
console.log(instance1.colors); //  ['red', 'blue', 'green', 'black']
instance1.sayName(); // 小明
instance1.sayAge(); // 18

let instance2 = new SubType('张三',19);
console.log(instance2.colors); //  ['red', 'blue', 'green']
instance2.sayName(); // 张三
instance2.sayAge(); // 19

这个方法是JavaScript中使用最多的继承方式,但是由于父类构造函数始终会被调用两次,一次是在创建子类原型时被调用,另外一次是在子类构造中被调用,所以会存在一定的效率问题

比较完美且规范的解决方案是:

​ 1.浅拷贝一个父类型的原型对象

​ 2.然后把这个拷贝对象设置为子类型的原型对象

​ 3.将这个拷贝对象的constructor方法指向子类型构造函数

​ 4.之后就可以往这个对象中添加其他的原型属性和方法了

这样就只需要在子类构造中调用一次父构造函数即可,继承的原型属性也不会包含实例属性

# 4.不太规范的继承方式(比较好理解)

在上面我们可以知道组合继承需要调用两次父构造,我们就可以在设置子类型原型时,不直接赋值父构造的实例,也不拷贝父类型的原型,而是直接将子类型原型Son.protorype的原型指针[[prototype]]指向父类型的原型对象Father.prototype,例如:

function SuperType (name){
    this.name = name;
    this.colors = ['red','blue','green'];
}

SuperType.prototype.sayName = function(){
	console.log(this.name);
}

function SubType(name,age){
    // 步骤2:继承实例属性
    SuperType.call(this,name);

    // 调用父构造之后,再设置自身的属性
    this.age = age;
}

// 步骤1:继承原型属性和方法
SubType.prototype.__proto__ = SuperType.prototype;
// 这里并没有重设原型,所以也不需要解决constructor丢失的问题
// SubType.prototype.constructor = SubType;
// 定义自身的原型方法
SubType.prototype.sayAge = function(){
	console.log(this.age);
}

let instance1 = new SubType('小明',18);
instance1.colors.push('black');
console.log(instance1.colors); //  ['red', 'blue', 'green', 'black']
instance1.sayName(); // 小明
instance1.sayAge(); // 18

let instance2 = new SubType('张三',19);
console.log(instance2.colors); //  ['red', 'blue', 'green']
instance2.sayName(); // 张三
instance2.sayAge(); // 19

这个方法的原型链和构造函数都非常清晰,在学习的过程中可以按照这个方法来实现,但是实际开发中不推荐