js继承主要包括原型链继承、借用构造函数继承、组合继承(原型链+构造函数)、组合继承优化、寄生组合继承5中方式,后3中都是由前两个组合优化而来,所以要先了解原型及原型链相关内容。
1、原型对象及原型链
在js中,一切皆对象,所以原型也是一个对象,称为原型对象。
在js中,每个函数类型的数据,都有一个prototype的属性,该属性所指向的对象就是原型对象。对于原型对象而言,其constructor属性则指回构造函数(构造函数与普通函数本质上没有差别,只是可以用new 关键字创建对象)。原型对象最主要的作用是存放对象的共有属性和共有方法。
构造函数类似java中的类,每次创建实例对象时,都会重复创建一次构造函数里面的属性和方法,这就有些浪费了。如果把共有属性和方法定义到构造函数的原型对象里,各个实例对象直接原型对象的方法即可(通过原型链实现),避免浪费空间。
示例:
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log("构造函数方法--- ", this.name, this.age);
}
}
Person.prototype.sex = "女";
Person.prototype.color = ["yellow", "red", "blue"];
Person.prototype.hello = function () {
console.log("原型对象方法---- hello ", this.sex);
}
let p1 = new Person("dlj1", 20);
let p2 = new Person("dlj2", 30);
console.log("p1 is ", p1);
console.log("p2 is ", p2);
p2.hello();
console.log("p1.color ", p1.color.join("/"));
console.log("person 原型对象 ", p1.__proto__, p1.constructor.prototype == p1.__proto__, p1.__proto__ == Person.prototype, Person.prototype.constructor == p1.constructor);
console.log("person 原型对象上级原型对象包含join方法 ", p1.color.__proto__.hasOwnProperty("join"));
从上面的示例可以看出,实例对象中没有sex属性和hello方法,但是并不影响p2调用hello方法,就是因为当实例对象内没有调用的方法或属性时,就会向对应构造函数的原型对象中查找,如果原型对象中也没有,则会继续向原型对象的原型对象上查找,直到最顶级Object.prototype(所有原型对象都是object的实例),如果最顶级没有找到则返回undefined,这个顺着proto逐步向上查找,形成了像链条一样的结构就叫原型链(也叫隐式原型链)。例如p1的原型对象中没有join方法,p1.color可以调用join就是因为上级原型对象(array)中包含join方法。
2、继承方式介绍
1)原型链继承
将父类实例作为子类原型,由于方法定义在父类原型上,复用了父类构造函数的方法,所以实现了方法复用。
示例:
function Person(name) {
this.name = name || "父类";
this.say = function () {
console.log("父类构造函数方法--- ", this.name);
}
}
Person.prototype.sex = "女";
Person.prototype.color = ["yellow", "red", "blue"];
Person.prototype.hello = function () {
console.log("父类原型对象方法---- hello ", this.sex);
}
function Child(age) {
this.age = age;
}
Child.prototype = new Person("dlj");
Child.prototype.constrctor = Child;
Child.prototype.address = "北京";
Child.prototype.cHello = function () {
console.log("子类原型对象方法---- chello ", this.sex);
}
let chd1 = new Child("22");
console.log(chd1.say());
console.log(chd1.hello());
console.log(chd1.cHello());
console.log("age name sex --- ", chd1.age, chd1.name, chd1.sex);
console.log("chd1 ", chd1);
console.log("chd1.__proto__ ", chd1.__proto__);
console.log("chd1.__proto__.__proto__ ", chd1.__proto__.__proto__);
console.log("chd1.__proto__ has color is ", chd1.__proto__.hasOwnProperty("color"));
chd1.color.push("aa");
console.log(new Person().color);
从上面的示例可以看出:
①子类原型对象中包含了父类构造函数中的属性、方法和子类原型对象中的属性、方法,但是不包含父类原型对象中方法和属性color。
②子类实例能调用父类原型对象中的hello方法 color属性则是由于父类原型对象是子类原型对象的原型对象。
③父类原型对象中引用类型数据会在各个实例中共享,一个子类实例修改父类原型对象中引用属性的值(color),其他实例对象调用该引用属性时,值也是修改后的。
④只能继承一个父类,不能多继承。
注意:子类原型对象上定义属性和方法必须在Child.prototype = new Person("dlj")之后,否则子类原型对象上独有的属性和方法会丢失。
2)借用构造函数
借用父类的构造函数来增强子类实例,相当于复制父类的实例属性给子类,所以子类实例之间相互独立,不共享父类的引用属性,并可实现多继承(call或apply).
示例:
function Person1(name) {
this.name = name || "父类";
this.job = ["job1", "job2"];
this.say = function () {
console.log("父类构造函数方法--- ", this.name);
}
}
Person1.prototype.sex = "女";
Person1.prototype.color = ["yellow", "red", "blue"];
Person1.prototype.hello = function () {
console.log("父类原型对象方法---- hello ", this.sex);
}
function Person2(name) {
this.name2 = name || "父类2";
this.job2 = ["job1", "job2"];
this.say2 = function () {
console.log("父类构造函数方法--- ", this.name2);
}
}
Person2.prototype.sex2 = "女";
Person2.prototype.color2 = ["yellow", "red", "blue"];
Person2.prototype.hello2 = function () {
console.log("父类原型对象方法---- hello ", this.sex2);
}
function Child(name, age) {
Person1.call(this, name);
Person2.call(this, name);
this.age = age;
this.hello3 = function () {
console.log("子类构造函数方法--- ", this.age);
}
}
Child.prototype.address = "北京";
Child.prototype.cHello = function () {
console.log("子类原型对象方法---- chello ");
}
let chd1 = new Child("dlj", 11);
let chd2 = new Child("dlj1", 111);
console.log("chi1 is ", chd1);
console.log("chd1.__proto__ ", chd1.__proto__);
console.log("chd1.__proto__.__proto__ ", chd1.__proto__.__proto__);
chd1.job2.push("job3");
console.log("chd1.job2 ", chd1.job2, chd2.job2);
console.log("chd1.sex ", chd1.sex);//undefined 子类实例未继承父类原型中属性
chd1.say2();
//console.log("chd1.hello ", chd1.hello());//报错 chd1.hello is not a function 子类实例未继承父类原型中和方法
从示例中可以看出:
①因为通过父类构造函数继承,不涉及父类原型对象,所以父类原型对象中的属性和方法不可用(无法方法复用),子类只能调用自身和继承父类构造函数中的方法和属性。
②子类可以继承多个父类,实现多继承。
③父类中引用属性(job2)在子类实例中相互独立。
3)组合继承
将借用构造函数和原型链继承组合使用,既可以实现父类原型对象方法的复用,有可以使父类中引用属性相互独立并实现多继承。
function Person1(name) {
this.name = name || "父类";
this.job = ["job1", "job2"];
this.say = function () {
console.log("父类构造函数方法--- ", this.name);
}
}
Person1.prototype.sex = "女";
Person1.prototype.color = ["yellow", "red", "blue"];
Person1.prototype.hello = function () {
console.log("父类原型对象方法---- hello ", this.sex);
}
function Person2(name) {
this.name2 = name || "父类2";
this.job2 = ["job1", "job2"];
this.say2 = function () {
console.log("父类构造函数方法--- ", this.name2);
}
}
Person2.prototype.sex2 = "女";
Person2.prototype.color2 = ["yellow", "red", "blue"];
Person2.prototype.hello2 = function () {
console.log("父类原型对象方法---- hello ", this.sex2);
}
function Child(name, age) {
Person1.call(this, name);
Person2.call(this, name);
this.age = age;
this.hello3 = function () {
console.log("子类构造函数方法--- ", this.age);
}
}
Child.prototype = new Person1("dlj");
Child.prototype.constrctor = Child;
Child.prototype.address = "北京";
Child.prototype.cHello = function () {
console.log("子类原型对象方法---- chello ");
}
let chd1 = new Child("dlj", 11);
let chd2 = new Child("dlj1", 111);
console.log("chi1 is ", chd1);
console.log("chd1.__proto__ ", chd1.__proto__);
console.log("chd1.__proto__.__proto__ ", chd1.__proto__.__proto__);
chd1.job2.push("job3");
console.log("chd1.job2 ", chd1.job2, chd2.job2);
console.log("chd1.sex ", chd1.sex);
chd1.say2();
chd1.hello();
从示例可以看出子类实例中属性、方法与子类原型对象的属性和方法有重复,这也算是组合继承唯一的缺点了。
4)组合继承优化
3)的实现方式之所以造成属性或方法重复,是因为调用了两次创建父类实例的方法,这是将父类原型对象直接赋值给子类原型可以避免一次创建父类实例方法的调用,从而解决重复的问题。将上个示例中Child.prototype = new Person1("dlj") 改为Child.prototype = Person1.prototype(或者改为Child.prototype = Object.create(Person1.prototype));其他不动,与3)相比,重复属性和方法被去掉了。
示例:
改为Child.prototype = Object.create(Person1.prototype))时
接3)中示例
console.log("Child.prototype.__proto__ ", Child.prototype.__proto__);
console.log("Person1.prototype ", Person1.prototype);
console.log(Child.prototype.__proto__ == Person1.prototype);
//Child.prototype = new Person1("dlj")时
Child.prototype.__proto__ {}
Person1.prototype Person1 {
sex: '女',
color: [ 'yellow', 'red', 'blue' ],
hello: [Function],
constrctor: [Function: Child],
address: '北京',
cHello: [Function]
}
false
//Child.prototype = Object.create(Person1.prototype)时
Child.prototype.__proto__ Person1 {
sex: '女',
color: [ 'yellow', 'red', 'blue' ],
hello: [Function]
}
Person1.prototype Person1 {
sex: '女',
color: [ 'yellow', 'red', 'blue' ],
hello: [Function]
}
true
Object.create(object,propertiesObject)方法创建一个新对象,使用第一个参数来提供创建对象的proto(以第一个参数作为新对象的构造函数的原型),第二个可选参数,是添加到新创建对象的属性。