在前端开发中,我们离不开面向过程和面向对象这两种开发模式。面向过程是指分析出实现需求所需要的步骤,通过函数一步一步地实现这些步骤,接着依次调用。面向对象是指将整个需求按功能、特性划分,将这些存有共性的部分封装成对象,创建了对象不是为了完成某一个步骤,而是描述某个事物在解决问题的步骤中的行为。对于复杂的项目,我们经常会用到面向对象的方式来组织我们的代码。前人早已给我们总结了几种常用的继承方式,熟悉这几种继承方式,对于我们的日常开发会有很大的裨益。
在 ES5 中,JS 没有 class 类,JS 中的继承是通过原型的方式来实现的。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都会有一个原型对象的指针。先来看一个简单的示例图。
所有的引用类型都继承自 Object.prototype,而且是通过原型链实现的。这些对象都会有 Object 具有的一些默认方法。我们在浏览器控制台里先输入“Object”,可以发现 Object 的类型是 function,然后再输入“Object.prototype”,可以发现这个一个对象,而且里面就放在我们平时常用的一些方法,比如 hasOwnProperty、propertyIsEnumerable、toLocaleString、toString 等。既然 Object.prototype 也是一个对象,那它的原型是什么呢?我们再输入“Object.prototype.proto”,则返回为 null。由此可见,所有对象的原型链最顶层是 null。
高能预警
“__proto__”这个属性已从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。为了更好的支持,建议只使用 Object.getPrototypeOf()。 而且“__proto__”这个属性的兼容性方面 IE 只有到了 11 的版本才支持。而 getPrototypeOf 的方法在 IE9 就已经支持了。
通过现代浏览器的操作属性的便利性,可以改变一个对象的 [[Prototype]] 属性, 这种行为在每一个 JavaScript 引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在 obj.__proto__ = ... 语句上, 它还会影响到所有继承来自该 [[Prototype]] 的对象,如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]。相反, 创建一个新的且可以继承 [[Prototype]] 的对象,推荐使用 Object.create()。
原型链继承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function Student() {}
Student.prototype = new Person("junjun", 20);
Student.prototype.study = function () {
return this.name + " is studying";
};
var student1 = new Student();
var student2 = new Student();
console.log(student1.getName()); //junjun
console.log(student1.study()); //junjun is studying
console.log(student2.getName()); //junjun
console.log(student2.study()); //junjun is studying
Student.prototype.name = "lili";
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //lili
console.log(student2.study()); //lili is studying
student1.name = "weihua";
console.log(student1.getName()); //weihua
console.log(student1.study()); //weihua is studying
console.log(student2.getName()); //lili
console.log(student2.study()); //lili is studying
console.log(Student.prototype.constructor === Student); //false
console.log(Student.prototype.constructor === Person); //true
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true
要点:子类的原型等于父类的实例。
特点:实现简单;可继承构造函数的属性和方法,也可继承原型的属性和方法。
缺点:子类实例共享父类属性,父类属性修改后会影响所有实例;在创建子类的实例时,不能向父类的构造函数中传递参数;在给子类原型添加新的属性和方法要在继承后定义。
借用构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function Student(name, age) {
Person.call(this, name, age);
this.job = "study";
}
Student.prototype.study = function () {
return this.name + " is studying";
};
var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);
console.log(student1.getName()); //Uncaught TypeError: student1.getName is not a function
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //Uncaught TypeError: student1.getName is not a function
console.log(student2.study()); //huahua is studying
console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //false
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //false
console.log(Student.prototype.isPrototypeOf(student1)); //true
要点:父类的构造函数在子类构造函数中执行。
特点:可以向父类传参,且不会有原型属性共享的问题;可以继承多个构造函数属性
缺点:只能继承父类的实例属性和方法,不能继承原型属性和方法;每个新实例都有父类构造函数的副本,臃肿
组合继承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function Student(name, age) {
Person.call(this, name, age);
this.job = "study";
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.study = function () {
return this.name + " is studying";
};
var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying
console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true
要点:父类的构造函数在子类构造函数中执行。并将子类的原型赋值为父类的实例,之后将该实例的 constructor 指向子类构造函数。使用原型链实现对原型方法的继承,而通过借用构造函数来实现对实例属性的继承。
特点: 可以继承父类实例和原型中的属性和方法。
缺点: 在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
原型式继承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function CreateObj(obj, name, age) {
function F() {
this.name = name;
this.age = age;
}
F.prototype = obj;
F.prototype.study = function () {
return this.name + " is studying";
};
return new F();
}
var person = new Person();
var student1 = CreateObj(person, "huahua", 16);
var student2 = CreateObj(person, "weiwei", 14);
console.log(student1.getName()); //huahua
console.log(student1.study()); //huahua is studying
console.log(student2.getName()); //weiwei
console.log(student2.study()); //weiwei is studying
console.log(student1.__proto__); //Person 实例
console.log(student1.__proto__.constructor === Person); //true
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
要点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
特点:类似于复制一个对象,用函数来包装。
缺点:原型上的属性和方法都是共享的,所有后面对原型上的父类实例修改会影响所有实例;
寄生式继承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function Student(name, age) {
Person.call(this, name, age);
this.job = "study";
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.study = function () {
return this.name + " is studying";
};
var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying
console.log(Student.prototype.constructor === Student); //false
console.log(Student.prototype.constructor === Person); //true
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true
要点:主要思想就是不需要为了指定子类型的原型而去调用超类型的构造函数,我们需要的无非是超类型的原型副本。这样就会断绝父类修改对子类的影响。
特点:父类的修改不会影响到子类的实例。且能共享父类的方法。
ES6 中的继承
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
getAge() {
return this.age;
}
}
class Student extends Person {
constructor(name, age, job) {
super(name, age);
this.job = job;
}
study() {
return this.name + " is studying";
}
}
var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying
console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true
特点:除了子类原型的构造函数不同,其他和寄生继承实现的效果一致