介绍
本文是 JavaScript 高级深入浅出系列的第十一篇,将承接第十篇,了解 ES6 规范中的 class
正文
1. 使用 class 定义类
按照第十篇的方式来定义一个类,不仅和普通函数没有很大区别,而且代码也不容易理解。
- 在 ES6 规范中,直接使用
class关键字来定义类。 - 但是这只是一种语法糖,最终还是转为传统的方式来定义一个类
那么,该如何使用class来定义一个类呢?有两种方式:类声明和类表达式
// 类声明
class Person {
}
// 表达式声明也是可以的,不过不建议在开发中使用
var Student = class {
}
2. 类的特性
通过对类的特性的研究,我们会发现,它和构造函数的某些特性是相同的
class Person {}
var p = new Person()
console.log(Person) // [class Person]
console.log(Person.prototype) // {}
console.log(Person.prototype.constructor) // [class Person]
console.log(p.__proto__ === Person.prototype) // true
console.log(typeof Person) // function
2.1 类的构造函数
通过类的构造方法constructor来创建实例时向类传递参数,一个类只能有一个构造函数(因此不能和 Java 一样可以重载构造函数)
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
var p = new Person('alex', 18)
console.log(p) // Person { name: 'alex', age: 18 }
调用构造函数,和之前new调用构造函数的步骤是一样的:
- 创建一个空对象
- 将类内部的
this指向这个空对象的this - 将类的
prototype赋值给这个空对象的[[prototype]](__proto__)属性 - 执行构造函数体
- 返回这个新创建对象
2.2 类的实例方法
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
// running 就是一个实例方法
running() {
console.log(`${this.name} is running`)
}
}
var p = new Person('alex', 18)
p.running()
// 等同于
Person.prototype.running = function() {
// ......
}
打印Object.getOwnDescripors(Person.prototype),得出
{
constructor: {
value: [class Person],
writable: true,
enumerable: false,
configurable: true
},
running: {
value: [Function: running],
writable: true,
enumerable: false,
configurable: true
}
}
2.3 类的访问器方法
var obj = {
_name: 'obj',
get name() {
return this._name
},
set name(val) {
this._name = val
},
}
在 ES6 之前,我们可以定义一些构造函数的访问器方法,在class中,可以这样定义:
class Person {
constructor(name) {
this._name = name
}
// 类的访问器方法
get name() {
return this._name
}
set name(val) {
this._name = val
}
}
const p = new Person('alex')
console.log(Object.getOwnPropertyDescriptors(p))
/*
{
_name: {
value: 'alex',
writable: true,
enumerable: true,
configurable: true
}
}
*/
通过getter、setter访问器函数,可以对读写进行拦截操作
2.4 类的静态方法
静态方法:类独有,实例没有,通过static关键字来描述一个类中的一个方法为静态方法
class Person {
constructor(name) {
this._name = name
}
static greeting() {
console.log(`Person 类 say hello`)
}
}
const p = new Person('alex')
Person.greeting() // 成功
p.greeting() // 报错,没有此方法
比如,我们可以写一个创建Person实例的静态方法
class Person {
constructor(name) {
this._name = name
}
static greeting() {
console.log(`Person 类 say hello`)
}
static createPerson(...params) {
return new Person(...params)
}
}
这样,就可以有两种创建Person实例的方式,区别在于一个是使用new,一个是调用类的静态方法
const p = new Person('alex')
const p2 = Person.createPerson('john')
3. 实现继承
不同于 ES5 的自定义一个工具函数实现继承来讲,ES6 直接写好了一个特性extends来实现继承,不过效果都是一样的
class Person {
constructor(name) {
this.name = name
}
running() {
console.log(`${this.name} is running`)
}
}
// extends 关键字继承父类
class Student extends Person {
constructor(name, sno) {
// super 调用父类的构造函数
// 传入自定义参数
super(name)
this.sno = sno
}
studying() {
console.log(`${this.name} is studying, and his sno is ${this.sno}`)
}
}
const s1 = new Student('alex', 1033)
s1.running() // alex is running
s1.studying() // alex is studying, and his sno is 1033
3.1 super 关键字
我们发现在上面继承一个类时,使用了super,以下为注意事项:
- 在子(派生)类的构造函数使用
this或返回默认对象前,必须通过super调用父类的构造函数 super的使用位置:子类的构造函数、实例方法、静态方法
super的用法主要有两种:
// 调用父类的构造方法
super([arguments])
// 调用父类上的静态方法
super.FunctionOnParent([arguments])
4. 方法的重写
// 在实际的开发中,可能会遇到父类的某个方法实现了某些功能
// 子类仍然需要这个方法,但是要加入自己特有的逻辑
// 因此可以使用方法的重写
class Person {
constructor(name) {
this.name = name
}
running() {
// ...... 某些逻辑
console.log(`${this.name} is running`)
}
}
class Student extends Person {
constructor(name, sno) {
super(name)
this.sno = sno
}
// 子类和父类都有同名的方法,就是对于该方法的重写
running() {
// 调用父类同名方法,执行逻辑
super.running()
console.log(`student ${this.name} is running`)
}
}
const s1 = new Student('alex', 1033)
s1.running() // 执行了 Person 和 Student 的 running 方法逻辑
5. class 语法糖转为 ES5 代码
由于很多用户的浏览器版本不同,旧版本无法兼容目前新版本的语法,所以为了兼顾大部分用户的体验,一般我们在编写新语法时,会使用一些工具(例如 babel)转为旧语法代码,这样旧版本浏览器就能识别
// ES6 代码
class Person {
constructor(name) {
this.name = name
}
running() {
console.log(`${this.name} is running`)
}
static staticMethod() {
console.log(`Person static method`)
}
}
// babel 转换为 ES5 代码 👇
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
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)
return Constructor
}
var Person = /*#__PURE__*/ (function() {
function Person(name) {
_classCallCheck(this, Person)
this.name = name
}
_createClass(
Person,
[
{
key: 'running',
value: function running() {
console.log(''.concat(this.name, ' is running'))
},
},
],
[
{
key: 'staticMethod',
value: function staticMethod() {
console.log('Person static method')
},
},
]
)
return Person
})()
5.1 解析代码
首先,将 Person 这个类转为一个函数,内部Person函数先对于调用进行一个校验,如果将Person这个类作为函数进行调用,那么就会报一个TypeError的错误。
var Person = /*#__PURE__*/ (function() {
function Person(name) {
_classCallCheck(this, Person)
this.name = name
}
_createClass(
Person,
[
{
key: 'running',
value: function running() {
console.log(''.concat(this.name, ' is running'))
},
},
],
[
{
key: 'staticMethod',
value: function staticMethod() {
console.log('Person static method')
},
},
]
)
return Person
})()
核心代码在于_createClass和_defineProperties两个函数
_createClass接收三个参数,- 第一个参数是需要挂载的目标对象,代码中是
Person。 - 第二个参数接收所定义的所有的实例方法的数组,数组中每一项是一个对象,
key为方法名称,value为对应的函数 - 第三个参数接收所定义的所有的静态方法的数组,数组存储的值和第二个参数相同
- 该函数的作用在于对实例方法和静态方法进行校验,并调用
_defineProperties,如果是实例方法,就传入Person.prototype和实例方法数组,如果是静态方法,就传入Person和静态方法数组
- 第一个参数是需要挂载的目标对象,代码中是
_defineProperties接收两个参数,- 第一个参数是需要挂载属性的目标
- 第二个参数是需要挂载的属性数组
- 对于属性数组遍历,通过
Object.defineProperty()的方式来一一将属性挂载到目标对象上
简单来说,将用户定义的静态方法挂载在该构造函数中,将用户定义的实例方法挂载在该构造函数的原型上。
5.2 /*#__pure__*/
这个是一个标记,意思是标记该函数为一个纯函数,在 webpack 对代码进行压缩优化时,如果遇到了纯函数标记,那么就可以对该函数进行tree-shaking。
tree-shaking?如果在分析依赖时,发现该函数没有被用到,那么就会将该函数的所有代码从代码树中删除掉,有效减少了代码的体积
6. ES6 转 ES5 之继承代码解读
// ES6 代码
class Person {}
class Student extends Person {}
// babel 转为 ES5 代码
'use strict'
function _typeof(obj) {
'@babel/helpers - typeof'
if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') {
_typeof = function _typeof(obj) {
return typeof obj
}
} else {
_typeof = function _typeof(obj) {
return obj &&
typeof Symbol === 'function' &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? 'symbol'
: typeof obj
}
}
return _typeof(obj)
}
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 },
})
if (superClass) _setPrototypeOf(subClass, superClass)
}
function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p
return o
}
return _setPrototypeOf(o, p)
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct()
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor
result = Reflect.construct(Super, arguments, NewTarget)
} else {
result = Super.apply(this, arguments)
}
return _possibleConstructorReturn(this, result)
}
}
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)
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
)
}
return self
}
function _isNativeReflectConstruct() {
if (typeof Reflect === 'undefined' || !Reflect.construct) return false
if (Reflect.construct.sham) return false
if (typeof Proxy === 'function') return true
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function() {})
)
return true
} catch (e) {
return false
}
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o)
}
return _getPrototypeOf(o)
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Person = function Person() {
_classCallCheck(this, Person)
}
var Student = /*#__PURE__*/ (function(_Person) {
_inherits(Student, _Person)
var _super = _createSuper(Student)
function Student() {
_classCallCheck(this, Student)
return _super.apply(this, arguments)
}
return Student
})(Person)
6.1 解析代码
首先从声明开始
// 声明一个 Person 构造函数,做了边界判断,如果直接调用该构造函数,报错
var Person = function Person() {
_classCallCheck(this, Person)
}
// 声明子类,这里是实现继承的核心方法
var Student = /*#__PURE__*/ (function(_Person) {
// _inherits() 函数用于实现继承
_inherits(Student, _Person)
var _super = _createSuper(Student)
function Student() {
_classCallCheck(this, Student)
return _super.apply(this, arguments)
}
return Student
})(Person)
_inherits函数
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 },
})
// _setPrototypeOf 函数,修改 subClass 的原型为 superClass
// 即 Student.__proto__ = Person
// 这一步操作目的是静态方法的继承
if (superClass) _setPrototypeOf(subClass, superClass)
}
function _setPrototypeOf(o, p) {
// 如果又 Object.setPrototypeOf 方法直接调用该方法
// 如果没有就手动实现,修改 proto,由于某些历史问题,直接修改 __proto__ 不仅会造成兼容的问题,还会有严重的性能缺陷,因此能直接使用 setPrototypeOf 就不要去修改 __proto__
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p
return o
}
return _setPrototypeOf(o, p)
}
_createSuper函数
这一个函数返回一个函数,用于下面的_super.apply(this, arguments)。为什么不要直接调用呢?因为上面做了边界判断,不可以直接调用构造函数,所以包装了一层,该函数返回了createSuperInternal函数(闭包)
// 调用
var _super = _createSuper(Student)
// 函数,Derived 派生
function _createSuper(Derived) {
// 判断当前环境支不支持 reflect
var hasNativeReflectConstruct = _isNativeReflectConstruct()
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor
result = Reflect.construct(Super, arguments, NewTarget)
} else {
result = Super.apply(this, arguments)
}
return _possibleConstructorReturn(this, result)
}
}
createSuperInternal函数
Super获取到原型(上面我们将子类的__proto__ = 父类,所以Super = Person 构造函数)。如果支持 Reflect,就用Reflect.construct来创建一个新对象(ES6 新增的 Reflect)。如果不支持 Reflect,使用Super.apply来创建一个新对象。
最后,调用_possibleConstructorReturn,这个函数还是进行了边界判断,返回的还是创建出来的新对象
function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor
result = Reflect.construct(Super, arguments, NewTarget)
} else {
result = Super.apply(this, arguments)
}
return _possibleConstructorReturn(this, result)
}
回到上文中:
var Student = /*#__PURE__*/ (function(_Person) {
// _inherits() 函数用于实现继承
_inherits(Student, _Person)
// 这里的 _super 返回的就是 `createSuperInternal` 函数
var _super = _createSuper(Student)
function Student() {
_classCallCheck(this, Student)
// _super.apply 该函数返回了创建出来的新对象
return _super.apply(this, arguments)
}
return Student
})(Person)
7. 继承内置类
我们可以让自己的类继承自内置类,例如Array
class MyArray extends Array {
constructor(length) {
super(length)
}
set lastValue(value) {
this[this.length - 1] = value
}
get lastValue() {
return this[this.length - 1]
}
}
// 可以使用父类 Array 内的方法
const arr = new MyArray(10).fill(10)
// 可以自定义一些功能
arr.lastValue = 9
console.log(arr, arr.lastValue)
8. JS 中的多态
面向对象有三大特性:封装、继承、多态。前两者我们在第10篇中已经详细解释过。下面我们来讨论一下 JS 中的多态
维基百科中对于多态的定义:多态(polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型
简单理解来说:不同的数据类型进行同一个操作,表现出不同的行为,这就是多态的表现
从定义来看,JS 是存在于多态的
8.1 传统的面向对象语言中的多态
在传统的面向对象的语言中(如 Java),多态一般表现为重写与重载。
- 重写,一般是子类重写父类某个方法的实现过程,要求方法名、参数类型、参数个数、返回值类型都相同
- 重载,同一个方法可以有不同的实现细节,方法名相同,参数类型、参数个数不同(重载究竟属不属于多态大多数的观点说的语焉不详,网上部分人认为重载仅属于函数的多态,并不属于面向对象中的多态,这里由你来自我辨别)
ECMAScript 并没有规范方法的重写与重载,甚至在 JS 中无法实现重载,但是可以实现重写
8.2 JS 中的多态
严格意义上的多态,是发生在类内部的,但是 JS 由于其是动态语言,所以非常灵活,因此也会有另一种多态的表现形式:
举个例子来理解一下吧:
var baiduMap = {
render: function () {
console.log('渲染百度地图')
},
}
var googleMap = {
render: function () {
console.log('渲染谷歌地图')
},
}
两个地图的行为是相同的(都拥有render行为,区别在于render做出的操作),按照这种形式来写代码,很容易就会发现,多个地图需要写多个对象,但是对象的行为都是相同的,会造成代码大量冗余。这个时候,我们可以将render抽离出来,甚至于不同的两个类型也可以实现多态的形式
// 提供统一的接口
var map = {
render: function (msg) {
console.log(msg)
},
}
var googleMap = {
msg: '渲染谷歌地图',
}
class BaiduMap {
constructor(msg) {
this.msg = msg
}
}
const baiduMap = new BaiduMap('渲染百度地图')
// 不同的类型触发统一的接口会有不同的表现
map.render(googleMap.msg) // 渲染谷歌地图
map.render(baiduMap.msg) // 渲染百度地图
这样的代码,就是一个多态的表现。多态是一种编程思想,不需要拘泥于某一种表现形式,只要满足于维基百科中的定义就是多态的表现
总结
本文中,你学到了:
- 如果使用
class定义一个类 - 类的实例方法、静态方法、属性
- 如何使用
extends来实现继承 class通过babel转为 ES5 代码的表现- JS 中的多态的表现