浅谈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); // 可以访问
  • 注意点:
  1. 一般情况下,应该先改变原型对象,再创建对象
  2. 一般情况下,对于新原型,会添加一个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: 张
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值