JavaScript中实现继承的6种方案

01-原型链的继承方案

function Person(){
this.name="czx";
}
function Student(){}
var p1=new Person();
Student.prototype=p1;
var student1=new Student();
console.log(student1); // Person{}
console.log(student1.name); // czx

这是最简单的一种方案,同时也是弊端最多的方案,我们来分析下他的弊端

  1. 如果直接打印Student的实例对象,打印出来是这样

    为啥不是打印出来的Student呢,我们先得了解一个东西,打印出来的这个名称是由constructor决定的

    我们直接将Person的实例对象赋值给Student的prototype,Student原来的prototype就被覆盖了

    而Person的实例对象的隐式原型是指向Person的prototype

    而Person的prototype中是有constructor的,constructor是指向Person本身的

    你们可以这么理解,Person.prototype,那么constructor就是指向Person,Object.prototype中的constructor就是指向Object

    所以,Student1没找到自己的constructor,沿着原型链往上找,找到了Person的constructor,所以就是Person

  2. 通过这种方式继承过来,打印学生的实例对象,继承过来的属性是看不见的

  3. 如果在父类Person上添加引用类型的数据,如果我们创建多个学生对象实例,修改了引用类型的数据,那么父类中的数据也是会被改变的,那么实例的对象,相互之间就不是独立的,可以看下下面这段代码

    function Person(){
    this.friends=[];
    }
    function Student(){}
    var p1=new Person();
    Student.prototype=p1;
    var student1=new Student();
    var student2=new Student();
    student1.friends.push("czx");
    console.log(student2.friends); // ["czx"]
  4. 无法传递参数

    细心的同学可能就会发现,我这样写是无法传递参数的.那以后我们要传递参数的话,就会相当麻烦

    所以,接下来我们看第二种方式


02-借用构造函数实现继承

这个方案的核心就是 call

function Person(name, age) {
this.name = name;
this.age = age;
} // 第一个弊端: Person执行了两次
// 第一次是new出来的,第二次是call执行的
// 第二个弊端是 在new的时候,会往student上加上一些不必要的属性,会把继承的属性加到了Student上面,这是没必要的
Student.prototype = new Person();
Student.prototype.studying = function () {
console.log(this.name + "再学习");
}
function Student(name, age, sno) {
Person.call(this, name, age);
this.sno = sno;
} let stu1 = new Student("czx", 18, 180047101);
stu1.studying();
console.log(stu1);

这个方案解决了第一种方案的(2),(3),(4)弊端

  1. 这两个放的顺序一定不能放错了

    Student.prototype = new Person();
    Student.prototype.studying = function () {
    console.log(this.name + "再学习");
    }

    顺序放错的话 studying那个函数加在了Student原来的原型上,而后又把Person的实例对象覆盖了Student的原型,那么在我们调用的话,就找不到了

  2. 我们来看下这一段

    function Student(name, age, sno) {
    Person.call(this, name, age);
    this.sno = sno;
    }

    因为在我们内直接使用call调用了Person并执行了,那么Person的属性也都可以在Student里面使用,这也能使的我们可以传递属性,可以复用父类中定义的属性.并且在我们打印的时候,也能把属性全部打印出来,大家可以自己试试

  3. 这个方案说实话已经很不错了,解决了大部分问题,如果简单使用也是可以的,但是这种方案就没有弊端吗?

    答案是有的,我们现在来看下它的弊端

    1. 第一个弊端: Person执行了两次
    2. 第一次是new出来的,第二次是call执行的
    3. 第二个弊端是 在new的时候,会往student上加上一些不必要的属性,会把继承的属性加到了Student上面,这是没必要的,我们只需要在call的时候,调用Person,并且把属性放到Student上面去就行了
    4. 第一种方案的第一个弊端也没有解决

我们现在来看下一种方案


03-直接继承父类的原型

function Person(name) {
this.name = name;
}
Person.prototype.running = function () {
console.log(this.name + " is running");
} function Teacher(name, career) {
Person.call(this, name);
this.career = career;
}
function Student(name, age) {
Person.call(this, name);;
this.age = age;
} // 这种方法可不可行呢?
// 答案是不行
// 我们看下面的例子
// 我明明是给学生单独加的属性,但是teacher也可以共用它,这是不合理的
// 我们需要的是学生和老师自己的原型上是属于自己独一无二的属性
// 这样子可复用性才会高
Student.prototype = Person.prototype;
Teacher.prototype = Person.prototype;
Student.prototype.studying = function () {
console.log(this.name + " is learning");
} var s1 = new Student("czx", 18);
var t1 = new Teacher("teacher", "math");
console.log(s1);
s1.running()
s1.studying();
t1.studying();

这种方案是基于第二种方案的一个改版,但是我们看下这种方案可不可行

看完代码,大家也知道不行了,我在代码上也有详细的注释

  1. 我明明是给学生单独加的属性,但是teacher也可以共用它,这是不合理的,这是因为我们加的方法都是直接加在了父类的原型上
  2. 我们需要的是学生和老师自己的原型上是属于自己独一无二的属性
  3. 这样子可复用性才会高

接下来会介绍基于对象来实现继承的两种方案,这两种方案对于最后一种方案的出现有很大的帮助,请大家仔细看看理解


04-原型式继承

这个方案是由道格拉斯·克罗克福德(Douglas Crockford)所提出来的,是对我们前端届贡献特别大的一个人物

这个方案是专门针对对象

我们先看下他当时是怎么实现的

// o是对象
function createObject(o){
function fn(){};
//将对象赋值给fn的原型
fn.prototype=o;
//将实例化的fn赋值给新对象,这样新对象的隐式原型就会指向我们传递进来的obj
var newObj=new fn();
return newObj;
}

大家可以浏览下我上面写的代码,这是道格拉斯当时的想法

现在随着js的逐渐变化,我们写这种方案也变得比较简单了,我们来看下面代码

function createObject(o){
var newObj={};
Obejct.setPrototypeof(newObj,o);
return newObj;
}

Object.setPrototypeof()可以给对象设置一个新的原型,相比之前更加方便了

基于最新的ECMA标准的化,我们还可以这么写

var newObj=Object.create(o);

直接创建一个以o为原型的对象

接下来是针对这一种方案做的一个完善版


05-寄生式继承

var personObj = {
running: function() {
console.log("running")
}
}
function createStudent(name) {
var stu = Object.create(personObj)
stu.name = name
stu.studying = function() {
console.log("studying~")
}
return stu
}
var stuObj = createStudent("why")
var stuObj1 = createStudent("kobe")
var stuObj2 = createStudent("james")

这种方案是基于第四种方案进行改编的

寄生式继承的思路是结合原型类继承和工厂模式的一种方式;

即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;

好,现在结合上述五种方案,我们可以开始介绍最后一种方案啦,只有了解了上述五种方案后,再来看这种方案,会有一种恍然大悟的感觉


06-寄生组合式继承

这种方案就是JS社区这么多年积累出来的最佳方案,我们来看下

//因为继承是一个可能很常用的方法,所以我们把继承的方法提炼出来了,作为一个工具函数
//这样我们只需要传递父类和子类,函数就能自动帮助我们实现继承
function inherit(faObj, chiObj) {
//因为父类和子类的原型都是对象,所以可以用这个方法
//创建一个以父类原型对象为原型的对象,并且将它赋值给子类原型
//这帮助我们解决了第二种方案的父类执行了两次的问题,不需要实例化父类对象
chiObj.prototype = Object.create(faObj.prototype);
//这是为了解决前面方案都没有解决的constructor的方法
Object.defineProperty(chiObj.prototype, "constructor", {
value: chiObj,
enumerable: false,
writable: false,
configurable: false,
})
} function Person(name, age, height) {
this.name = name;
this.age = age;
this.height = height;
}
//下面是我们讲的第二种方案 借用构造函数实现继承
function Student(name, age, height, sno) {
Person.call(this, name, age, height);
this.sno = sno;
}
function Teacher(name, age, height, pro) {
Person.call(this, name, age, height);
this.pro = pro;
} //在这里使用我们自己定义的继承函数
inherit(Person, Student);
inherit(Person, Teacher);
//注意:上下的顺序不能错,我在第二种方案的时候有讲过
Person.prototype.playing = function () {
console.log(this.name + " is playing");
}
Student.prototype.studying = function () {
console.log(this.name + " is studying");
}
Teacher.prototype.teaching = function () {
console.log(this.name + " is teaching");
}
var s1 = new Student("czx", 18, 1.88, 180047101);
var t1 = new Teacher("Mr.cao", 21, 1.88, "computer"); //可以打印出所有的属性 并且打印的是对应的类,而不是父类了
console.log(s1); // Student{}
console.log(t1); // Teacher{}
s1.studying();
t1.teaching();
s1.playing();
t1.playing();

我在代码中写的很详细了,大家一定要好好看看下,理解理解,如果觉得有啥疑惑,可以把之前的几种方案连起来好好看看

如果还有什么疑惑,欢迎大家在评论区留言

Spirit带你彻底搞懂JS的6种继承方案的更多相关文章

  1. 帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)

    作为一名前端工程师,必须搞懂JS中的prototype.__proto__与constructor属性,相信很多初学者对这些属性存在许多困惑,容易把它们混淆,本文旨在帮助大家理清它们之间的关系并彻底搞 ...

  2. 让你彻底搞懂JS中复杂运算符==

    让你彻底搞懂JS中复杂运算符== 大家知道,==是JavaScript中比较复杂的一个运算符.它的运算规则奇怪,容易让人犯错,从而成为JavaScript中“最糟糕的特性”之一. 在仔细阅读了ECMA ...

  3. 彻底搞懂 JS 中 this 机制

    彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...

  4. 一文搞懂 js 中的各种 for 循环的不同之处

    一文搞懂 js 中的各种 for 循环的不同之处 See the Pen for...in vs for...of by xgqfrms (@xgqfrms) on CodePen. for &quo ...

  5. 细说 js 的7种继承方式

    在这之前,先搞清楚下面这个问题: function Father(){} Father.prototype.name = 'father'; Father.prototype.children = [ ...

  6. js的6种继承方式

    重新理解js的6种继承方式 注:本文引用于http://www.cnblogs.com/ayqy/p/4471638.html 重点看第三点 组合继承(最常用) 写在前面 一直不喜欢JS的OOP,在学 ...

  7. 彻底搞懂js __proto__ prototype constructor

    在开始之前,必须要知道的是:对象具有__proto__.constructor(函数也是对象固也具有以上)属性,而函数独有prototype 在博客园看到一张图分析到位很彻底,这里共享: 刚开始看这图 ...

  8. js的三种继承方式及其优缺点

    [转] 第一种,prototype的方式: //父类 function person(){ this.hair = 'black'; this.eye = 'black'; this.skin = ' ...

  9. 重新理解JS的6种继承方式

    写在前面 一直不喜欢JS的OOP,在学习阶段好像也用不到,总觉得JS的OOP不伦不类的,可能是因为先接触了Java,所以对JS的OO部分有些抵触. 偏见归偏见,既然面试官问到了JS的OOP,那么说明这 ...

随机推荐

  1. ORM 之 EF的使用(一)

    早期对数据库进行操作 通过Ado.Net 操作数据库 需要操作sqlCommand/sqlConnection/adapter/datareader 如图 后来 基于面向对象的思想 出现了中间件ORM ...

  2. Spring详解(四)------注解配置DI

    第一步:在 applicationContext.xml 中引入命名空间 这里我们简单讲解一下这里引入的命名空间,简单来说就是用来约束xml文件格式的.第一个 xmlns:context ,这表示标签 ...

  3. Redis缓存雪崩、缓存穿透、热点key

    转载自  https://blog.csdn.net/wang0112233/article/details/79558612 https://www.sohu.com/a/230787856_231 ...

  4. 多线程编程<五>

    1 /** 2 * 中断线程:当线程由于调用sleep(),join(),wait()而暂停时,如果中断它,则会收到一个InterruptedException异常. 3 * 调用Thread.isI ...

  5. Vue状态管理模式---Vuex

    1. Vuex是做什么的? 官方解释: Vuex 是一个专为Vue.js 应用程序开发的 状态管理模式 它采用 集中式存储管理 应用的所有组件的状态, 并以相应的规则保证状态以一种可预测的方式发生变化 ...

  6. MAC下Jetbrains编译器无法打开问题解决

    这段时间不知道怎么回事,每次打开Rider必定闪退,毫无头绪,只好暂时放弃使用Rider,试用了一段时间Visual Studio. 可惜...虽然大学时候觉得VS天下第一,但是用惯了JB的编译器,再 ...

  7. 谈谈redis缓存击穿透和缓存击穿的区别,雪崩效应

    面试经历 在很长的一段时间里,我以为缓存击穿和缓存穿透是一个东西,直到最近去腾讯面试,面试官问我缓存击穿和穿透的区别:我回答它俩是一样的,面试官马上抬起头用他那细长的单眼皮眼睛瞪着我说:"你 ...

  8. 菜鸟入门Linux之路(方法论浅谈)

    Linux是为人熟知的OS之王,已"统治"世界.要想学好绝非易事. 作为菜鸟,可以与Linux亲密接触的方法很多,如视频.书籍.各种企培资料等等,如今的在线教育也如火如荼. 总结说 ...

  9. Linux - last 命令(Mac 电脑)

    前言 为啥写这篇? 因为听 grep.sed 教程的时候有这个命令栗子 加上工作中,运维给我排查问题的时候也用到了,感觉挺重要,先了解为敬! 命令作用 显示用户和TTY的最后登录次数 这个是在 Mac ...

  10. pluto中监听各个网口的500端口处理逻辑

    1. pluto中监听各个网口的500端口处理逻辑 whack_handle() find_ifaces() find_raw_ifaces4() socket.setsockopt.bind.ioc ...