ES6博采众长终于原生实现了类(class)这个概念,当然他的继承也变得相对来说简单了,也是extends关键字即可如:
class Person{
constructor(name,gender){
this.name = name;
this.gender = gender;
}
changeName = (name)=>{
this.name=name;
}
changeGender = (gender)=>{
this.gender = gender;
}
}
class Student extends Person{
constructor(name,gender,age){
super(name,gender);
this.age = age;
}
changeAge = (age)=>{
this.age = age;
}
}
在ES5的时代我们没有原生实现的类,于是我们只能模拟类和继承来实现相同效果。并且有助于我们理解js原理。
类的实现:
function Person(name,gender){
this.name = name;
this.gender = gender;
}
Person.prototype.changeName=function(name){
this.name = name;
console.log(this.name);
}
Person.prototype.changeGender=function(gender){
this.gender = gender;
console.log(this.gender);
}
ES5实现类的继承,这里介绍主要的三种继承方式:寄生组合继承(原型继承)、浅拷贝继承、深拷贝继承。
首先声明一个Student类的构造函数再函数中通过new方法绑定this,实现构造函数的继承,但是并没有继承Person原型链上的方法。
function Student(name,gender,age){
Person.call(this,name,gender);
this.age = age;
}
如果直接使用 Student.prototype = Person.prototype会导致直接赋值,是传址,一个改变会导致另一个改变。
如果使用Student.prototype = new Person()会将构造函数中的属性(如this.name,this.gender)也一并拷贝到原型链上。
1.组合继承
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
Student.prototype = new Person();
Student.prototype.constructor = Student;
2.寄生组合继承(推荐)
①原理重新声明一个空函数,没有任何的属性,用作连接原型对象使用
②使用Object.create达到①相同效果
function extend(faClass){
function Proxy(){};
proxy.prototype = faClass.prototype;
return new Proxy();
}
Student.prototype = extend(Person);
//构造函数
Student.prototype.constructor = Student;
let s = new Student('a','nv',18);
console.log(s);
s.changeName('b')
使用Object.create()方法:
//我们创建一个新的Student.prototype对象,并关联到Student.prototype上。
Student.prototype = Object.create(Person.prototype);
//构造函数
Student.prototype.constructor = Student;
let s = new Student('a','nv',18);
console.log(s);
s.changeName('c')
上面的继承中没有了Student.prototype.constructor属性,所以需要我们手动修复一下他:Student.prototype.constructor = Student。
上述代码的核心部分就是语句:Student.prototype = Object.create(Person.prototype)。调用Object.create(...)会凭空创建一个“新”的对象并把新对象内部的[[Prototype]]关联到你指定的对象(本例中是Student.prototype)。
换句话说,这条语句的意思是:“创建一个新的Student.prototype对象并把它关联到Person.prototype”。
声明function Student(){...}时,和其他函数一样,Student会有一个.prototye关联到默认的对象,但是这个对象并不是我们想要的Person.prototype。因此我们创建了一个新对象并把它关联到我们希望的对象上,直接把原始的关联对象抛弃掉。
注意,下面这两种方式时常见的错误做法,实际上他们都存在一些问题:
//和你想要的机制不一样
Student.prototype = Person.prototype;
//基本上满足你的需求,但是可能会产生一些副作用
Student.prototype = new Person();
Student.prototype = Person.prototype并不会创建一个关联到Student.prototype的新对象,他只是让Student.prototype直接引用Person.prototype对象。因此当你执行类似Student.prototype.changeName=...的赋值语句时会直接修改Student.prototype对象本身。显然这不是你想要的结果,否则你根本不需要Student对象,直接使用Person就可以了,这样代码也会更简单一些。
Student.prototype = new Person()的确会创建一个关联到Student.prototype的新对象。但是它使用了Person(...)的“构造函数调用”,如果函数Person有一些副作用(比如写日志、修改状态、注册到其他对象、给this添加数据属性,等等)的话,就会影响到Student()的“后代”,后果不堪设想。
因此要创建一个合适的关联对象,我们必须使用Object.create(...)而不是使用狙有副作用的Person(...)。这样做的唯一缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。
如果能有一个标准并且可靠的方法来修改对象的[[prototype]]关联就好了。再ES6之前,我们只能通过设置__proto__属性来实现,但是这个方法并不是标准并且无法兼容所有浏览器。ES6添加了辅助函数Object.setPrototypeOf(..),可以用标准并且可靠的方法来修改关联。
我们来对比一下两种把Student.prototype关联到Person.prototype的方法:
//再ES6之前需要抛弃默认的Student.prototype
Student.prototype = Object.create(Person.prototype);
//ES6开始可以直接修改现有的Student.prototype
Object.setPrototypeOf(Student.prototype,Person.prototype);
如果忽略掉Object.create(..)方法带来的轻微性能损失(抛弃的对象需要进行垃圾回收机制回收),它实际上比ES6及其之后的方法更短而且可读性更高。不过无论如何,这是两种完全不同的语法。
3.浅拷贝方式
即将需要继承的类(函数)上的原型上的方法拷贝到当前类上的原型即可,当然浅拷贝对于嵌套的对象就是传址了,对于简单的继承可以使用。将父类的属性和方法拷贝一份到子类中
function Student(name,gender,age){
Person.call(this,name,gender);
this.age = age;
}
for(let key in Person.prototype){
Student.prototype[key] = Person.prototype[key];
}
let s = new Student('a','nv',18);
console.log(s);
s.changeName('c')
4.深拷贝方式
function Person(name,gender){
this.name = name;
this.gender = gender;
}
Person.prototype.changeName=function(name){
this.name = name;
console.log(this.name);
}
Person.prototype.changeGender=function(gender){
this.gender = gender;
console.log(this.gender);
}
Person.prototype.children=[
{
name:'a'
},{
name:'b'
}
];
function Student(name,gender,age){
Person.call(this,name,gender);
this.age = age;
}
function deepCody(resource){
let target = Array.isArray(resource) ? [] : {};
for(let key in resource){
if(resource.hasOwnProperty(key)){
if(typeof resource[key] === 'object'){
target[key] = deepCody(resource[key])
}else{
target[key] = resource[key];
}
}
}
return target;
}
Student.prototype = deepCody(Person.prototype);
//这里改变了Student原型上的嵌套对象的值,对比发现没有改变Person相应的值,实现了拷贝
Student.prototype.children[0].name = 'c';
console.log(new Person);
let s = new Student('a','nv',18);
console.log(s);
s.changeName('c')
5.实例继承
为父类实例添加新特性,作为子类实例返回
function Student(){
let instance = new Person('a','b');
instance.age = 18;
return instance;
}
6.构造继承
实质是利用 call 来改变 Cat 中的 this 指向,可以实现多继承,不能继承原型属性/方法。一般和组合继承和寄生组合继承结合使用。
function Student(name,gender,age){
Person.call(this,name,gender);
this.age = age;
}