一、什么是继承?
首先了解一下什么是原型链: JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……(原型链的尽头就是null) 也就是,访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着_proto_这条链向上找,这就是原型链。
那么,如何判断一个属性到底是基本的还是从原型中找到的呢?这就要用到hasOwnProperty():
function Dog(type) {
this.type = type;
this.say = 'wangwang';
}
//在原型对象中添加一个属性
Dog.prototype.name = '阿牧';
const dog = new Dog('德牧');
console.log(dog);
//自身没有name属性,是继承来的
//hasOwnProperty()判断属性或方法是否是自身的
console.log(dog.hasOwnProperty('name')); //false
console.log(dog.hasOwnProperty('say')); //true
打开控制台界面如下: 可以看到,name属性是从原型中找到的,并不是他自身的基本属性,而这个hasOwnProperty属性也是从Object.prototype中来的。 对象的原型链是沿着__proto__这条线走的,因此在查找dog.hasOwnProperty属性时,就会顺着原型链一直查找到Object.prototype。 由于所有对象的原型链都会找到Object.prototype,因此所有对象都会有Object.prototype的方法。这就是所谓的“继承”。
二、实现继承的方法
1、原型继承
实例化父类,让它作为子类的原型,从而实现子类可以访问到父类构造函数以及原型上的属性或者方法。 优点是简单易于实现,父类新增的实例与属性子类都能访问到; 缺点是在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给父类传递参数。
function Person(name , age) {
this.name = name ;
this.age = age ;
this.type = '人' ;
}
Person.prototype.say = function () {
console.log('我是人');
}
function YellowPerson(name , age) {
this.color = 'yellow' ;
this.talk = function () {
console.log('我黄种人');
}
}
YellowPerson.prototype = new Person('cc' , 18) ;
const y = new YellowPerson('yy' , 6) ;
console.log(y);
console.log(y.type);
y.say()
可以看到,const y = new YellowPerson('yy' , 6) 这条子类的参数并没有传上去。
2、构造函数继承
原理是调用父类的构造函数然后改变他的this指向,也就是复制父类的实例属性给子类 这样做的优点是解决了子类构造函数向父类传递参数的问题,可以实现多继承(call或apply多个父类),解决了原型中包含实例引用类型值被所有实例共享的问题; 缺点是不能继承原型属性、方法,只能继承父类的实例属性和方法,方法都在构造函数中定义,无法复用;
function Person(name, age) {
this.name = name;
this.age = age;
this.type = '人';
}
Person.prototype.say = function() {
console.log('我是人');
}
function YellowPerson(name, age) {
// 调用了一个函数,改变了它的this指向
Person.call(this, name, age);
this.color = 'yellow';
this.talk = function() {
console.log('我黄种人');
}
}
const y = new YellowPerson('cc', 18);
console.log(y);
可以看到,const y = new YellowPerson('cc', 18); 参数成功上传,但是原型中的say方法没有成功继承。 构造函数解决了引用类型被所有实例共享的问题,但正是因为解决了这个问题,导致一个很矛盾的问题出现了—函数也是引用类型,也没办法共享了。也就是说,每个实例里面的函数,虽然功能一样,但是却不是一个函数,就相当于我们每实例化一个子类,就复制了一遍函数代码。
3、组合继承
通过上述可以看出,构造函数继承和原型链继承的优缺点是互补的,组合继承就是各取上面2种继承的长处,普通属性使用构造函数继承,函数使用原型链继承。
这样做的缺点就是由于调用了两次父类,所以产生了两份实例,导致原型链上出现了多余的属性 。
function Person(name , age) {
this.name = name ;
this.age = age ;
this.type = '人' ;
}
Person.prototype.say = function () {
console.log('我是人');
}
function YellowPerson(name , age) {
Person.call(this , name , age) ;
this.color = 'yellow' ;
this.talk = function () {
console.log('我黄种人');
}
}
YellowPerson.prototype = new Person('cc' , 18) ;
const y = new YellowPerson('yy' , 6) ;
console.log(y);
console.log(y.type);
y.say()
console.log(y.name);
可以看到子类参数成功上传,原型对象中的方法也成功继承,但是也复制了多余的父类属性,父类构造函数被调用了两次。同时子类实例以及子类原型对象上都会存在name属性。虽然根据原型链机制,并不会访问到原型对象上的同名属性,但总归是不完美。
组合继承的优化
function Person(name , age) {
this.name = name ;
this.age = age ;
this.type = '人' ;
}
Person.prototype.say = function () {
console.log('我是人');
}
function YellowPerson(name , age) {
Person.call(this , name , age)
this.color = 'yellow' ;
this.talk = function () {
console.log('我黄种人');
}
}
//将原型对象深复制
YellowPerson.prototype = {...Person.prototype} ;
YellowPerson.prototype.aa = 'a' ;
const y = new YellowPerson('yy' , 19) ;
console.log(y);
console.log(Person.prototype);
没有多余属性,问题解决
4、寄生式继承(中间件继承)
寄生组合继承其实就是在组合继承的基础上,解决了父类构造函数调用两次的问题
//定义父类
function Person(name) {
this.type = 'human';
this.age = '18';
this.name = name;
}
//在原型中添加
Person.prototype.say = function() {
console.log('hi');
}
//定义继承方法
function fn() {}
fn.prototype = Person.prototype; //地址共享
function Chinese(name) {
this.color = 'yellow';
this.talk = function() {
console.log('我是中国人');
}
}
Chinese.prototype = new fn();
const c = new Chinese('csy')
console.log(c);
创建一个空的构造函数,让这个构造函数的原型对象指向父类的原型对象,也就是说这个构造函数就有了父类的原型对象上的属性和方法; 没有继承构造函数的属性和方法,只继承了原型方法和原型属性。这就是为什么组合寄生式继承优于普通的组合继承的地方,因为之前已经继承过一次,不再重复继承多一次原型的属性和方法。
下面实现组合寄生式继承: 就在Chinese添加Person.call(this, name) 这一行代码就行了 看结果: 完美继承
5、ES6的继承
ES6提供了class语法糖,同时提供了extends用于实现类的继承。这也是项目开发中推荐使用的方式。
class Person {
// 相当于构造函数
constructor(name , age) {
this.name = name ;
this.age = age ;
this.type = '人'
}
say() {
console.log('人');
}
}
// extends 继承
class YellowPerson extends Person {
constructor(name , age) {
// super 继承父类的属性和方法 ,
// 这个函数类似于 Person.call(this , name , age) + 原型对象的继承
super(name , age) ; // 向父类传参,必须要写这句话,否则会报错 super作为函数调用时表示父类的构造函数,但super内部的this指向的是子类
this.color = 'yellow' ;
}
talk() {
console.log('我是黄种人');
}
}
const y = new YellowPerson('yy' , 20) ;
console.log(y);
ES6中的类完全可以看作是构造函数的另一种写法; 上面的代码表明,类的数据类型就是函数,类的本身就指向构造函数; 使用时直接对类使用new命令,和构造函数的用法完全一致。
|