前言
最近在读《你不知道的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-properties
height = 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的实现原理:
- 声明构造函数 Animal, 保留 contructor 传参和内部逻辑,所有this下的属性和方法都是各个实例自己的,不会共享。
- 处理不同类型的属性和方法:
• static 方法属于构造函数本身,不属于实例;
• 普通方法会挂载到原型链上, 被所有实例公用
• 公共属性用单独定义的 _defineProperty 方法处理,保证所有实例共享 - 返回构造函数 Animal
- 用 立即执行函数 function(){}() 包裹上面的逻辑,构建自己的作用域,防止变量名冲突
JS中的继承
es5继承
关于JS的继承,有以下几种方式
- 原型链继承
- 构造继承
- 实例继承
- 拷贝继承
- 组合继承
- 寄生组合继承
这些继承方式有各自的优缺点,具体的评判不再赘述,这篇博客写的很详细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;
}
可以看到这个方法接受 子类和父类两个参数,做了两件事:
- 将父类实例作为子类的原型
- 修正原型、原型链、构造函数之间的指向
最后的指向结果是:
除此之外,当子类没有自己的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 6的class关键字也只是基于JavaScript原型继承的语法糖,并没有引入新的对象继承模式。
基于以上描述,关于Js中到底有没有" 类 "的概念 这个问题的答案,仁者见仁,智者见智。
笔者倾向于认同 Js中没有“类” 的答案。如果你认为有,你说的对<(^-^)>
《你不知道的Javascript》是一套挺好的进阶JS书籍,推荐阅读。有机会以后多写一点相关的内容。
参考文献
[1]Kyle Simpson.你不知道的JavaScript[M].人民邮电出版社
[2] 面向对象的 JavaScript:封装、继承与多态
[3] JS实现继承的几种方式