前言 先来个整体印象。如图所示,JS中继承可以按照是否使用object函数(在下文中会提到),将继承分成两部分(Object.create是ES5新增的方法,用来规范化这个函数)。
其中,原型链继承和原生式继承有一样的优缺点,构造函数继承与寄生式继承也相互对应。寄生组合继承基于Object.create, 同时优化了组合继承,成为了完美的继承方式。ES6 Class Extends的结果与寄生组合继承基本一致,但是实现方案又略有不同。
下面马上进入正题。
一、原型链继承 核心: 将父类的实例作为子类的原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // 父类 function Person(name,age) { this.name = name || 'unknow' this.age = age || 0 this.hobbies = ['music','reading'] } // 为父类新增一个方法 Person.prototype.say = function() { console.log('I am a person') } // 子类 function Student(name){ this.name = name this.score = 80 } // 继承 注意:继承必须要写在子类方法定义的前面 Student.prototype = new Person() // 为子类新增一个方法(在继承之后, 否则会被覆盖) Student.prototype.study = function () { console.log('I am studing') } var stu = new Student('lucy') console.log(stu.name) // lucy --子类覆盖父类的属性 console.log(stu.age) // 0 --父类的属性 console.log(stu.score) // 80 --子类自己的属性 stu.say() // I am a person --继承自父类的方法 stu.study() // I am studing --子类自己的方法 console.log(stu instanceof Student) // true console.log(stu instanceof Person) // true var stu1 = new Student() var stu2 = new Student() stu1.hobbies.push('basketball') // 父类的引用属性会被所有子类实例共享 console.log(stu1.hobbies) // music,reading,basketball console.log(stu2.hobbies) // music,reading,basketball
特点:
1、非常纯粹的继承关系,实例是子类的实例,也是父类的实例;
2、父类新增原型方法/属性,子类都能访问到;
3、简单,易于实现;
缺点:
1、要想为子类新增方法,必须要在new Person()这样的语句之后执行;
2、无法实现多继承;
3、父类的引用属性被所有子类实例共享;
4、创建子类实例时,无法向父类构造器函数传参;
二、构造函数继承 为了解决原型链继承中存在的“父类引用属性被所有子类实例共享”和“无法向父类构造器函数传参”的问题,引入了构造函数继承的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 父类 function Person(name) { this.name = name this.hobbies = ['music','reading'] this.say = function() {} } // 子类 function Student(){ Person.call(this, name) // 实现了向父类构造器传参 } var stu1 = new Student('lucy') var stu2 = new Student('lili') stu1.hobbies.push('basketball') // 解决了父类引用属性被共享问题 console.log(stu1.hobbies) // music,reading,basketball console.log(stu2.hobbies) // music,reading console.log(stu1.name) // lucy console.log(stu2.name) // lili console.log(stu1 instanceof Student) // true console.log(stu1 instanceof Person) // false console.log(stu1.say === stu2.say) // false 证明了父类的函数,在子类的实例下是不共享的
构造函数解决了引用类型被所有实例共享的问题,但正是因为解决了这个问题,导致一个很矛盾的问题出现了,函数也是引用类型,也没办法共享了。也就是说,每个实例里面的函数,虽然功能一样,但是却不是同一个函数,就相当于我们每实例化一个子类,就复制了一遍的函数代码。
特点:
1、解决了原型链继承中,子类实例共享父类引用属性的问题;
2、创建子类实例时,可以向父类传递参数;
3、可以实现多继承(call多个父类对象);
缺点:
1、实例并不是父类的实例,只是子类的实例;
2、只能继承父类的实例属性和方法,不能继承原型属性和方法;
3、无法实现函数复用,每个子类都有父类实例函数的副本,影响性能;
三、组合继承 为了解决构造函数继承中函数无法共享复用的问题,采用函数使用原型链继承,普通属性仍然使用构造函数继承的方法,该继承称之为:组合继承。相当于取了原型链继承和构造函数继承的长处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 父类 function Person() { this.hobbies = ['music','reading'] } // 父类函数 Person.prototype.say = function() { console.log('I am a person') } // 子类 function Student(){ Person.call(this) // 构造函数继承(继承属性) 第二次调用Person() } // 继承 Student.prototype = new Person() // 原型链继承(继承方法) 第一次调用Person() // 实例化var stu1 = new Student() var stu2 = new Student() stu1.hobbies.push('basketball') console.log(stu1.hobbies) // music,reading,basketball console.log(stu2.hobbies) // music,reading console.log(stu1.say == stu2.say) // true
组合继承据说是JS中最常用的继承方式。
特点:
1、父类的函数可以被复用;
2、父类的引用属性不会被共享;
3、子类构建实例时可以向父类传递参数;
缺点:
调用了两次父类的构造函数,第一次给子类的原型添加了父类的hobbies属性,第二次又给子类的构造函数添加了父类的hobbies属性,从而覆盖了子类原型中的同名参数。这种覆盖的情况造成了性能上的浪费。
小结一下:
1、原型链继承会共享引用属性;
2、构造函数继承会独享所有属性,包括引用属性(重点是函数);
3、组合继承使上面两种互补,实现了相对完美的继承;
四、原生式继承 核心:原型式继承的object方法本质上是对参数对象的一个浅复制。
优点:父类方法可以复用
缺点:
1、父类的引用属性会被所有子类实例共享;
2、子类构建实例时不能向父类传递参数;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function object(o){ function F(){} F.prototype = o; return new F(); } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; // 下面这行代码等同于var yetAnotherPerson = Object.create(person); var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create()与 object()方法的行为相同。——《JAVASCript高级编程》
五、寄生式继承 核心:使用原生式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力
优缺点:仅提供一种思路,没什么优点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function createAnother(original){ var clone=object(original); //通过调用函数创建一个新对象 clone.sayHi = function(){ //以某种方式来增强这个对象 alert("hi"); }; return clone; //返回这个对象 } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //"hi"
六、寄生组合继承 刚才说到组合继承有一个会两次调用父类的构造函数造成浪费的缺点,寄生组合继承就可以解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); // 创建了父类原型的浅复制 prototype.constructor = subType; // 修正原型的构造函数 subType.prototype = prototype; // 将子类的原型替换为这个原型 } function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } // 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费 inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); }
优缺点:这是一种完美的继承方式.
七、ES6 Class extends 核心: ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
1 2 3 4 5 6 7 class A {} class B extends A { constructor() { super(); } }
ES6实现继承的具体原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class A { } class B { } Object.setPrototypeOf = function (obj, proto) { obj.__proto__ = proto; return obj; } // B 的实例继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype); // B 继承 A 的静态属性 Object.setPrototypeOf(B, A);
ES6继承与ES5继承的异同:
相同点:本质上ES6继承是ES5继承的语法糖
不同点:
1、ES6继承中子类的构造函数的原型链指向父类的构造函数,ES5中使用的是构造函数复制,没有原型链指向。
2、ES6子类实例的构建,基于父类实例,ES5中不是。
小结
1、ES6 Class extends是ES5继承的语法糖
2、JS的继承除了构造函数继承之外都基于原型链构建的
3、可以用寄生组合继承实现ES6 Class extends,但是还是会有细微的差别