努力让学习成为一种习惯,自信来源于充分的准备
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
前言
在js实现继承的多种方式中,我们介绍了es5中的多种继承方式。主要是借助原型链与构造函数,但是写法感官上与传统的面向对象语言差异很大并且过于繁琐
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类,但需要注意的是class只是一个语法糖,底层原理实现与es5基本一致但也有所不同
那么class背后到底做了什么呢,让我们借助babel来揭开class的黑魔法
阅读完本篇文章你将get以下技能
es6中class实现继承的原理是什么es6与es5实现继承的区别有哪些
es5与es6写法对比
在揭开面纱之前,我们先看看es5与es6的各功能对比,让大家对class有一个感性的认识
实例生成
// es5写法
function Person () {
this.name = 'kk'
}
const p = new Person()
// es6写法
class Person {
name = 'kk'
constructor() {
// this.name = 'kk' 两者写法一样
}
}
const p = new Person()
es5与es6均是使用new方式调用函数生成实例,不过两者有所区别
es5函数本身直接作为构造函数,es6将constructor方法作为构造函数,这里有一个比较容易理解错的点是认为es6是调用内部constructor方法,但这是错的!!,(实际babel编译的时候也并不会将constructor作为prototype的一个属性,而是直接作为入口构造函数),这么说可能有些拗口,我们借助代码理解下
// es6
// 虽然下面是true
console.log('constructor' in Person.prototype) // true
// 但是
new Person()
//! 不等同于!!!!
new Person.prototype.constructor()
// 手动修改constructor
Person.prototype.constructor = 1
new Person() // 依旧可以生成
new Person.prototype.constructor() // Uncaught TypeError: Person.prototype.constructor is not a constructor
这里其实和es5行为表现一致,constructor并不是一个安全靠谱的属性
es5的构造函数可以作为普通函数调用,而es6必须使用new调用
Person() // Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
Person.prototype.constructor()// Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
原型方法
// es5写法
function Person () {
this.name = 'kk'
}
Person.prototype.getName = function () { return this.name }
const p = new Person()
// es6写法
class Person {
name = 'kk'
constructor() {}
getName() { return this.name }
}
const p = new Person()
这里需要需要注意两点:
- 类内部包括
constrcutor在内的所有方法均在其原型对象上
console.log(Object.hasOwn(Person.prototype, 'name')); // false
console.log(Object.hasOwn(Person.prototype, 'constructor')); // true
console.log(Object.hasOwn(Person.prototype, 'getName')); // true
- 类内部定义的方法均不可枚举,这与
es5不一样
console.log(Object.keys(Person.prototype)) // []
console.log(Object.getOwnPropertyNames(Person.prototype)) // ['constructor', 'getName']
静态属性、方法
这里引用阮一峰大佬es6教程的说明:
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”,属性也是一样
class Person {
static age = '12'
static getAge = function () {
return this.age
}
}
const p = new Person()
console.log(p.age); // undefined
console.log(p.getAge); // undefined
console.log(Person.age); // '12'
console.log(Person.getAge()); // '12'
其对应es5的代码为
function Person() {}
Person.age = '12'
Person.getAge = function () { return this.age }
const p = new Person()
console.log(p.age); // undefined
console.log(p.getAge); // undefined
console.log(Person.age); // '12'
console.log(Person.getAge()); '12'
这里需要注意的是:对于class,静态方法内部的this指向类本身。对于es5构造函数,则指向函数本身。区别在于class子类可以继承父类的静态属性和方法
getter与setter
class Person {
get name() {
return 'kk'
}
set name(_name) {
console.log(_name);
}
}
const p = new Person()
console.log(p.name); // kk
p.name = 'oo' // oo
console.log(Object.hasOwn(Person.prototype, 'name')) // true
这里es5表现与其一致
function Person() {}
Person.prototype = {
constructor: Person,
get name() {
return 'kk'
},
set name(_name) {
console.log(_name);
}
}
babel是如何编译的
在前面我们列举了class的特点,并且都可以用es5的方式实现。只是有些细节会不一样
但是babel编译的时候并不会直接转化成上面这样,它会添加许多辅助函数并且以更加严谨、完善的方式帮助我们实现es6特性,接下来让我们一步一步看babel是如何实现class的
ps: 为了方便理解,下面babel转化后的代码只会放与核心实现最相关的代码,并适当做一些简化
编译构造函数与实例属性
es6代码
class Person {
constructor(name) {
this.name = name
}
}
const p = new Person()
babel编译
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Person = function Person(name) {
_classCallCheck(this, Person)
this.name = name
}
var p = new Person()
这里我们可以看到,babel直接将class内部的constructor方法用于构造函数。其中_classCallCheck函数检测this是不是其构造函数的实例,如果不是则抛出错误。因为class只能通过new方式调用
注意,如果这里采用在构造函数外面定义实例属性的方式,babel编辑的结果会稍微有些不同
es6代码
class Person {
name = 'kk'
constructor() {}
}
const p = new Person()
babel编译
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
}
function _classCallCheck(instance, Constructor) {...}
var Person = function Person(name) {
_classCallCheck(this, Person)
_defineProperty(this, 'name', 'kk')
}
var p = new Person()
两者其实本质差不多
编译原型方法,静态属性、方法、getter/setter
es6代码:
class Person {
static age = 1
static getAge = function () {
return this.age
}
constructor(name) {
this.name = name
}
getName() {
return this.name
}
get habbit() {
return 'basketball'
}
set habbit(_habbit) {
console.log(_habbit)
}
}
const p = new Person()
babel编译:
function _classCallCheck(instance, Constructor) {...}
function _defineProperty(obj, key, value) {...}
var Person = function () {
function Person(name) {
_classCallCheck(this, Person);
this.name = name;
}
return _createClass(Person, [{
key: "getName",
value: function getName() {
return this.name;
}
}, {
key: "habbit",
get: function get() {
return 'basketball';
},
set: function set(_habbit) {
console.log(_habbit);
}
}]);
}();
_defineProperty(Person, "age", 1);
_defineProperty(Person, "getAge", function () {
return this.age;
});
var p = new Person();
这里我们可以看到对于静态属性、方法,babel利用_defineProperty函数直接将其定义在函数本身上面, 另外新增了一个_createClass函数,第一个参数是构造函数,第二个参数是需要定义在原型上的属性、方法,我们来看看_createClass函数内部
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ('value' in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps)
if (staticProps) _defineProperties(Constructor, staticProps)
Object.defineProperty(Constructor, 'prototype', { writable: false })
return Constructor
}
可以看出,babel将class内部的getter、setter、方法统一放到protoProps里,并依次设置到构造函数的原型对象上。静态属性和方法统一放到staticProps里,并依次设置到了构造函数上(上面我们发现,针对静态属性、方法。babel直接放到了最外面处理而没有传入_createClass函数中,但其实两者本质都是一样的),最后返回构造函数
这里其实有点像寄生式继承:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象,但这里只是增强,不涉及原型链的操作,对于babel有专门处理继承、原型相关操作的函数,这个下面会讲到
_defineProperties方法很好理解。运用Object.defineProperty设置属性,class内部定义的方法默认都是不可枚举的。如果有value,设置writable为true,否则就使用getter和setter
继承
es6
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
var child1 = new Child('kk', '18');
babel编译
function _createClass(Constructor, protoProps, staticProps) {...}
function _classCallCheck(instance, Constructor) {...}
var Parent = _createClass(function Parent(name) {
_classCallCheck(this, Parent)
this.name = name
})
var Child = (function (_Parent) {
function Child(name, age) {
var _this
_classCallCheck(this, Child)
_this = _callSuper(this, Child, [name])
_this.age = age
return _this
}
_inherits(Child, _Parent)
return _createClass(Child)
})(Parent)
var child1 = new Child('kk', '18')
这里有两个新增的函数:_inherits、_callSuper,我们来具体看下
_inherits
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf
? Object.setPrototypeOf.bind()
: function _setPrototypeOf(o, p) {
o.__proto__ = p
return o
}
return _setPrototypeOf(o, p)
}
function _inherits(subClass, superClass) {
if (typeof superClass !== 'function' && superClass !== null) {
throw new TypeError('Super expression must either be null or a function')
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true }
})
Object.defineProperty(subClass, 'prototype', { writable: false })
if (superClass) _setPrototypeOf(subClass, superClass)
}
extends继承目标必须是一个function或者null
类本质还是构造函数,因此它同时具有[[prototype]]与prototype
从babel编译的代码我们可以得出以下关系
// 这里用非标准的方式,更加直观
console.log(Child.__proto__ === Parent) // true
console.log(Child.prototype.__proto__ === Parent.prototype) // true
其中class继承的原型链相比于es5的原型链多了一条关系:Child.__proto__ -> Parent
es5
es6
为什么会多一条这样的关系呢,我们来看段代码
// es6 class
class A {
static a = 1
static test = function () { return this.a }
}
class B extends A {}
console.log(B.a) // 1
console.log(B.test()) //1
es5 寄生组合继承
function createObj(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(o, options) {
const t = createObj(o)
Object.defineProperties(t, options)
return t
}
function Parent(name) {
this.name = name
}
Parent.a = 1
parent.test = function () {
return this.a
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = inheritPrototype(Parent.prototype, {
constructor: {
value: Child,
configurable: true,
writable: true,
enumerable: false
}
})
console.log(Child.a) // undefined
console.log(Child.test) // undefined
对于class,子类可以继承父类的静态属性、方法。而es5的继承模式则不支持。这正是Child.__proto__ -> Parent这条链路的作用!
_callSuper
这块的逻辑相对比较复杂,也是es6与es5继承最大的不同点,为了方便阅读,我把babel编译调用_callSuper的例子复制过来
var Parent = _createClass(function Parent(name) {
this.name = name
})
var Child = (function (_Parent) {
function Child(name, age) {
var _this
_this = _callSuper(this, Child, [name])
_this.age = age
return _this
}
_inherits(Child, _Parent);
return _createClass(Child);
})(Parent)
// 这样代码有点绕。简化下
function _callSuper(t, o, e) {
return (
(o = _getPrototypeOf(o)),
_possibleConstructorReturn(
t,
_isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)
)
)
}
// 上面代码相当于,注意这里的childThis并不是最终Child构造函数里的this
// 这里可以认为传进去了一个空对象{}
function _callSuper(childThis,Child, [name]) {
return _possibleConstructorReturn(
// 这里就相当于childThis, Parent.apply(childThis, [name])
childThis, Child.__proto__.apply(childThis, [name])
)
}
我们接着看_possibleConstructorReturn干了啥
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === 'object' || typeof call === 'function')) {
return call
} else if (call !== void 0) {
throw new TypeError('Derived constructors may only return object or undefined')
}
return _assertThisInitialized(self)
}
由上面逻辑我们可以知道:父类构造函数执行的返回值是关键
我们用代码验证下构造函数各种类型返回值对应实例的结果
// 显示返回对象
class A {
constructor() {
this.a = 2
return {
a: 1
}
}
}
const a = new A()
console.log(a) // {a: 1}
console.log(a instanceof A) // false
// 返回null
class A {
constructor() {
this.a = 1
return null
}
}
const a = new A()
console.log(a) // {a: 1}
console.log(a instanceof A) // true
// 返回函数
class A {
constructor() {
this.a = 1
return function() {}
}
}
const a = new A()
console.log(a) // ƒ () {}
console.log(a instanceof A) // false
// 不返回即返回undefined
class A {
constructor() {
this.a = 1
}
}
const a = new A()
console.log(a) // {a:1}
console.log(a instanceof A) // true
// 返回基本类型
class A {
constructor() {
this.a = 1
return 1
}
}
const a = new A()
console.log(a) // {a:1}
console.log(a instanceof A) // true
由上面例子可以得出结论:当Parent.call(childThis)的值是一个有值对象的时候,返回该对象。其他情况都返回childThis
这里有个关键的顺序点:
function Child(name, age) {
var _this
_this = _callSuper(this, Child, [name])
_this.age = age
return _this
}
我们可以发现,子类实际的this由父类构造函数按其执行规则执行后的返回结果来进行初始化,这也说明:在子类构造函数里,我们不能在super函数调用之前使用this。因为此时this还没有初始化
我们看以下代码
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {}
}
var child1 = new Child('kk', '18')
babel编译结果:
var Parent = /*#__PURE__*/_createClass(function Parent(name) {
_classCallCheck(this, Parent);
this.name = name;
});
var Child = /*#__PURE__*/function (_Parent) {
function Child(name, age) {
var _this;
_classCallCheck(this, Child);
// _this为undefined, 会抛出异常
return _assertThisInitialized(_this);
}
_inherits(Child, _Parent);
return _createClass(Child);
}(Parent);
var child1 = new Child('kk', '18');
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
}
return self
}
我们可以发现,没有调用super(), _this没有初始化。子类构造函数的this为undefined,后会抛出异常
最后
这里我们对开头的两个问题进行归纳总结下
es6中class实现继承的原理是什么
es6中class实现继承的原理与es5一样,都是基于原型链与构造函数
es6与es5实现继承的区别有哪些
es6继承的原型链对比es5多了一条直接基于构造函数本身的继承。这使得子类可以访问父类上的静态属性和静态方法es5的继承机制是先生成子类实例对象,然后再将父类的属性添加进去。即
实例在前,继承在后es6的继承机制则是先将父类的属性和方法,加到一个空的对象上面,然后再对其进行加工,添加子类自己的实例属性和方法,最后作为自己的实例对象。即“
继承在前,实例在后”
到这里,就是本篇文章的全部内容了
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
如果你有疑问或者出入,评论区告诉我,我们一起讨论