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)或者拷贝,理解继承的原理和优化继承的方式就能更好的理解继承。