🤔写了2年前端从来不用面向对象?

简介: 对比错误实现和正确实现的代码示例,展示了面向组合的设计方式如何使代码更加干净、可复用,并提升了维护性和灵活性。

嗨,大家好!这里是道长王jj~

有位新同事刚刚加入公司,负责开发某个信息系统配套的后台管理系统。要求在后台管理系统中实现不同权限角色的功能。

本着快速交付实现价值,他创建了不同权限角色的对象,例如admineditoremployee,然后手动复制了每个角色的代码,并对其中的功能进行了修改和扩展。

代码看起来好像没什么问题,虽然结构清晰,但是代码重复率极高,一个功能被连续copy了多次,但是对于业务来说,代码逻辑调整是经常的事情,这个代码如果上线,对于下一次调整来说简直就是灾难

就像下面这样:

// admin角色
const admin = {
   
  name: 'Admin',
  login: function () {
   
    // 登录逻辑
  },
  // 管理员特有的功能
  manageUsers: function () {
   
    // 用户管理逻辑
  },
  manageContent: function () {
   
    // 内容管理逻辑
  }
};

// editor角色
const editor = {
   
  name: 'Editor',
  login: function () {
   
    // 登录逻辑
  },
  // 编辑员特有的功能
  editContent: function () {
   
    // 内容编辑逻辑
  }
};

// employee角色
const employee = {
   
  name: 'Employee',
  login: function () {
   
    // 登录逻辑
  },
  // 员工特有的功能
  viewContent: function () {
   
    // 内容查看逻辑
  }
};

本着既然被我发现了就不得不拿出来鞭尸的原则。

今天我要和大家探讨一下JavaScript中常见的继承方式,包括回顾ES5的几种继承方法和在ES6中最佳的继承方法。我们将以简洁明了的方式介绍每种方式,并讨论它们的优缺点。如果你对继承概念还不太了解,或者在实际应用中遇到了问题,本文将为你带来一些解决方案。

准备好了吗?让我们开始探索吧!🚀

💡(ES5+)第一种:借助call

我们先来看一种借助call方法实现继承的方式。下面是一个示例代码:

function UserRole(name) {
   
    this.name = name;
}

function Login() {
   
    UserRole.call(this, 'userName01');
    this.type = 'edutior';
}

console.log(new Login());
/**
{
    "name": "userName01",
    "type": "edutior"
}
*/

通过使用call方法,子类能够获取父类的属性值。然而,这种方式存在一个问题,就是无法继承父类原型对象中的方法

💡(ES5+)第二种:借助原型链

接下来,让我们来看一种借助原型链实现继承的方式。以下是示例代码:

function UserRole(name, routers) {
   
    this.name = name;
    this.routes = routers;
}

function Login() {
   
    this.type = 'edutior';
}

Login.prototype = new UserRole('userName01', [1,2,3]);

console.log(new Login());
/**
{
    "type": "edutior"
    [[Prototype]]: {
        "name": "userName01",
        "routes": [1,2,3]
    }
}
*/

使用原型链继承时,子类能够访问父类的方法和属性。然而,这种方式存在一个潜在的问题。

举个例子,明明我只改变了login01的play属性,为什么login02也跟着变了呢?

const login01 = new Login();
const login02 = new Login();
login01.routes.push(4)
console.log(login01.routes, login02.routes);
// [1,2,3,4]  [1,2,3,4]

如果我们修改了一个实例的属性,其他实例也会受到影响,因为它们共享同一个原型对象

💡(ES5+)第三种:将前两种组合

我们可以将前两种继承方式进行组合,解决各自的问题。以下是示例代码:

function UserRole(name, routers) {
   
    this.name = name;
    this.routes = routers;
}

function Login() {
   
    UserRole.call(this, 'userName01', [1,2,3]);
    this.type = 'edutior';
}

Login.prototype = new UserRole('userName01', [1,2,3]);

const login01 = new Login();
const login02 = new Login();
login01.routes.push(4)
console.log(login01.routes, login02.routes);
// [1,2,3,4]  [1,2,3]

通过这种组合方式,我们解决了之前的问题。然而,这种方式会导致父类的构造函数被执行两次,强迫症非常难受。

💡(ES5+)第四种:组合继承的优化

为了优化组合继承的方式,我们可以进行一些改进。以下是示例代码:

function UserRole(name, routers) {
   
    this.name = name;
    this.routes = routers;
}

function Login() {
   
    UserRole.call(this, 'userName01', [1,2,3]);
    this.type = 'edutior';
}

Login.prototype = UserRole.prototype;

const login01 = new Login();
console.log(login01)
/**
{
    "name": "userName01",
    "routes": [1,2,3],
    "type": "edutior"
    [[Prototype]]: {
        constructor: UserRole(name, routers)
    }
}
*/

通过将子类的原型直接指向父类的原型,并修复子类构造函数的引用,我们避免了父类构造函数被执行两次的问题。

但是这个时候又出现了一个新的问题:

子类实例的构造函数是UserRole,显然这是不对的,应该是Login!

💡(ES5+推荐)第五种:寄生组合继承

最后,我要介绍的是一种最推荐的继承方式:寄生组合继承。以下是示例代码:

function UserRole(name, routers) {
   
    this.name = name;
    this.routes = routers;
}

function Login() {
   
    UserRole.call(this, 'userName01', [1,2,3]);
    this.type = 'edutior';
}

Login.prototype = Object.create(UserRole.prototype);
Login.prototype.constructor = Login;

const login01 = new Login();
console.log(login01)
/**
{
    "name": "userName01",
    "routes": [1,2,3],
    "type": "edutior"
    [[Prototype]]: {
        constructor: Login()
    }
}
*/

寄生组合继承是一种接近完美的继承方式。它解决了属性共享和方法继承的问题,并且避免了父类构造函数被执行多次的情况。

✨“寄生组合继承”在ES6的编译过程中是怎么运用的

如果我们使用ES6的extends关键字进行继承,并编译成ES5的代码,会得到类似以下的JavaScript代码:

function _possibleConstructorReturn(self, call) {
   
    // ...
    return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}

function _inherits(subClass, superClass) {
   
    // ...
    // 注意这里
    subClass.prototype = Object.create(superClass && superClass.prototype, {
   
        constructor: {
   
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

var Parent = function Parent() {
   
    // 验证是否是 Parent 构造出来的 this
    _classCallCheck(this, Parent);
};

var Child = function (_Parent) {
   
    _inherits(Child, _Parent);

    function Child() {
   
        _classCallCheck(this, Child);

        return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
    }

    return Child;
}(Parent);

你可以看到,在编译后的JavaScript代码中,实际上也是采用了寄生组合继承的方式来实现继承。

同时,它还使用了Object.setPrototypeOf来继承父类的静态方法,又进一步改进了第五种方法所忽略的地方。

❓只要使用了“继承”,就是好的面向对象设计吗?

先说结论:不一定!

假设现在后台管理系统中有N个不同的角色,每个角色都有login,level,content三个方法。

class Role{
   
  constructor(name) {
   
    this.name = name;
  }
  login(){
   
    console.log("login!");
  }
  level(){
   
    console.log("level!")
  }
  content(){
   
    console.log("content!")
  }
}
class otherRole extends Role{
   }

通过继承,我们可以实现角色的基本功能,并在此基础上扩展不同角色的特定功能。看起来似乎一切都很完美,但是现在出现了一个问题:**“游客”**角色,它并没有level(会员等级)。

如果让**“游客”**的类继承自Role,那就会我明明只需要logincontent,但是我却还有一个冗余的level。我不需要level这个方法,但由于继承的原因,它也被传递给了子类。

继承的最大问题在于:无法决定继承哪些属性,所有属性都必须继承。

当然,我们可以再创建一个父类,将level方法去掉。

为了满足不同子类的特性,我们需要创建不同的父类,这导致大量重复的代码。

而且一旦子类发生变动,父类也需要相应更新,这会导致代码的耦合性过高,维护性也变得困难

📌最好的方法( 函数式编程):用“组合”实现面向对象

“组合”是当今编程语法发展的趋势。Go就完全采用的是基于组合的设计方式。

// 函数组合
const compose = (...fns) => (arg) => fns.reduceRight((acc, fn) => fn(acc), arg);

// 函数定义
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;

// 组合后的函数
const composedFn = compose(
  square,
  double,
  addOne
);

// 使用组合函数
const result = composedFn(3);
console.log(result); // 输出 64

代码整洁干净,可以随处复用。面向组合的设计趋势已然到来。

结语

本文简要回顾了JavaScript中常见的继承方式,并介绍了它们的优缺点。

每种方式都有自己的特点,可以根据具体需求选择合适的方式。

同时,我们也提到了面向对象设计中继承的问题,即无法选择性地继承属性和方法的困扰。

面向组合的设计方式可以有效地解决这个问题,并成为当今编程语法发展的趋势。

如果对于这些继承方式还有疑问,或者希望深入讨论相关话题,请告诉我。我很愿意和你一起学习和进步。😊

目录
相关文章
|
3月前
|
前端开发 JavaScript Go
写了2年前端从来不用面向对象?
写了2年前端从来不用面向对象?
|
前端开发 JavaScript Java
重学前端 7 # JavaScript对象:面向对象还是基于对象?
重学前端 7 # JavaScript对象:面向对象还是基于对象?
76 0
|
前端开发 JavaScript
|
前端开发 JavaScript Java
从前端到全栈 -- 最全面向对象总结——《我的Java打怪日记》
从事前端的这两年中,也接触了后端领域,像 `PHP` `Node`, 并且都用它们做过一些 `demo` 和 `私活` ,站在职业发展道路上来讲,了解后端是好的,自己可以熟练前后的开发流程,整条链路下来,很清晰,懂了后端,在自己以后创业或者接私活,都是不错的。 而且现在在`Web`后台领域, `Java` 是 老大哥了,`Spring` 全家桶走遍天下,于是最近又重拾 `Java` 了,从前端迈向全栈
1464 0
从前端到全栈 --  最全面向对象总结——《我的Java打怪日记》
|
前端开发 JavaScript Java
前端学习笔记(2) - JavaScript面向对象
javaScript是不是面向对象语言? JavaScript(es6之前)有对象的概念,却没有类的概念,JavaScript对象可以任意添加属性,而java、c++等其他语言却不能。以至于有些人认为JavaScript并非是面向对象的语言,而是基于对象的语言。
979 0
|
前端开发
前端开发之面向对象
【一】 面向对象的基本概念   面向对象的英文全称叫做Object Oriented,简称OO。OO其实包括OOA(Object Oriented;Analysis,面向对象分析)、OOD(Object Oriented Design,面向对象设计)和OOP(Object;Oriented Programming,面向对象的程序设计)。
1080 0
|
JavaScript 前端开发
前端开发:面向对象与javascript中的面向对象实现(二)构造函数与原型
前端开发:面向对象与javascript中的面向对象实现(二)构造函数与原型 前言(题外话):   有人说拖延症是一个绝症,哎呀治不好了。先不说这是一个每个人都多多少少会有的,也不管它究竟对生活有多么大的影响,单单是自己的念想受到了一定得局限,想法不能够像平地而起的高楼大厦建成一样。
984 0
|
JavaScript 前端开发
前端开发:面向对象与javascript中的面向对象实现(一)
前端开发:面向对象与javascript中的面向对象实现(一) 前言:       人生在世,这找不到对象是万万不行的。咱们生活中,找不到对象要挨骂,代码里也一样。朋友问我说:“嘿,在干嘛呢......”,我:“找不到对象!”,他:“就你那样也能找得到对象?”。
884 0
|
8月前
|
Web App开发 前端开发 JavaScript
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-fiber解决了什么问题
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-fiber解决了什么问题
95 0
|
8月前
|
前端开发 定位技术
前端学习笔记202305学习笔记第二十三天-地图单线程配置
前端学习笔记202305学习笔记第二十三天-地图单线程配置
64 0
前端学习笔记202305学习笔记第二十三天-地图单线程配置