Skip to content

作者:Yuan Tang
更新于:5 个月前
字数统计:3.9k 字
阅读时长:17 分钟

含义

声明定义

可以使用类声明和赋值表达式定义类,推荐使用类声明来定义类

js
// 类声明
class User {
}
console.log(new Article())
let Article = class {
}
console.log(new User())

类方法间不需要逗号

js
class User {
  show() {}
  get() {
    console.log('get method')
  }
}
const hd = new User()
hd.get()

构造函数

使用 constructor 构造函数传递参数,下例中show为构造函数方法,getName为原型方法

  • constructor 会在 new 时自动执行
js
class User {
  constructor(name) {
    this.name = name // name赋值为tydumpling
    this.show = function () {}
  }

  getName() {
    return this.name
  }
}
const dd = new User('tydumpling')
console.log(dd)

构造函数用于传递对象的初始参数,但不是必须定义的,如果不设置系统会设置如下类型

  • 子构造器中调用完super 后才可以使用 this
js
constructor(...args) {
  super(...args);
}

原理分析

类其实是函数

js
class User {
}
console.log(typeof User) // function

constructor 用于定义函数代码,下面是与普通函数的对比,结构是一致的

js
class User {
  constructor(name) {
    this.name = name
  }

  show() {}
}
console.dir(User)
console.log(User == User.prototype.constructor) // true

// 下面是对比的普通函数
function Fn(name) {
  this.name = name
}
console.dir(Fn)
console.log(Fn == Fn.prototype.constructor) // true

在类中定义的方法也保存在函数原型中

js
class User {
  constructor(name) {
    this.name = name
  }

  show() {}
}
console.dir(User)
console.log(Object.getOwnPropertyNames(User.prototype)) // ["constructor", "show"]

所以下面定义的类

js
class User {
  constructor(name) {
    this.name = name
  }

  show() {
    console.log(this.name)
  }
}

与下面使用函数的定义是一致的

js
function User(name) {
  this.name = name
}
Fn.prototype.show = function () {
  console.log(this.name)
}

不同的是,类定义的show是不可遍历的;构造函数原型定义是可遍历的。

属性定义

class 中定义的属性为每个new 出的对象独立创建,下面定义了 sitename 两个对象属性

js
class User {
  site = 'tydumpling'
  constructor(name) {
    this.name = name
  }

  show() {
    console.log(`${this.site}:${this.name}`)
  }
}
const hd = new User('tydumpling')
hd.show() // tydumpling:tydumpling

函数差异

class 是使用函数声明类的语法糖,但也有些区别

class 中定义的方法不能枚举。打印 class 定义的类时,发现定义函数默认特征为 false ,也是一种语法糖,让其只需获取对象的属性,而不会获取原型上的。

js
class User {
  constructor(name) {
    this.name = name
  }

  show() {
    console.log(this.name)
  }
}
const dd = new User('tydumpling')
// 不会枚举出show属性
for (const key in dd)
  console.log(key)

function Fn(name) {
  this.name = name
}
Fn.prototype.show = function () {
  console.log(this.name)
}
const obj = new Fn('tydumpling')
for (const key in obj)
  console.log(key)

严格模式

class 默认使用strict 严格模式执行

js
class User {
  constructor(name) {
    this.name = name
  }

  show() {
    function test() {
    	// 严格模式下输出 undefined
      console.log(this)
    }
    test()
  }
}
const dd = new User('tydumpling')
dd.show()

function Fn(name) {
  this.name = name
}
Fn.prototype.show = function () {
  function test() {
  	// 非严格模式输出 Window
    console.log(this)
  }
  test()
}
const obj = new Fn('tydumpling')
obj.show()

静态访问

静态属性

静态属性即为类设置属性,而不是为生成的对象设置,下面是原理实现

js
function User() {}
User.site = 'tydumpling'
console.dir(User)

const hd = new User()
console.log(hd.site) // undefiend
console.log(User.site) // tydumpling

class 中为属性添加 static 关键字即声明为静态属性

  • 可以把为所有对象使用的值定义为静态属性
  • 静态属性不属于对象,只属于类,只能类使用
js
class Request {
  static HOST = 'https://www.houdunren.com'

  query(api) {
    return `${Request.HOST}/${api}`
  }
}
const request = new Request()

静态方法

指通过类访问不能使用对象访问的方法,比如系统的Math.round()就是静态方法

  • 一般来讲方法不需要对象属性参与计算就可以定义为静态方法

下面是静态方法实现原理

js
function User() {
  this.show = function () {
    return 'this is a object function'
  }
}
User.show = function () {
  return 'this is a User function'
}
const dd = new User()
console.dir(dd.show()) // this is a object function
console.dir(User.show()) // this is a User function

class 内声明的方法前使用 static 定义的方法即是静态方法。通过类调用的方法是静态方法,通过类的实例调用的方法是成员方法

js
class User {
  constructor(name) {
    this.name = name
  }

  static create(name) {
    return new User(name)
  }
}
const dd = User.create('tydumpling')
console.log(dd)

下面使用静态方法在课程类中的使用

js
const data = [
  { name: 'js', price: 100 },
  { name: 'mysql', price: 212 },
  { name: 'vue.js', price: 98 }
]
class Lesson {
  constructor(data) {
    this.model = data
  }

  get price() {
    return this.model.price
  }

  get name() {
    return this.model.name
  }

  // 批量生成对象
  static createBatch(data) {
    return data.map(item => new Lesson(item))
  }

  // 最贵的课程
  static MaxPrice(collection) {
    return collection.sort((a, b) => b.price() - a.price())[0]
  }
}
const lessons = Lesson.createBatch(data)
console.log(lessons)
console.log(Lesson.MaxPrice(lessons).name)

访问器

使用访问器可以对对象的属性进行访问控制,下面是使用访问器对私有属性进行管理。

语法介绍

  • 使用访问器可以管控属性,有效的防止属性随意修改
  • 访问器就是在函数前加上 get/set修饰,操作属性时不需要加函数的扩号,直接用函数名
js
class User {
  constructor(name) {
    this.data = { name }
  }

  get name() {
    return this.data.name
  }

  set name(value) {
    if (value.trim() == '')
      throw new Error('invalid params')
    this.data.name = value
  }
}
const hd = new User('tydumpling')
hd.name = 'tydumpling'
console.log(hd.name)

访问控制

设置对象的私有属性有多种方式,包括后面章节介绍的模块封装。

public

public 指不受保护的属性,在类的内部与外部都可以访问到

js
class User {
  url = 'houdunren.com'
  constructor(name) {
    this.name = name
  }
}
const hd = new User('tydumpling')
console.log(hd.name, hd.url)

protected

protected是受保护的属性修释,不允许外部直接操作,但可以继承后在类内部访问,有以下几种方式定义

命名保护

将属性定义为以 _ 开始,来告诉使用者这是一个私有属性,请不要在外部使用。

  • 外部修改私有属性时可以使用访问器 setter 操作
  • 但这只是提示,就像吸烟时烟盒上的吸烟有害健康,但还是可以抽的
js
class Article {
  _host = 'https://tydumpling.com'

  set host(url) {
    if (!/^https:\/\//i.test(url))
      throw new Error('网址错误')

    this._host = url
  }

  lists() {
    return `${this._host}/article`
  }
}
const article = new Article()
console.log(article.lists()) // https://tydumpling.com/article
article.host = 'https://tydumpling.com'
console.log(article.lists()) // https://tydumpling.com/article

继承时是可以使用的

js
class Common {
  _host = 'https://tydumpling.com'
  set host(url) {
    if (!/^https:\/\//i.test(url))
      throw new Error('网址错误')

    this._host = url
  }
}
class Article extends Common {
  lists() {
    return `${this._host}/article`
  }
}
const article = new Article()
console.log(article.lists()) // https://tydumpling.com/article
article.host = 'https://tydumpling.com'
console.log(article.lists()) // https://tydumpling.com/article

Symbol

下面使用 Symbol定义私有访问属性,即在外部通过查看对象结构无法获取的属性

js
const protecteds = Symbol()
class Common {
  constructor() {
    this[protecteds] = {}
    this[protecteds].host = 'https://houdunren.com'
  }

  set host(url) {
    if (!/^https?:/i.test(url))
      throw new Error('非常网址')

    this[protecteds].host = url
  }

  get host() {
    return this[protecteds].host
  }
}
class User extends Common {
  constructor(name) {
    super()
    this[protecteds].name = name
  }

  get name() {
    return this[protecteds].name
  }
}
const hd = new User('tydumpling')
hd.host = 'https://www.hdcms.com'
// console.log(hd[Symbol()]);
console.log(hd.name)

WeakMap

WeakMap 是一组键/值对的集,下面利用WeakMap类型特性定义私有属性

js
const _host = new WeakMap()
class Common {
  constructor() {
    _host.set(this, 'https://houdunren.com')
  }

  set host(url) {
    if (!/^https:\/\//i.test(url))
      throw new Error('网址错误')

    _host.set(this, url)
  }
}
class Article extends Common {
  constructor() {
    super()
  }

  lists() {
    return `${_host.get(this)}/article`
  }
}
const article = new Article()
console.log(article.lists()) // https://houdunren.com/article
article.host = 'https://hdcms.com'
console.log(article.lists()) // https://hdcms.com/article

也可以统一定义私有属性

js
const protecteds = new WeakMap()
class Common {
  constructor() {
    protecteds.set(this, {
      host: 'https://houdunren.com',
      port: '80'
    })
  }

  set host(url) {
    if (!/^https:\/\//i.test(url))
      throw new Error('网址错误')

    protecteds.set(this, { ...protecteds.get(this), host: url })
  }
}
class Article extends Common {
  constructor() {
    super()
  }

  lists() {
    return `${protecteds.get(this).host}/article`
  }
}
const article = new Article()
console.log(article.lists()) // https://houdunren.com/article
article.host = 'https://hdcms.com'
console.log(article.lists()) // https://hdcms.com/article

private

private 指私有属性,只在当前类可以访问到,并且不允许继承使用

  • 为属性或方法名前加 # 为声明为私有属性
  • 私有属性只能在声明的类中使用

下面声明私有属性 #host 与私有方法 check 用于检测用户名

js
class User {
  // private
  #host = 'https://houdunren.com'
  constructor(name) {
    this.name = name
    this.#check(name)
  }

  set host(url) {
    if (!/^https?:/i.test(url))
      throw new Error('非常网址')

    this.#host = url
  }

  get host() {
    return this.#host
  }

  #check = () => {
    if (this.name.length <= 5)
      throw new Error('用户名长度不能小于五位')

    return true
  }
}
const hd = new User('后盾人在线教程')
hd.host = 'https://www.hdcms.com'
console.log(hd.host)

属性保护

保护属性并使用访问器控制

js
const protecteds = Symbol('protected')
class User {
  constructor(name) {
    this[protecteds] = { name }
  }

  get name() {
    return this[protecteds].name
  }

  set name(value) {
    if (value.trim() == '')
      throw new Error('invalid params')
    this[protecteds].name = value
  }
}
const hd = new User('tydumpling')
hd.name = 'tydumpling'
console.log(hd.name)
console.log(Object.keys(hd))

详解继承

属性继承

属性继承的原型如下

js
function User(name) {
  this.name = name
}
function Admin(name) {
  User.call(this, name)
}
const hd = new Admin('tydumpling')
console.log(hd)

这就解释了为什么在子类构造函数中要先执行super

js
class User {
  constructor(name) {
    this.name = name
  }
}
class Admin extends User {
  constructor(name) {
    super(name)
  }
}
const hd = new Admin('tydumpling')
console.log(hd)

继承原理

class 继承内部使用原型继承

image-20191211135724814
js
class User {
  show() {
    console.log('user.show')
  }
}
class Admin extends User {
  info() {
    this.show()
  }
}
const hd = new Admin()
console.dir(hd)

方法继承

原生的继承主要是操作原型链,实现起来比较麻烦,使用 class 就要简单的多了。

  • 继承时必须在子类构造函数中调用 super() 执行父类构造函数
  • super.show() 执行父类方法

下面是子类继承了父类的方法show

js
class Person {
  constructor(name) {
    this.name = name
  }

  show() {
    return `后盾人会员: ${this.name}`
  }
}
class User extends Person {
  constructor(name) {
    super(name)
  }

  run() {
    return super.show()
  }
}
const dd = new User('tydumpling')
console.dir(dd.run())

可以使用 extends 继承表达式返回的类

js
function controller() {
  return class {
    show() {
      console.log('user.show')
    }
  }
}
class Admin extends controller() {
  info() {
    this.show()
  }
}
const hd = new Admin()
console.dir(hd)

super

表示从当前原型中执行方法,

  • super 一直指向当前对象

下面是使用 this 模拟super,会有以下问题

  • this指向当前对象,结果并不是 adminname
js
const user = {
  name: 'user',
  show() {
    return this.name
  }
}
const admin = {
  __proto__: user,
  name: 'admin',
  show() {
    return this.__proto__.show()
  }
}
console.log(admin.show()) // user

为了解决以上问题,需要调用父类方法时传递this

js
const user = {
  name: 'user',
  show() {
    return this.name
  }
}
const admin = {
  __proto__: user,
  name: 'admin',
  show() {
    return this.__proto__.show.call(this)
  }
}
console.log(admin.show()) // admin

上面看似结果正常,但如果是多层继承时,会出现新的问题

  • 因为始终传递的是当前对象this ,造成从 this 原型循环调用
js
const common = {
  show() {
    console.log('common.init')
  }
}
const user = {
  __proto__: common,
  name: 'user',
  show() {
    return this.__proto__.show.call(this) // this被改变,相当于再调用一次admin的__proto__的show函数(也就是user的show函数)造成死循环
  }
}
const admin = {
  __proto__: user,
  name: 'admin',
  get() {
    return this.__proto__.show.call(this) // user的this改成了admin的
  }
}
console.log(admin.get())

为了解决以上问题 js 提供了 super 关键字

  • 使用 super 调用时,在所有继承中 this 始终为调用对象
  • super 是用来查找当前对象的原型,而不像上面使用 this 查找原型造成死循环
  • 也就是说把查询原型方法的事情交给了 superthis 只是单纯的调用对象在各个继承中使用
js
const common = {
  show() {
    return this.name
  }
}
const user = {
  __proto__: common,
  name: 'user',
  show() {
    return super.show(this)
  }
}
const admin = {
  __proto__: user,
  name: 'admin',
  get() {
    return super.show()
  }
}
console.log(admin.get())

super 只能在类或对象的方法中使用,而不能在函数中使用,下面将产生错误

js
const user = {
  name: 'user',
  show() {
    return this.name
  }
}
const admin = {
  __proto__: user,
  name: 'admin',
  get() {
    return super.show()
  }
}
console.log(admin.get()) // Uncaught SyntaxError: 'super' keyword unexpected here

constructor

super 指调父类引用,在构造函数constructor 中必须先调用super()

  • super() 指调用父类的构造函数
  • 必须在 constructor 函数里的this 调用前执行 super()
js
class User {
  constructor(name) {
    this.name = name
  }

  show() {
    console.log(this.name)
  }
}
class Admin extends User {
  constructor(name) {
    super(name)
  }
}
const hd = new Admin('tydumpling')
hd.show()

constructor 中先调用 super 方法的原理如下

js
function Parent(name) {
  this.name = name
}
function User(...args) {
  Parent.apply(this, args)
}
User.prototype = Object.create(User.prototype)
User.prototype.constructor = User
const hd = new User('tydumpling')
console.log(hd.name)

父类方法

使用super 可以执行父类方法

  • 不添加方法名是执调用父类构造函数
js
class User {
  constructor(name) {
    this.name = name
  }

  getName() {
    return this.name
  }
}
class Admin extends User {
  constructor(name) {
    super(name)
  }
}
const hd = new Admin('tydumpling')
console.log(hd.getName())

下面是通过父类方法获取课程总价

js
class Controller {
  sum() {
    return this.data.reduce((t, c) => t + c.price, 0)
  }
}
class Lesson extends Controller {
  constructor(lessons) {
    super()
    this.data = lessons
  }

  info() {
    return {
      totalPrice: super.sum(),
      data: this.data
    }
  }
}
const data = [
  { name: 'js', price: 100 },
  { name: 'mysql', price: 212 },
  { name: 'vue.js', price: 98 }
]
const hd = new Lesson(data)
console.log(hd.info())

方法覆盖

子类存在父类同名方法时使用子类方法

js
class User {
  constructor(name) {
    this.name = name
  }

  say() {
    return this.name
  }
}
class Admin extends User {
  constructor(name) {
    super(name)
  }

  say() {
    return `你好:${super.say()}`
  }
}
const dd = new Admin('tydumpling')
console.log(dd.say()) // 你好:tydumpling

下面是覆盖父类方法,只获取课程名称

js
class Controller {
  say() {
    return this.name
  }

  total() {
    return this.data.reduce((t, c) => t + c.price, 0)
  }

  getByKey(key) {
    return this.data.filter(item => item.name.includes(key))
  }
}
class Lesson extends Controller {
  constructor(lessons) {
    super()
    this.data = lessons
  }

  getByKey(key) {
    return super.getByKey(key).map(item => item.name)
  }
}
const data = [
  { name: 'js', price: 100 },
  { name: 'mysql', price: 212 },
  { name: 'vue.js', price: 98 }
]
const hd = new Lesson(data)
console.log(hd.getByKey('js'))

静态继承

静态的属性和方法也是可以被继承使用的,下面是原理分析

js
function User() {}
User.site = 'tydumpling'
User.url = function () {
  return 'tydumpling.com'
}
function Admin() {}
Admin.__proto__ = User
console.dir(Admin) //
console.log(Admin.url()) // tydumpling.com

下面使用 class 来演示静态继承

js
class User {
  static site = 'tydumpling'
  static host() {
    return 'tydumpling.com'
  }
}
class Admin extends User {}
console.dir(Admin)

对象检测

instanceof

使用 instanceof 用于检测,下面是在原型中的分析(已经在原型与继承中讲过)

js
function User() {}
function Admin() {}
Admin.prototype = Object.create(User.prototype)
const hd = new Admin()
console.log(hd instanceof Admin) // true
console.log(hd instanceof User) // true

console.log(hd.__proto__ == Admin.prototype)
console.log(hd.__proto__.__proto__ == User.prototype)

下面是递归检测原型的代码,帮助你分析 instanceof 的原理

js
function checkPrototype(obj, constructor) {
  if (!obj.__proto__)
    return false
  if (obj.__proto__ == constructor.prototype)
    return true
  return checkPrototype(obj.__proto__, constructor)
}

class 内部实现就是基于原型,所以使用instanceof 判断和上面原型是一样的

js
class User {}
class Admin extends User {}
const hd = new Admin()
console.log(hd instanceof Admin)
console.log(hd instanceof User)

isPrototypeOf

使用 isPrototypeOf 判断一个对象是否在另一个对象的原型链中,下面是原理分析

js
const a = {}
const b = {
  __proto__: a
}
const c = {
  __proto__: b
}
console.log(a.isPrototypeOf(b)) // true
console.log(a.isPrototypeOf(c)) // true
console.log(c.isPrototypeOf(b)) // false

下面在使用 class 语法中使用

js
class User {}
class Admin extends User {}
const hd = new Admin()
console.log(Admin.prototype.isPrototypeOf(hd)) // true
console.log(User.prototype.isPrototypeOf(hd)) // true

继承内置类

使用原型扩展内置类

js
function Arr(...args) {
  args.forEach(item => this.push(item)) // 继承了数组的push方法
  this.first = function () {
    return this[0]
  }
  this.max = function () {
    return this.data.sort((a, b) => b - a)[0]
  }
}
const a = [1, 23]
Arr.prototype = Object.create(Array.prototype) // 继承数组的方法

const arr = new Arr('tydumpling', 2, 3)
console.log(arr.first())

使用 class扩展内置类

js
class NewArr extends Array {
  constructor(...args) {
    super(...args)
  }

  first() {
    return this[0]
  }

  add(value) {
    this.push(value)
  }

  remove(value) {
    const pos = this.findIndex((curValue) => {
      return curValue == value
    })
    this.splice(pos, 1)
  }
}
const hd = new NewArr(5, 3, 2, 1)
console.log(hd.length) // 4
console.log(hd.first()) // 5

hd.add('houdunren')
console.log(hd.join(',')) // 5,3,2,1,houdunren

hd.remove('3')
console.log(hd.join(',')) // 5,2,1,houdunren

mixin

关于mixin 的使用在原型章节已经讨论过,在class 使用也是相同的原理

JS不能实现多继承,如果要使用多个类的方法时可以使用mixin混合模式来完成。

  • mixin 类是一个包含许多供其它类使用的方法的类
  • mixin 类不用来继承做为其它类的父类

其他语言也有类似的操作比如php 语言中可以使用 trait 完成类似操作

js
const Tool = {
  max(key) {
    return this.data.sort((a, b) => b[key] - a[key])[0]
  }
}

class Lesson {
  constructor(lessons) {
    this.lessons = lessons
  }

  get data() {
    return this.lessons
  }
}

Object.assign(Lesson.prototype, Tool)
const data = [
  { name: 'js', price: 100 },
  { name: 'mysql', price: 212 },
  { name: 'vue.js', price: 98 }
]
const hd = new Lesson(data)
console.log(hd.max('price'))

实例操作

html
<style>
  * {
    padding: 0;
    margin: 0;
    box-sizing: content-box;
  }
  body {
    padding: 30px;
  }
  .slide {
    width: 300px;
    display: flex;
    flex-direction: column;
    /* box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); */
  }
  .slide dt {
    height: 30px;
    background: #34495e;
    color: white;
    display: flex;
    align-items: center;
    padding-left: 10px;
    cursor: pointer;
  }
  .slide dt:first-of-type {
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
  }
  .slide dd {
    height: 100px;
    background: #f1c40f;
    overflow: hidden;
  }
  .slide dd div {
    padding: 10px;
  }
  .slide dd:last-of-type {
    border-bottom-left-radius: 10px;
    border-bottom-right-radius: 10px;
  }
</style>
<body>
  <div class="slide s1">
    <dt>tydumpling</dt>
    <dd>
      <div>houdunren.com</div>
    </dd>
    <dt>tydumpling</dt>
    <dd>
      <div>hdcms.com</div>
    </dd>
    <dt>tydumpling</dt>
    <dd>
      <div>hdcms.com</div>
    </dd>
  </div>
</body>

<script>
  class Animation {
    constructor(el) {
      this.el = el;
      this.timeout = 5;
      this.isShow = true;
      this.defaultHeight = this.height;
    }
    hide(callback) {
      this.isShow = false;
      let id = setInterval(() => {
        if (this.height <= 0) {
          clearInterval(id);
          callback && callback();
          return;
        }
        this.height = this.height - 1;
      }, this.timeout);
    }
    show(callback) {
      this.isShow = false;
      let id = setInterval(() => {
        if (this.height >= this.defaultHeight) {
          clearInterval(id);
          callback && callback();
          return;
        }
        this.height = this.height + 1;
      }, this.timeout);
    }
    get height() {
      return window.getComputedStyle(this.el).height.slice(0, -2) * 1;
    }
    set height(height) {
      this.el.style.height = height + "px";
    }
  }
  class Slide {
    constructor(el) {
      this.el = document.querySelector(el);
      this.links = this.el.querySelectorAll("dt");
      this.panels = [...this.el.querySelectorAll("dd")].map(
        item => new Panel(item)
      );
      this.bind();
    }
    bind() {
      this.links.forEach((item, i) => {
        item.addEventListener("click", () => {
          this.action(i);
        });
      });
    }
    action(i) {
      Panel.hideAll(Panel.filter(this.panels, i), () => {
        this.panels[i].show();
      });
    }
  }
  class Panel extends Animation {
    static num = 0;
    static hideAll(items, callback) {
      if (Panel.num > 0) return;
      items.forEach(item => {
        Panel.num++;
        item.hide(() => {
          Panel.num--;
        });
      });
      callback && callback();
    }
    static filter(items, i) {
      return items.filter((item, index) => index != i);
    }
  }
  let hd = new Slide(".s1");
</script>

Contributors

Yuan Tang