您的位置:首页 > Web前端 > JavaScript

JavaScript对象继承

2017-08-13 17:16 253 查看
通常的面向对象的编程支持两种继承方式:接口继承和实现继承;ECMAScript中仅支持实现继承;

原型链继承

原型链继承方式的实质是用一个新类型的实例重写原型对象;显然这种方式不能使用字面量形式重写prototype;

该方式的缺点:一是原型中包含引用类型时一个实例对原型的引用类型值改变会影响到其他所有的实例对象;二是创建子类实例时无法在不影响其他子类实例的情况下向父类构造函数传递参数;

// 原型链继承
function Person () {
this.type = 'person';
this.cans = ['talk', 'eat', 'sleep'];
this.greet = function () {
console.log('Hello, I am ' + this.type);
}
}

function Student () {
this.type = 'student';
this.name = 'B';
this.gender = 'male';
this.greet = function () {
console.log('Hello, I am ' + this.type);
}
}

Student.prototype = new Person();

var person1 = new Person();
var stu1 = new Student();
var stu2 = new Student();

stu1.cans.push('sing');
console.log(stu1.cans); // [ 'talk', 'eat', 'sleep', 'sing' ]
console.log(stu2.cans); // [ 'talk', 'eat', 'sleep', 'sing' ]


借用构造函数

该方式与上述方法不同,其在子类构造函数内部调用父类构造函数,可以借助call和apply实现;该方式解决了原型中包含引用类型值的问题;

优点:通过调用call和apply,可以在子类构造函数向父类构造函数传递参数,且不影响其他子类实例;

缺点:同样存在与构造函数一样的问题,在构造函数中定义的方法无法复用;而且在父类原型中定义的方法子类实例无法访问到;

// 借用构造函数
function Person (name, age) {
this.name = name;
this.age = age;
this.cans = ['talk', 'eat', 'sleep'];
}
Person.prototype.greet = function () {
console.log(`Hello, ${name}`);
}

function Student () {
Person.call(this, 'Neil', 20);
this.grade = 98;
}

var stu3 = new Student();
console.log(stu3.name); // Neil
console.log(stu3.age);  // 20
console.log(stu3.grade);    //98
stu3.greet();   //TypeError
console.log(Student.prototype.__proto__)    // {},并未真正继承自Person,所以Student实例中无法调用Person原型中定义的属性和方法


组合继承

有时也称作伪经典继承,结合了原型链继承和借用构造函数两种方式的优点。其原理是使用原型链实现对原型属性和方法的继承,使用借用构造函数实现对实例属性的继承;

//组合继承
function Person (name) {
this.name = name;
this.cans = ['talk', 'eat', 'sleep'];
}

Person.prototype.test = true;
Person.prototype.greet = function () {
console.log('Hello ', this.name);
}

function Student (name, grade) {
// 借用构造函数继承实例属性
Person.call(this, name);
this.grade = grade;
}
// 原型链实现原型属性和方法的继承
Student.prototype = new Person();

Student.prototype.sayGrade = function () {
console.log(this.name + "'s grade is " + this.grade);
}

var stu4 = new Student('D', 89);
var stu5 = new Student('E', 90);
console.log(typeof Object.getOwnPropertyNames(stu4) === 'array');
stu4.cans.push('sing');
console.log('==============实例属性============');
console.log('1: ' + stu4.name);
console.log('2: ' + stu4.cans);
console.log('3: ' + stu5.cans);
console.log('==============原型属性和方法============');
console.log('4: ' + stu4.test);
stu4.greet();
console.log('==============自身属性和方法============');
console.log('6: ' + stu4.grade);
stu4.sayGrade();




原型式继承

原型式继承借助已存在的对象创建新对象,实质是对传入其中的对象实现了一次浅拷贝,然后可以根据需求在新对象上进行修改;

// 原型式继承
var init = {
name: 'E',
interests: ['basketball, music']
}

function object (o) {
function F () {}
F.prototype = o;
return new F();
}

var me = object(init);
me.name = "E+"
me.interests.push('movies');
var me2 = object(init);
console.log(me.name); // E+
console.log(me2.name);  // E
console.log(me.interests);  // [ 'basketball, music', 'movies' ]
console.log(me2.interests); // [ 'basketball, music', 'movies' ]


ES5增加了Object.create()方法可以更加规范地实现原型式继承;它接受两个参数,第一个为创建新对象所依赖的prototype,第二项用于给新对象添加新属性,它的设置与Object.defineProperties()一样采用对象字面量形式;支持该方法的浏览器包括IE 9+、Chrome、Firefox 4+、Safari 5+等;

// ES5新增的Object.create()方法规范了原型式继承
var init = {
name: 'E',
interests: ['basketball, music']
}

var me = Object.create(init, {
name: {
value: 'Neil',
enumerable: true // 默认为false
},
age: {
value: 24,
writable: true  // 默认为false
}
});

console.log(me.name); // Neil
console.log(me.interests);  // [ 'basketball, music' ]
console.log(init.isPrototypeOf(me));  // true


寄生式继承

寄生式继承与原型式继承基本思想类似,具体实现则与工厂模式类似;该方法使用了一个仅用于封装继承过程的函数,在函数内部对新创建的对象进行改造;

显然,该方法与构造函数模式类似,也存在函数不可复用的缺点;

// 寄生式继承
function object (o) {
function F () {}
F.prototype = o;
return new F();
}
var o = {
name: 'F',
interests: ['basketball', 'movies'],
greet: function () {
console.log('Hi!');
}
}

function createObject (superObject) {
var copy = object(superObject);   // 创建新对象
copy.interests.push('spots');  // 以某种方式增强新对象
copy.print = function () {
console.log(this.interests);
}
return copy;  // 返回创建的新对象
}

var obj = createObject(o);
console.log(obj.name);
obj.print();
obj.greet();


寄生组合式继承

寄生组合式继承的目的是为了改善JS最常用的继承方式——组合继承的不足:每次创建子类实例会调用两次父类构造函数;两次调用会导致在子类实例上和子类原型上同时存在两组同名的属性,两次调用分别是: 1、在子类构造函数内部 2、在创建子类原型时(用于原型属性和方法的继承);但是创建子类原型时其实只需要子父类原型的副本即可;这样仅调用一次构造函数,同时避免在子类原型创建不必要的属性,有效提高了效率;同时,该方式是引用类型最理想的继承方式

其本质是使用寄生式继承方式继承父类原型,然后将其赋值给子类原型;

// 寄生组合式继承
function object (o) {
function F () {}
F.prototype = o;
return new F();
}

function inheritPrototype (subType, superType) {
var prototype = object(superType.prototype);  // 以父类原型创建新对象,相当于创建了父类副本而不是调用父类构造函数
prototype.constructor = subType;  // 增强对象
subType.prototype = prototype;  // 指定对象
}

function SuperType (name) {
this.name = name;
this.interests = ['basketball', 'music'];
}

SuperType.prototype.print = function () {
console.log(this.interests);
}

function SubType (name, age) {
SuperType.call(this, name);
this.age = age;
}

inheritPrototype(SubType, SuperType); // 该行需要在添加属性或方法到SubType.prototype之前

SubType.prototype.report = function () {
console.log('age is ' + this.age);
}

var instance = new SubType('G', 25);
var instance2 = new SubType('H', 27);
console.log(instance.name);
console.log(instance.age);
instance.interests.push('chess');
instance.print();
instance.report();
console.log('=============================');
instance2.print();




因为每个实例的原型都只是SuperType.prototype的一个对象副本,各自是独立的,因此instance对引用类型值的改变不会影响到instance2;

inheritPrototype函数需要出现在给子类原型添加属性或者方法之前;因为inheritPrototype方法中重写了SubType.prototype,出现在后边会将提前添加的属性和方法覆盖重写,访问方法会报错TypeError: not a funciton,属性值为undefined;

参考文献:

Object.create():https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create

《JavaScript高级程序设计》第三版
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息