前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
有位新同事刚刚加入公司,负责开发某个信息系统配套的后台管理系统。要求在后台管理系统中实现不同权限角色的功能。
本着快速交付实现价值,他创建了不同权限角色的对象,例如admin
、editor
和employee
,然后手动复制了每个角色的代码,并对其中的功能进行了修改和扩展。
代码看起来好像没什么问题,虽然结构清晰,但是代码重复率极高,一个功能被连续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
,那就会我明明只需要login
和content
,但是我却还有一个冗余的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中常见的继承方式,并介绍了它们的优缺点。
每种方式都有自己的特点,可以根据具体需求选择合适的方式。
同时,我们也提到了面向对象设计中继承的问题,即无法选择性地继承属性和方法的困扰。
面向组合的设计方式可以有效地解决这个问题,并成为当今编程语法发展的趋势。
如果对于这些继承方式还有疑问,或者希望深入讨论相关话题,请告诉我。我很愿意和你一起学习和进步。😊
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库