JavaScript 继承
JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。所以我们只要改变对象的原型,就可以做到继承。
先设置一个统一的父类Person
// 定义一个Person类
function Person(name) {
// 属性
this.name = name || 'Person';
// 实例方法
this.sleep = function () {
console.log(this.name + '正在偷懒~~~!');
}
}
// 原型方法
Person.prototype.work = function (job) {
console.log(this.name + '的工作是:' + job);
};
1、原型链继承
将父类的实例作为子类的原型
function Chinese(address) {
this.address = address || "earth";
}
//要在改变原型对象之后
Chinese.prototype.skin = '黄色';
Chinese.prototype = new Person();
// Test Code
let chinese = new Chinese('亚洲中国');
console.log(chinese.skin);//undefined
console.log(chinese.name);//Person
console.log(chinese.address);//亚洲中国
chinese.work('IT');//Person的工作是:IT
chinese.sleep();//Person正在偷懒~~~!
console.log(chinese instanceof Person); //true
console.log(chinese instanceof Chinese); //true
let beijing = new Chinese('北京');
console.log(beijing.name);//Person
优点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例;
- 父类新增原型方法/原型属性,子类都能访问到;
- 简单,易于实现。
缺点:
- 要想为子类新增原型属性和方法,得注意一下要在改变原型对象之后,如上面代码的skin原型属性,(算不上缺点,注意点);
- 无法实现多继承;
- 来自原型对象的所有属性被所有实例共享;
- 创建子类实例时,无法向父类构造函数传参。
2、构造继承
使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Chinese(address,name) {
Person.call(this,name);
this.address = address || "earth";
}
//Test Code
let chinese = new Chinese('亚洲中国','北京');
console.log(chinese.name);//北京
console.log(chinese.address);//亚洲中国
chinese.sleep();//北京正在偷懒~~~!
console.log(chinese instanceof Person); //false
console.log(chinese instanceof Chinese); //true
let shanghai = new Chinese('亚洲中国','上海');
console.log(shanghai.name);//上海
console.log(shanghai.address);//亚洲中国
shanghai.sleep();//上海正在偷懒~~~!
console.log(shanghai instanceof Person); //false
console.log(shanghai instanceof Chinese); //true
chinese.work('IT');//Uncaught TypeError: chinese.work is not a function
优点:
- 解决了1中,子类实例共享父类引用属性的问题;
- 创建子类实例时,可以向父类传递参数;
- 可以实现多继承(call多个父类对象)。
缺点:
- 实例并不是父类的实例,只是子类的实例;
- 只能继承父类的实例属性和方法,不能继承原型属性/方法;(上面代码中原型方法work就会报错);
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。
3、实例继承
为父类实例添加新特性,作为子类实例返回
function Chinese(address,name) {
this.address = address || "earth";
var instance = new Person();
instance.name = name || '北京人';
return instance;
}
// Test Code
var chinese = new Chinese('亚洲中国','上海人');
console.log(chinese.name);//上海人
chinese.sleep();//上海人正在偷懒~~~!
chinese.work('IT');//上海人的工作是:IT
console.log(chinese instanceof Person); // true
console.log(chinese instanceof Chinese); // false
优点:
- 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果。
缺点:
- 实例是父类的实例,不是子类的实例;
- 不支持多继承。
4、拷贝继承
把父对象的属性,全部拷贝给子对象
function deepCopy(p, c) {
var c = c || {};
let mp = new p();
for (var i in mp) {
if (typeof mp[i] === 'object') {
c[i] = (mp[i].constructor === Array) ? [] : {};
deepCopy(mp[i], c[i]);
} else {
c[i] = mp[i];
}
}
return c;
}
function Chinese(address) {
this.address = address || "earth";
}
let chinese= new deepCopy(Person,Chinese);
console.log(chinese.name);//上海人
chinese.sleep();//上海人正在偷懒~~~!
chinese.work('IT');//上海人的工作是:IT
console.log(chinese instanceof Person); // false
console.log(chinese instanceof Chinese); // false
上面写的是深拷贝,递归父类所以的属性和方法。
优点:
- 支持多继承。
缺点:
- 效率较低,内存占用高(因为要拷贝父类的属性);
- 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)。
5、组合继承
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Chinese(address,name) {
Person.call(this,name);//第一次调用父类构造器
this.address = address || "earth";
}
Chinese.prototype = new Person();//第二次调用父类构造器
Chinese.prototype.constructor=Chinese;
let chinese = new Chinese('亚洲中国','上海人');
console.log(chinese.name);//上海人
console.log(chinese.address);//亚洲中国
chinese.sleep();//上海人正在偷懒~~~!
chinese.work('IT');//上海人的工作是:IT
console.log(chinese instanceof Person); // true
console.log(chinese instanceof Chinese); // true
为了防止原型链紊乱,编程时务必要遵守一点:如果替换了prototype对象,那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。
特点:
- 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
- 既是子类的实例,也是父类的实例
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
6、 原型继承(很多文章喜欢叫寄生组合继承)
通过这种方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Chinese(address, name) {
Person.call(this, name);//第一次调用父类构造器
this.address = address || "earth";
}
(function () {
// 创建一个没有实例方法的类
var Super = function () { };
Super.prototype = Person.prototype;
//将实例作为子类的原型
Chinese.prototype = new Super();
})();
Chinese.prototype.constructor = Chinese;
let chinese = new Chinese('亚洲中国', '北京人');
console.log(chinese.name);//北京人
console.log(chinese.address);//亚洲中国
chinese.sleep();//北京人正在偷懒~~~!
chinese.work('IT');//北京人的工作是:IT
console.log(chinese instanceof Person); // true
console.log(chinese instanceof Chinese); // true
这种方式是最好的,但是实现有点复杂,需要寄托一个临时的类,这样在改变子类的prototype对象时,仅仅实例化了一个拥有父类prototype对象的类,并没有实例化父类的方法和属性。
归根到底,就是想办法让子类拥有父类的实例属性和方法,还有父类的原型属性和方法。
总结
JavaScript的原型继承实现的最佳方式就是:
1、定义子类的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this;
2、借助中间函数F实现原型链继承,最好通过封装的函数完成(可以复用);
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
3、继续在子类的构造函数的原型上定义新方法。
7、 Object.create()(终极大招)
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。
let person = {
name: "Person",
sleep: function(value) {
console.log(`My name is ${this.name}. I am ${value}`);
}
};
let child = Object.create(person);
console.log(child.name);//Person
child.name='child';
console.log(child.name);//child
child.sleep('sleeping!');//My name is child. I am sleeping!
上例中,child通过Object.create()创建,原型为对象person。child具有person的所以属性和方法。
7.1 语法
Object.create(proto, [propertiesObject])
参数:
proto
:新创建对象的原型对象。
propertiesObject
:可选。如果没有指定为 undefined
,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()
方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。")的第二个参数。
返回值
一个新对象,带着指定的原型对象和属性。
第六点中的继承进行改造
//将这个方法改造
(function () {
// 创建一个没有实例方法的类
var Super = function () { };
Super.prototype = Person.prototype;
//将实例作为子类的原型
Chinese.prototype = new Super();
})();
//使用Object.create(),实现
Chinese.prototype =Object.create(Person.prototype);
完整代码如下:
// 定义一个Person类
function Person(name) {
// 属性
this.name = name || "Person";
// 实例方法
this.sleep = function() {
console.log(this.name + "正在偷懒~~~!");
};
}
// 原型方法
Person.prototype.work = function(job) {
console.log(this.name + "的工作是:" + job);
};
function Chinese(address, name) {
Person.call(this, name); //第一次调用父类构造器
this.address = address || "earth";
}
Chinese.prototype = Object.create(Person.prototype);
Chinese.prototype.constructor = Chinese;
let chinese = new Chinese("亚洲中国", "北京人");
console.log(chinese.name); //北京人
console.log(chinese.address); //亚洲中国
chinese.sleep(); //北京人正在偷懒~~~!
chinese.work("IT"); //北京人的工作是:IT
console.log(chinese instanceof Person); // true
console.log(chinese instanceof Chinese); // true
是不是比上面都要简单,其实就两步:
第一步使用Object.create,创建一个实例对象(__proto__
指向父类的prototype),然后把子类的prototype对象指向这个对象,这样:子类的prototype指向Object.create创建的实例对象,这个实例对象的__proto__
又指向父类的prototype。
记得要改变子类的constructor的指向,将constructor属性指回原来的构造函数。
第二步在之类中调用父类构造器。
这样就完美,继承了父类的私有属性和方法,也继承了父类的原型链上的方法和属性。
如果你希望能继承到多个对象,则可以使用混入的方式
//子类
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}
// 继承一个父类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它类
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;
MyClass.prototype.myMethod = function() {
// do a thing
};
Object.assign会把 OtherSuperClass
原型上的函数拷贝到 MyClass
原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。
PS:
我们知道JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。其实新的关键字class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单。有兴趣的可以去找些资料看看,和java的class一样的用法。
Class继承
class Earth {
work(job) {
console.log(this.name + '的工作是:' + job);
}
}
class Person extends Earth {
constructor(name) {
super();// 记得用super调用父类的构造方法!
this.name = name;
}
sleep() {
console.log(this.name + '正在偷懒~~~!');
}
}
class Chinese extends Person {
constructor(name, address) {
super(name); // 记得用super调用父类的构造方法!
this.address = address;
}
}
let mChinese = new Chinese('上海');
mChinese.sleep();//上海正在偷懒~~~!
mChinese.work('IT');//上海的工作是:IT
是不是很简单o(∩_∩)o 哈哈!注意一下,不是所有的主流浏览器都支持ES6的class。如果一定要现在就用上,记得babel工具转码。