继承的八种写法
继承的实现
推荐组合继承(四)、寄生组合式继承(七)、ES6 继承(八)
一、原型链法(使用原型)
基本思想是利用原型让一个引用类型继承另一个引用类型的方法和实例。
代码如下
function staff(){ this.company = 'ABC'; } staff.prototype.companyName = function(){ return this.company; } function employee(name,profession){ this.employeeName = name; this.profession = profession; } // 继承 staff employee.prototype = new staff(); // 将这个对象的 constructor 手动改成 employee,否则还会是 staff employee.prototype.constructor = employee; // 不使用对象字面量方式创建原型方法,会重写原型链 employee.prototype.showInfo = function(){ return this.employeeName + "'s profession is " + this.profession; } let instance = new employee('Andy','front-end'); // 测试 console.log(instance.companyName()); // ABC console.log(instance.showInfo()); // "Andy's profession is front-end" // 通过 hasOwnProperty() 方法来确定自身属性与其原型属性 console.log(instance.hasOwnProperty('employeeName')) // true console.log(instance.hasOwnProperty('company')) // false // 通过 isPrototypeOf() 方法来确定原型和实例的关系 console.log(employee.prototype.isPrototypeOf(instance)); // true console.log(staff.prototype.isPrototypeOf(instance)); // true console.log(Object.prototype.isPrototypeOf(instance)); // true
存在的问题
原型链实现继承最大的问题是:
当原型中存在引用类型值时,实例可以修改其值。
function staff(){ this.test = [1,2,3,4]; } function employee(name,profession){ this.employeeName = name; this.profession = profession; } employee.prototype = new staff(); let instanceOne = new employee(); let instanceTwo = new employee(); instanceOne.test.push(5); console.log(instanceTwo.test); // [1, 2, 3, 4, 5]
鉴于此问题:所以我们在实践中会少单独使用原型链实现继承。
小结
- 基于构造函数和原型链
- 通过
hasOwnProperty()
方法来确定自身属性与其原型属性 - 通过
isPrototypeOf()
方法来确定原型和实例的关系 - 在实例中可以修改原型中引用类型的值
二.仅继承父构造函数的原型对象
此方法和方法一区别就是将:
employee.prototype = new staff();
改成:
Employee.prototype = Person.prototype;
优点
- 构建继承关系时不需要新建对象实例
- 由于公用一个原型对象,所以在访问对象的时候不需要遍历原型链,效率自然就高
缺点
- 和方法一相同,子对象的修改会影响父对象。
小结
- 基于构造函数,没有使用原型链
- 子对象和父对象公用一个原型对象
三、借用构造函数法
此方法可以解决原型中引用类型值被修改的问题
function staff(){ this.test = [1,2,3]; } staff.prototype.companyName = function(){ return this.company; } function employee(name,profession){ staff.call(this); this.employeeName = name; this.profession = profession; } // 不使用对象字面量方式创建原型方法,会重写原型链 employee.prototype.showInfo = function(){ return this.employeeName + "'s profession is " + this.profession; } let instanceOne = new employee('Andy','front-end'); let instanceTwo = new employee('Mick','after-end'); instanceOne.test.push(4); // 测试 console.log(instanceTwo.test); // [1,2,3] // console.log(instanceOne.companyName()); // 报错 // 通过 hasOwnProperty() 方法来确定自身属性与其原型属性 console.log(instanceOne.hasOwnProperty('test')) // true // 通过 isPrototypeOf() 方法来确定原型和实例的关系 console.log(staff.prototype.isPrototypeOf(instanceOne)); // false
从上面的结果可以看出:
- 借用构造函数法可以解决原型中引用类型值被修改的问题
- 可是
instanceOne
与staff
已经没有原型链的关系了
缺点
- 只能继承父对象的实例属性和方法,不能继承父对象原型属性和方法
- 无法实现函数复用,每个子对象都有父对象实例的副本,性能欠优
四、组合继承(推荐)
指的是将原型链技术和借用构造函数技术结合起来,二者皆取其长处的一种经典继承方式。
function staff(){ this.company = "ABC"; this.test = [1,2,3]; } staff.prototype.companyName = function(){ return this.company; } function employee(name,profession){ // 继承属性 staff.call(this); this.employeeName = name; this.profession = profession; } // 继承方法 employee.prototype = new staff(); employee.prototype.constructor = employee; employee.prototype.showInfo = function(){ return this.employeeName + "'s profession is " + this.profession; } let instanceOne = new employee('Andy','front-end'); let instanceTwo = new employee('Mick','after-end'); instanceOne.test.push(4); // 测试 console.log(instanceTwo.test); // [1,2,3] console.log(instanceOne.companyName()); // ABC // 通过 hasOwnProperty() 方法来确定自身属性与其原型属性 console.log(instanceOne.hasOwnProperty('test')) // true // 通过 isPrototypeOf() 方法来确定原型和实例的关系 console.log(staff.prototype.isPrototypeOf(instanceOne)); // true
优点
- 可以复用原型上定义的方法
- 可以保证每个函数有自己的属性,可以解决原型中引用类型值被修改的问题
缺点
-
staff
会被调用 2 次:第 1 次是employee.prototype = new staff();
,第 2 次是调用staff.call(this)
。
五、原型式继承 - Object.create()
利用一个临时性的构造函数(空对象)作为中介,将某个对象直接赋值给构造函数的原型。
function object(obj){ function F(){} F.prototype = obj; return new F(); }
本质上 object()
对传入其中的对象执行了一次浅复制,将构造函数 F
的原型直接指向传入的对象。
var employee = { test: [1,2,3] } let instanceOne = object(employee); let instanceTwo = object(employee); // 测试 instanceOne.test.push(4); console.log(instanceTwo.test); // [1, 2, 3, 4]
缺点
- 原型中引用类型值会被修改
- 无法传递参数
另,ES5 中存在 Object.create()
的方法规范化了原型式继承,能够代替 object
方法。
六、寄生式继承
要点:在原型式继承的基础上,通过封装继承过程的函数增强对象,返回对象
function createAnother(original){ var clone = object(original); // 通过调用 object() 函数创建一个新对象 clone.sayHi = function(){ // 以某种方式来增强对象 alert("hi"); }; return clone; // 返回这个对象 }
createAnother
函数的主要作用是为构造函数新增属性和方法,以增强函数。
缺点(同原型式继承):
- 原型中引用类型值会被修改
- 无法传递参数
七、寄生组合式继承(推荐)
该方法主要是解决组合继承调用两次超类构造函数的问题。
function inheritPrototype(sub, super){ var prototype = Object.create(super.prototype); // 创建对象,父原型的副本 prototype.constructor = sub; // 增强对象 sub.prototype = prototype; // 指定对象,赋给子的原型 } function staff(){ this.company = "ABC"; this.test = [1,2,3]; } staff.prototype.companyName = function(){ return this.company; } function employee(name,profession){ staff.call(this, name); this.employeeName = name; this.profession = profession; } // 将父类原型指向子类 inheritPrototype(employee,staff) let instanceOne = new employee("Andy", "A"); let instanceTwo = new employee("Rose", "B"); instanceOne.test.push(4); // 测试 console.log(instanceTwo.test); // [1,2,3] console.log(instanceOne.companyName()); // ABC // 通过 hasOwnProperty() 方法来确定自身属性与其原型属性 console.log(instanceOne.hasOwnProperty('test')) // true // 通过 isPrototypeOf() 方法来确定原型和实例的关系 console.log(staff.prototype.isPrototypeOf(instanceOne)); // true
八、Class 的继承(推荐)
Class 可以通过 extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class staff { constructor(){ this.company = "ABC"; this.test = [1,2,3]; } companyName(){ return this.company; } } class employee extends staff { constructor(name,profession){ super(); this.employeeName = name; this.profession = profession; } } // 将父类原型指向子类 let instanceOne = new employee("Andy", "A"); let instanceTwo = new employee("Rose", "B"); instanceOne.test.push(4); // 测试 console.log(instanceTwo.test); // [1,2,3] console.log(instanceOne.companyName()); // ABC // 通过 Object.getPrototypeOf() 方法可以用来从子类上获取父类 console.log(Object.getPrototypeOf(employee) === staff) // 通过 hasOwnProperty() 方法来确定自身属性与其原型属性 console.log(instanceOne.hasOwnProperty('test')) // true // 通过 isPrototypeOf() 方法来确定原型和实例的关系 console.log(staff.prototype.isPrototypeOf(instanceOne)); // true
super
关键字,它在这里表示父类的构造函数,用来新建父类的 this
对象。
- 子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类没有自己的this
对象,而是继承父类的this
对象,然后对其进行加工。- 只有调用
super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super
方法才能返回父类实例。
`super` 虽然代表了父类 `A` 的构造函数,但是返回的是子类 `B` 的实例,即` super` 内部的 `this ` 指的是 `B`,因此 `super()` 在这里相当于 A.prototype.constructor.call(this)
ES5 和 ES6 实现继承的区别
ES5 的继承,实质是先创造子类的实例对象 this
,然后再将父类的方法添加到 this
上面(Parent.apply(this)
)。
ES6 的继承机制完全不同,实质是先创造父类的实例对象 this
(所以必须先调用 super()
方法),然后再用子类的构造函数修改 this
。
extends 继承核心代码(寄生组合式继承)
function _inherits(subType, superType) { subType.prototype = Object.create(superType && superType.prototype, { constructor: { value: subType, enumerable: false, writable: true, configurable: true } }); if (superType) { Object.setPrototypeOf ? Object.setPrototypeOf(subType, superType) : subType.__proto__ = superType; } }
由此可以看出:
- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类。 - 子类
prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
另:ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。
`super` 虽然代表了父类 `A` 的构造函数,但是返回的是子类 `B` 的实例,即` super` 内部的 `this ` 指的是 `B`,因此 `super()` 在这里相当于
A.prototype.constructor.call(this)
转载:https://github.com/wanqihua/blog/issues/5