1、原型链继承
function Parent() {
this.name = "hehe";
this.play = [1, 2, 3];
}
function Child() {
this.type = "child2";
}
Child.prototype = new Parent();
let a = new Child();
let b = new Child();
a.play.push(4);
console.log(a.play); //[1,2,3,4]
console.log(b.play); //[1,2,3,4]
可以看出,上面的例子中,a与b两个实例,使用的是同一个原型对象,他们的内存空间是共享的,当一个发生变化的时候,另一个也随之发生了变化,这是使用原型链继承的一个缺点;
2、构造函数继承(借助call)
function Parent() {
this.name = "hehe";
this.play = [1, 2, 3];
}
function Child() {
Parent.call(this);
this.type = "child";
}
let a = new Child();
let b = new Child();
a.play.push(4);
console.log(a.play); //[1,2,3,4]
console.log(b.play); //[1,2,3]
截止到这里,Parent.call(this)完美解决了上面的问题,但是看一下下面的代码:
Parent.prototype.getName = function () {
return this.name;
};
function Child() {
Parent.call(this);
this.type = "child";
}
let a = new Child();
console.log(a); //正常显示 name play
console.log(a.getName()); // 报错 Uncaught TypeError: a.getName is not a function
这里说明构造函数继承优化了原型链继承的缺点,但是同样它本省只能继承父类的实例属性和方法,不能继承原型属性或者方法。
3、组合继承(前两种的组合)
function Parent() {
this.name = "hehe";
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
};
function Child() {
// 第二次调用 Parent3()
Parent.call(this);
this.type = "child3";
}
// 第一次调用 Parent3()
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;
let a = new Child();
let b = new Child();
a.play.push(4);
console.log(a.play);//[1,2,3,4]
console.log(a.play);//[1,2,3]
console.log(a.getName());//"hehe"
这种写法解决了上面的两个写法带来的问题,但是,我们可以看到,Parent3 执行了两次,多了一次性能开销,下面介绍到的第六种方式可以更好地解决这个问题;
4、上边三种更多的是围绕着构造函数的方式,下面介绍介绍普通对象的继承 (Object.create)
首先介绍一下Object.create的用法
语法:
Object.create(proto, [propertiesObject])
方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。
参数:
proto : 必须。表示新建对象的原型对象,即该参数会被赋值到目标对象(即新对象,或说是最后返回的对象)的原型上。该参数可以是null, 对象, 函数的prototype属性 (创建空的对象时需传null , 否则会抛出TypeError异常)。
propertiesObject : 可选。 添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。
返回值:
在指定原型对象上添加新属性后的对象。
举个例子:
Object.create() 方式创建 var a = { rep: 'apple' } var b = Object.create(a) console.log(b) // {} console.log(b.__proto__) // {rep: "apple"} console.log(b.rep) // {rep: "apple"}
回到继承:
let parent = {
name: "parents1",
friends: [1, 2, 3],
getName: function () {
return this.name;
},
};
let parent1 = Object.create(parent);
parent1.name = "hehe";
parent1.friends.push(4);
let parent2 = Object.create(parent1);
parent1.name = "hehehehhe";
parent1.friends.push(5);
console.log(parent1.name); // hehehehhe
console.log(parent1.name === parent1.getName()); // true
console.log(parent2.name); // hehehehhe
console.log(parent1.friends); // [1,2,3,4,5]
console.log(parent2.friends); // [1,2,3,4,5]
不难看出,这种继承方式的缺点也很明显,多个实例的引用类型属性指向相同的内存,存在篡改的可能;
5、寄生式继承:
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function () {
return this.name;
},
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function () {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
// let person5 = Object.create(parent5);
// person5.getFriends = function () {
// return this.friends;
// };
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // [p1,p2,p3]
这里是寄生式继承。但是还是多个实例的引用类型属性指向相同的内存。
6、寄生组合式继承,究极继承方式
function Parent() {
this.name = "hhe";
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
};
function Child() {
Parent.call(this);
this.name = "hahhahaa";
}
// 与第四个方法相比,这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
let a = new Child();
let b = new Child();
a.play.push(4);
console.log(a.play); // [1,2,3,4]
console.log(b.play); // [1,2,3]
console.log(a.getName()); // hahhahaa
这玩意得上手敲,光看看不那么透。
补充说一下ES6中extends的用法
class A {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
}
class B extends A {
constructor(name, age, heh) {
super();
this.job = "IT";
}
getJob() {
return this.job;
}
getNameAndJob() {
return super.getName() + this.job;
}
}
var b = new B("Tom", 20, 4);
console.log(b.age);
console.log(b.getName());
console.log(b.getJob());
console.log(b.getNameAndJob());
//输出:Tom,20,Tom,IT,TomIT
上面定义了一个B类,通过extends关键字,继承了A类的所有属性和方法,A类中的所有方法是默认添加到B的原型上,所以extends继承的实质仍然是原型链。
super这个关键字,既可以当做函数使用,也可以当做对象使用,当作函数使用时,super代表父类的构造函数,并在子类中执行Parent.call(this);可以参照上面提到的第六种继承方式;
注意:
1、子类必须在constructor中调用super方法,如果子类中没有定义constructor方法,constructor方法及其内部的super方法会被默认添加。
例:
上面可以直接写:class B extends A{}
2、在子类的constructor方法中,只有调用super之后,才可以使用this关键字,否则会报错;
3、super()智能用在子类的constructor方法之中,用在其他地方会报错。