JS中的继承
JS继承的概念
通过【某种方式】让一个对象可以访问到另一个对象中的属性和方法,我们把这种方式称之为继承 并不是所谓的 xxx extends yyy
为什么要有继承
先来看一个例子
function Person(name,age) {
this.name = name;
this.age = age;
this.say = function(){
console.log(`我叫${this.name},今年${this.age}岁`)
}
}
let p1 = new Person("张三",18);
let p2 = new Person("李四",18);
// p1对象和p2对象的say方法是否是同一个方法:false
console.log(p1.say === p2.say);
上面方法中,p1、p2调用say方法时,结果显示并不是同一个方法,也就是没有指向同一块内存,会造成内存浪费
解决方案就是把say方法写在Person.prototype中,那么say方法就是同一个方法了,如下面这个例子
// 为Person增加run方法
Person.prototype.run = function(){
console.log("跑步");
}
//此时p1、p2都能访问到run方法
p1.run();
p2.run();
//验证p1.run和p2.run是否是同一个方法? true
console.log(p1.run === p2.run); // 指向同一个方法,这种方式避免了内存的浪费
console.log(p1.run === Person.prototype.run); // true
// 结论:只要往某个构造函数的prototype对象中添加某个属性、方法,那么这样的属性、方法都可以被所有的构造函数的实例所共享
// --> 这里的【构造函数的prototype对象】称之为原型对象
// Person.prototype 是 p1 p2的原型对象
// Person.prototype是Person构造函数的【实例】的原型对象
继承的第一种方式:原型链继承
- 使用方式:
// 还是以Person为例
function Person(name,age) {
this.name = name;
this.age = age;
}
let p1 = new Person("张三",18);
// 为Person增加say方法
Person.prototype.say = function(){}
// 这样写就会存在一个问题,当方法过多时,就会产生冗余代码
Person.prototype.s1 = function(){}
Person.prototype.s2 = function(){}
Person.prototype.s3 = function(){}
Person.prototype.s4 = function(){}
Person.prototype.s5 = function(){}
// 为了减少这种重复,改良版
Person.prototype = {
constructor: Person,
a1:function(){},
a2:function(){},
a3:function(){},
a4:function(){},
a5:function(){}
}
console.log(p1.s1); // 可以访问
console.log(p1.a1); // undefined
// 原因:p1对象在创建的时候已经有了一个确定的原型对象,就是旧的Person.prototype
// 由于Person.prototype后面被重新赋值,但是p1对象的原型对象没有改变,所以p1对象不能访问到新原型对象中的a1-a5方法
let p2 = new Person("李四",18);
console.log(p2.s1); // undefined Person.prototype被重新赋值,无s1方法
console.log(p2.a1); // 可以访问
- 注意点:
- 一般情况下,应该先改变原型对象,再创建对象
- 一般情况下,对于新原型,会添加一个constructor属性,从而不破坏原有的原型对象的结构
继承的第二种方式:拷贝继承(混入继承)
- 场景:有时候想使用某个对象中的属性,但是又不能直接修改它,于是就可以创建一个该对象的拷贝
var o1 = {age: 2};
var o2 = o1;
o2.age = 18;
// 1、修改了o2对象的age属性
// 2、由于o2对象与o1对象是同一个对象
// 3、所以此时o1对象的age属性也被修改了
js实现拷贝继承
// 1、已经拥有了o3对象
var o3 = {gender: "男", grade: "初三", group: "第五组", name: "张三"};
// 2、创建一个o3对象的拷贝(克隆):for...in循环
var o4 = {};
// a、取出o3对象中的每一个属性
for (var key in o3) {
// key就是o3对象中的每一个属性
// b、获取到对应的属性值
var value = o3[key];
// c、把属性值放到o4中
o4[key] = value;
}
// 3、修改克隆对象,把该对象的name属性改为“李四“
o4.name = "李四";
console.log(o4); // 最终的目标对象结果
// 后续如果修改了o4对象中的相关属性,就不会影响到o3
- 由于拷贝继承在实际开发使用场景非常多,所以很多库对此有了实现
- jquery: $.extend
- es6中有了
对象扩展运算符
仿佛就是专门为了拷贝继承而生:
var source = {name: "张三", age: 15};
// 让target是一个新对象,同事拥有了name、age属性
var target = { ...source }
var target2 = { ...source, age:18}
继承的第三种方式:原型式继承
- 场景:
a. 创建一个纯洁的对象:对象什么属性都没有
b. 创建一个继承自某个父对象的子对象
var parent = {age:18, gender:"男"};
var student = Object.create(parent);
student._proto_ === parent
- 使用方式:
- 空对象: Object.create(null)
var o1 = { say: function(){}}
var o2 = Object.create(o1);
继承的第四种方式:借用构造函数实现继承
- 场景: 适用于2种构造函数之间逻辑有相似的情况
- 原理:函数的call、apply调用方式
function Animal(name,age,gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
function Person(name,age,gender,say) {
this.name = name;
this.age = age;
this.gender = gender;
this.say = function() {
console.log("你好");
};
}
- 局限性:Animal(父类构造函数)的代码必须完全适用于Person(子类构造函数)
- 以上代码借用构造函数实现
function Animal(name,age,gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
function Person(name,age,gender,say) {
// 这段代码调用错误
// 为什么错误?
// 因为这种函数的调用方式,函数内部的this只能指向window
// Animal(name,age,gender);
// 目的:将Animal函数内部的this指向Person的实例
// Animal.call(this,name,age,gender);
// -->等价于:
Animal.apply(this,[name,age,gender]);
this.say = say;
}
var p1 = new Person("张三",18,"男",function(){})
继承的第五种方式:寄生继承
- 场景:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象
- 使用方式:
function createAnother(source) {
// 通过调用函数创建一个新对象
// Object.create()方法创建的对象时,属性是在原型下面的,也可以直接访问 anotherPerson.name
var clone = Object.create(source);
// 以某种方式来增强这个对象
clone.say = function() {
console.log("你好")
}
// 返回这个对象
return clone;
}
var Person = {
name: "张三",
age: 18
}
// 不仅有Person的所有属性还有自己的say方法
var anotherPerson = createAnother(Person);
console.log(anotherPerson); // {say: ƒ}
console.log(anotherPerson.__proto__); // {name: "张三",age:18}
console.log(anotherPerson.name); // 张三
继承的第六种方式:组合继承
- 使用方式:
function SuperType(name) {
this.name = name;
this.gender = "男";
}
SuperType.prototype.say = function() {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this,name); //第二次调用SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); // 第一次调用SuperType()
SubType.prototype.run = function(){
console.log(`${this.name}在跑步`);
}
- 组合继承最大的问题就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型时,另一次是在子类型构造函数内部。
继承的第七种方式:寄生组合继承
- 定义:所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法
- 基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
- 使用方式:
// 1、定义父类型
function SuperType(name) {
this.name = name;
this.gender = "男";
}
SuperType.prototype.say = function() {
console.log("name:",this.name);
}
// 2、定义继承方法
function inheritPrototype(subType,superType) {
// 创建对象
var protoType = Object.create(super.prototype);
// 增强对象
protoType.constructor = subType;
// 指定对象
subType.prototype = prototype;
}
// 3、 定义子类实现继承
function SubType(name,age) {
SuperType.call(this,age);
this.age = age;
}
//将子类SubType的原型指向父类SuperType原型的一个副本
//注意:要执行该动作后才能在SubType的prototype上定义方法,否则没用
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function() {
console.log("age:",this.age);
}
// SubType的实例
var instance = new SubType("张三",18);
console.log(instance.name); //张三
console.log(instance.age); //18
instance.sayName(); // name: 张