# 说明
原型和继承的关系相辅相成,原型链是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
这个方法的原型链和构造函数都非常清晰,在学习的过程中可以按照这个方法来实现,但是实际开发中不推荐