首页 > 技术文章 > 理解 javascript 的继承 - es5

chrissong 2019-03-05 09:18 原文

javascript是一种基于原型链的语言,继承是在平时的工作遇到最多的一个知识点下面讨论下在es5中的几种继承方式.
1.原型继承
原型继承即是根据原型链的规则把父类挂在子类的prototype上面,原型继承有两种形式:继承到实例对象,继承到原型对象
注意:原型继承在改变原型对象的时候,需要把原型里面的constructor重新指向原构造函数,以免破坏原型链规则。
第一种:继承到实例对象
function Person() {
  this.name = "person";
  this.arr = [1];
  this.sayName = function() {
    return this.name;
  };
}
Person.prototype.gender = "man";
 
function Student() {
  this.age = "12";
}
Student.prototype = new Person();
console.log(Student.prototype.constructor); // f Person()
Student.prototype.constructor = Student; //改变原型对象的指向后,会破坏原型链的继承规则,必须手动纠正,要把原型对象的构造函数指向本身的构造函数,
var student = new Student();
var person = new Student();
console.log(student.sayName()); //person
console.log(student.name); //person`
student.name = "student";
console.log(student.name); //student
console.log(person.name); //person
student.arr.push(2); //
console.log(student.arr); //[1,2]
console.log(person.arr); //[1,2]
缺点:
1.因为原型对象是公用一个对象,所以当原型对象里面的值是引用类型时候,改变会发生相互影响。
2.创建子类实例时,无法向父类构造函数传参
 
第二种:继承到原型对象
Student.prototype = Person.prototype;
Student.prototype.constructor = Student;
var student = new Student();
var person = new Person();
console.log(student.gender); //man`
Student.prototype.gender = "felman"; // 因为这里的Student.prototype和Person.prototype是共同的引用,所以改变其中一个互相都有形象
console.log(student.gender); //felman
console.log(person.gender); //felman
 
解决办法:取用一个空的函数作为构造函数过度
var F = function() {};
F.prototype = Person.prototype;
Student.prototype = new F();
Student.prototype.constructor = Student;
var student = new Student();
var person = new Person();
console.log(student.gender); //man`
Student.prototype.gender = "felman"; 
console.log(student.gender); //felman
console.log(student.name); //undefined
console.log(person.gender); //man
 
// 封装一个基于原型的继承函数
function extend(Child, Person) {
  var F = function() {};
  F.prototype = Person.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
  Child.uber = Parent.prototype; // 创建一个继承指针
}
缺点:只继承了父类的原型对象上的属性方法,无法继承其构造函数里面的属性方法。
 
2.构造函数/绑定继承
利用借用父类的构造函数来增强子类的实例,相当于复制一份父类的构造函数到子类
function Person(val) {
  this.name = val;
  this.arr = [1];
  this.sayName = function() {
    return this.name;
};
}
Person.prototype.gender = "man";
function Student(val) {
  Person.call(this, val);
  this.age = "12”; 
}
var student = new Student("student");
var student1 = new Student("student");
console.log(student.name); //student
console.log(student.age); //12
console.log(student.sayName()); //student
console.log(student.gender) //undefined
console.log(student.sayName === student1.sayName); //false
缺点:没创建一个实例都要存一份sayName函数,无法实现函数复用(没有用到原型)
 
3.构造函数+原型=组合继承
前面两种继承,一种无法继承父类的原型对象,一种无法继承父类的构造函数,所以选择两种结合的方式进行互相弥补。
function Person(val) {
  this.name = val;
  this.arr = [1];
}
Person.prototype.gender = "man";
Person.prototype.sayName = function() {
  return this.name;
};
 
function Student(val) {
  Person.call(this, val);
  this.age = "12";
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
var student = new Student("student");
var student1 = new Student("student");
console.log(student.name); //student
console.log(student.age); //12
console.log(student.gender); //man
console.log(student.sayName()); //student
console.log(student.sayName === student1.sayName); //true
利用call实现构造函数的复制,和原型对象对父类实例的继承实现对父类的彻底继承。
缺点:这种方式有两个缺陷,一个是原型继承的缺陷没有避免,二是会重复继承父类的构造函数里面的属性方法。
 
4.寄生组合继承
为了解决上面方法的缺点,并实现对父类的完美继承,我们就去要对其进行改进
function Person(val) {
  this.name = val;
  this.arr = [1];
}
Person.prototype.gender = "man";
Person.prototype.sayName = function() {
  return this.name;
};
function Student(val) {
  Person.call(this, val); // 继承构造函数的属性
  this.age = "12";
}
var F=function(){}
F.prototype=Person.prototype
Student.prototype=new F() // 只继承原型上的属性
Student.prototype.constructor = Student;
var student = new Student("student");
var student1 = new Student("student");
console.log(student.name); //student
console.log(student.age); //12
console.log(student.gender); //man
console.log(student.sayName()); //student
console.log(student.sayName === student1.sayName); //true
利用一个空对象继承父类的原型,然后让子类去继承这个对象实现对父类原型的继承,利用call来实现子类构造函数对父类构造函数的继承。
缺点:有点麻烦,不容易理解。
 
5.拷贝继承
实现继承的原则就是要让子类用到父类的属性和方法,这些方法和属性都存在父类的内存中,复制一份到子类是一种简单粗暴的方式,鉴于浅拷贝会造成共享引用类型的值的改变会产生影响。我们直接用深拷贝来完成继承。
检查类型
function checkType(target) {
  return Object.toString(target).slice(8, -1);
}
 
function deepClone(target) {
  let result;
  let targetType = checkType(target);
  if (targetType === "array") {
    result = [];
  } else if (targetType === "object") {
    result = {};
  } else {
    return target;
  }
  // 遍历
  for (let i in target) {
    let value = target[i];
// 递归的判断条件为引用类型的时候递归
    if (checkType(value) === "Object" || checkType(value) === "Array") {
       result[i] = deepClone(value);
    } else {
       result[i] = value;
    }
  }
  return result;
 }
 
function Person() {
  this.name = "person";
  this.arr = [1];
  this.sayName = function() {
    return this.name;
  };
}
Person.prototype.gender = "man";
var person = new Person();
var student = deepClone(person);
console.log(student.gender); //man
console.log(student.name); //person
缺点:没有用到原型带来的好处,对象较复杂情况用深拷贝带来的性能问题。
 
总结:不同的继承方式有适用的场景,继承的原理基本是基于原型链,改变引用对象(call)或者拷贝,理解继承的原理和优化继承的方式就能更好的理解继承。
 
 
 
 
 
 
 

推荐阅读