Javascript中类的实现

2023-09-25 12 0

前言

最近在读《你不知道的Javascript》 系列,读到关于原型和类的部分,里面提到一个观点:js中只有对象,没有类这个概念。
在网上搜索关于“Javascript” 中是否有类 的问题,有很多争议。
一段比较准确的描述是:

在ECMAScript 6出现class的概念之后,才算是告别了直接通过原型对象来模拟类和类继承,但class也只是基于JavaScript原型继承的语法糖,并没有引入新的对象继承模式。

这篇文章并不是为 Js是否有"类" 这个问题盖棺定论,而是看Js具体是如何通过原型实现传统 面向对象语言class关键字的功能的。

阅读本篇前,你需要提前了解什么是真正的类和面向对象编程思想,以及js的原型链设计。

我们开始吧。

Js是如何实现类的

es 5: 构造函数法

用构造函数模拟“类”,在其内部用this关键字指代实例对象,用 new 关键字生成实例。

function Cat(){this.name = "大毛"//作为构造函数被调用时,this指向实例,这里定义了实例的属性
}// 定义在 构造函数 原型链上的属性和方法,被所有实例共享,是同一个值(引用)
Cat.prototype.makeSound = function(){alert("喵喵喵");
} 

es 6: class关键字

class Animal {constructor(name,age){this.name = name;this.age = age;this.move= function(){};}static sleep(){console.log('sleeping')}speakSomething(){console.log(123)}//公共属性,转译配置需要插件 plugin-proposal-calss-propertiesheight = 0;
}

经过babel转译后看到的代码如下(babel preset: es2015-loose):

"use strict";function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }var Animal = /*#__PURE__*/function () {function Animal(name, age) {_defineProperty(this, "height", 0);this.name = name;this.age = age;this.move = function () {};}// static 方法只会属于构造函数自身,不属于实例对象Animal.sleep = function sleep() {console.log('sleeping');};//原型对象var _proto = Animal.prototype;// 普通方法会属于构造函数的原型对象,所有的实例都可以通过原型链找到_proto.speakSomething = function speakSomething() {console.log(123);};return Animal;
}();// 立即执行函数

由此,我们可以知道class的实现原理:

  1. 声明构造函数 Animal, 保留 contructor 传参和内部逻辑,所有this下的属性和方法都是各个实例自己的,不会共享。
  2. 处理不同类型的属性和方法:
    • static 方法属于构造函数本身,不属于实例;
    • 普通方法会挂载到原型链上, 被所有实例公用
    • 公共属性用单独定义的 _defineProperty 方法处理,保证所有实例共享
  3. 返回构造函数 Animal
  4. 用 立即执行函数 function(){}() 包裹上面的逻辑,构建自己的作用域,防止变量名冲突

JS中的继承

es5继承

关于JS的继承,有以下几种方式

  1. 原型链继承
  2. 构造继承
  3. 实例继承
  4. 拷贝继承
  5. 组合继承
  6. 寄生组合继承
    这些继承方式有各自的优缺点,具体的评判不再赘述,这篇博客写的很详细JS实现继承的几种方式

es6继承

es6引入了class关键字,相应的使用 extends实现继承,这似乎是显而易见的事。但注意我们在开头所说的,

class也只是基于JavaScript原型继承的语法糖,并没有引入新的对象继承模式

extends实现继承的原理实际上也应建立在es5继承的几种方式之上。
因为es6实现类的本质还是利用构造函数,因此以下父类、子类的概念可以理解为相应的父类的构造函数和子类的构造函数

class Animal {constructor(name,age){this.name = name;this.age = age;}speakSomething(){console.log(123)}  
}class Dog extends Animal(){}class Cat extends Animal(){constructor(){}
}class Bird extends Animal(){constructor(props){this.move = "fly"}
}

将以上代码用babel进行转译之后:

"use strict";//用来处理公共属性的方法,不知道为什么这么实现,而不是像function一样,直接挂在_proto 上
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }var Animal = /*#__PURE__*/function () {// 构造函数,保留内部逻辑function Animal(name, age) { //处理公共属性_defineProperty(this, "height", {});this.name = name;this.age = age;this.move= function(){};}// static 方法只会属于构造函数自身,不属于实例对象Animal.sleep = function sleep() {console.log('sleeping');};//原型对象var _proto = Animal.prototype;// 普通方法会属于构造函数的原型对象,所有的实例都可以通过原型链找到_proto.speakSomething = function speakSomething() {console.log(123);};return Animal;
}();  // 立即执行函数

可以看到,所有的继承子类(实质是一个构造函数)都调用了一个 _inheritsLoose 方法,看下这个代码做了什么:

function _inheritsLoose(subClass, superClass) { // Object.create(proto, [propertiesObject]) 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。// 创建一个新的父类的实例 obj.__proto__ = superClass.prototype,可以看成 new superClass() 的等效结果// 将子类(构造函数)原型指向这个新创建出来的父类实例subClass.prototype = Object.create(superClass.prototype); // 将子类原型的构造函数指向子类自身;subClass.prototype.constructor = subClass; // 子类构造函数的原型链指向父类构造函数subClass.__proto__ = superClass; 
}

可以看到这个方法接受 子类和父类两个参数,做了两件事:

  1. 将父类实例作为子类的原型
  2. 修正原型、原型链、构造函数之间的指向
    最后的指向结果是:
    在这里插入图片描述
    除此之外,当子类没有自己的constructor时,会默认调用父类的构造函数,并传入参数,类似上面的构造函数继承;
    当子类有自己的constructor时,会覆盖上面默认的constructor。在最后调用一个 _assertThisInitialized方法:
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self;
}

这个方法判断子类的this是否已经初始化,可以忽略。

super(props)

在 react 中写组件时常常在构造函数中 用到 super(props), 这个是在做什么呢?看一下

class Cat extends Animal(){constructor(props){super(props)}
}

babel转移后:

var Cat = /*#__PURE__*/function (_Animal) {_inheritsLoose(Cat, _Animal);function Cat(props) {return _Animal.call(this, props) || this;}return Cat;
}(Animal());

实际上也是调用了父类的构造函数,传入props。 super表示父类的构造函数。
结合上面的分析,可以得出结论:

  • 当子类没有自己的constructor时,extends 采用 组合继承(原型链继承 + 构造函数继承 ) 的方式实现继承。
  • 当子类重写自己的constructor时,extends 采用 原型链继承 的方式实现继承。当constructor 内 调用 super(props) 时,即手动调用了 父类的构造函数,实现了构造函数继承。

总结

Js通过原型和原型链模拟类的行为,es 6class关键字也只是基于JavaScript原型继承的语法糖,并没有引入新的对象继承模式。

基于以上描述,关于Js中到底有没有" 类 "的概念 这个问题的答案,仁者见仁,智者见智。

笔者倾向于认同 Js中没有“类” 的答案。如果你认为有,你说的对<(^-^)>

《你不知道的Javascript》是一套挺好的进阶JS书籍,推荐阅读。有机会以后多写一点相关的内容。

参考文献

[1]Kyle Simpson.你不知道的JavaScript[M].人民邮电出版社
[2] 面向对象的 JavaScript:封装、继承与多态
[3] JS实现继承的几种方式

代码编程
赞赏

相关文章

【C】浅析 #define 宏和函数的区别
【C】浅析 关键字
【C】库函数之 sqrt
【C】折半(二分)查找
fio_generate_plots
【Linux】进程的调度算法