夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 第1章 什么是JavaScript
  • 第2章 HTML中的JavaScript
  • 第3章 语言基础
  • 第4章 变量、作用域与内存
  • 第5章 基本引用类型
  • 第6章 集合引用类型
  • 第7章 迭代器与生成器
  • 第8章 对象、类与面向对象编程
    • 第9章 代理与反射
    • 第10章 函数
    • 第11章 异步编程
    • 第12章 BOM
    • 第14章 DOM
    • 第15章 DOM扩展
    • 第16章 DOM2和DOM3
    • 第17章 事件
    • 第18章 动画与Canvas图形
    • 第19章 表单脚本
    • 第20章 JavaScript API
    • 第21章 错误处理与调试
    • 第23章 JSON
    • 第24章 网络请求与远程资源
    • 第25章 客户端存储
    • 第26章 模块
    • 第27章 工作者进程
    • 第28章 最佳实践
    • 《JavaScript高级程序设计》
    frost
    2021-09-20

    第8章 对象、类与面向对象编程

    # 对象、类与面向对象编程

    对象是一组属性的无序集合,包括一系列的键值对。

    # 理解对象

    # 创建对象的方式

    • new Object()
    • 对象字面量
    • 工厂模式,构造函数模式,原型模式(下一节介绍)

    # 属性的类型

    分为 2 类,用 2 个中括号来标识内部特性,如[[Enumberable]]。

    • 数据属性

      • [[Configurable]]:表示属性是否可以 delete,是否可以修改特性,是否可以改为访问器属性,默认 true。
      • [[Enumberable]]:表示属性是否可以通过 for-in 循环遍历到,默认 true。
      • [[Writable]]:表示属性的值是否可以被修改,默认 true。
      • [[Value]]:表示属性实际的值,默认 undefined。
    • 访问器属性

      • [[Configurable]]:同上
      • [[Enumberable]]:同上
      • [[Get]]:获取函数,在读取属性时调用,默认 undefined。
      • [[Set]]:设置函数,在写入属性时调用,默认 undefined。

      修改数据属性和访问器属性都是通过 Object.definedPropery()来实现,分别表示为:

      // 数据属性
      let person = {}
      Object.defineProperty(person, 'name', {
        writable: false,
        value: 'nick',
      })
      console.log(person.name) // nick
      person.name = 'milk'
      console.log(person.name) // nick
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      let person = {}
      let name1 = 'jack'
      Object.defineProperty(person, 'name', {
        get() {
          return name1
        },
        set(newValue) {
          console.log('new', newValue)
          // name1 = newValue
        },
      })
      
      console.log(person.name)
      name1 = 'rose'
      console.log(person.name)
      
      person.name = 'mike'
      console.log(person.name)
      // 输出如下
      // jack
      // rose
      // new mike
      // rose
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23

    # 定义多个属性

    可以通过 Object.defineProperties()来一次性定义多个属性。

    let book = {}
    Object.defineProperties(book, {
      year: {
        value: 2017,
      },
      version: {
        value: '1.1.0',
      },
    })
    console.log(book.year) // 2017
    console.log(book.version) // '1.1.0'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # 读取属性的特性

    使用 Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。

    使用 Object.getOwnPropertyDescriptors()方法可以得到一个新对象,包括了所有属性及其描述符

    # 合并对象

    使用 Object.assign()来合并对象,接收一个目标对象和一个或多个源对象作为参数,将每个源对象中可枚举和自由属性复制到目标对象。

    let desc = { id: 'desc' }
    let src = { id: 'src' }
    let result = Object.assign(desc, src)
    console.log(result) // { id: 'src' }
    console.log(result === desc) // true
    
    1
    2
    3
    4
    5
    • 如果有多个同名属性,则复制最后一个值

    • Object.assign()是浅拷贝

      let desc = {}
      let src = {
        a: {},
        b: 1,
      }
      let result = Object.assign(desc, src)
      result.a.b = 2
      result.b = 3
      console.log(src.a) // { b: 2 }
      console.log(src.b) // 1
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

    # 相等判定

    console.log(NaN === NaN) // false
    console.log(-0 === +0) // true
    
    console.log(Object.is(NaN, NaN)) // true
    console.log(Object.is(-0, +0)) // false
    
    1
    2
    3
    4
    5

    # 增强的对象语法

    • 属性值简写

      let name = 'myname'
      // let person = {name:name} // 属性名和变量名一样
      let person = { name }
      
      1
      2
      3
    • 可计算属性

      或者说在对象字面量中动态命名属性

      let name = 'jack'
      let person = {
        [name]: 'jack chen',
      }
      console.log(person[name])
      
      1
      2
      3
      4
      5
    • 简写方法名

      let person = {
        say: function() {
          console.log('hello')
        },
        run() {
          console.log('run')
        },
      }
      person.say()
      person.run()
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

    # 对象解构

    const person = {
      name: 'jack',
      age: 18,
      favorite: 'swimming',
    }
    const { name, age, sex = 'man', favorite: hobby } = person
    console.log(name, age, sex, hobby) // jack 18 man swimming
    
    1
    2
    3
    4
    5
    6
    7

    # 创建对象

    使用 new Object()或对象字面量方式创建对象很方便,但不能快速复用,即创建具有同样接口的多个对象时,需要重复多次。下面介绍 3 种方式,可以基于接口快速创建对象。

    分别是工厂模式、构造函数模式和原型模式。

    # 工厂模式

    工厂模式是一种按照特定接口创建对象的方式,这种模式最直观的解决了创建多个对象的需求。

    缺点:没有解决对象标识问题(即新创建的对象是什么类型)。

    解读:对象标识,应该是指这个工厂函数创建的对象和其他工厂函数创建的对象在类别上的区分,即他们共同的模具是什么类型。

    function createPerson(name, age) {
      let obj = new Object() // 创建对象
      obj.name = name // 描述接口(属性)
      obj.age = age
      obj.say = function() {
        // 描述接口(方法)
        console.log(this.name)
      }
      return obj // 返回对象
    }
    let person1 = createPerson('nick', 20)
    let person2 = createPerson('jack', 18)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    # 构造函数模式

    看下构造函数如何创建对象

    function Person(name, age) {
      this.name = name
      this.age = age
      this.say = function() {
        console.log(this.name)
      }
    }
    let person1 = new Person('nick', 20)
    let person2 = new Person('jack', 18)
    person1.say() // nick
    person2.say() // jack
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 特点

    和工厂模式相比,构造函数模式由如下特点:

    • 使用 new 操作符
    • 没有显示创建对象
    • 属性和方法直接赋值给 this
    • 没有 return
    • 使用 instanceof 可以确定对象的类型

    除此之外没有其他区别,描述调用构造函数的执行过程:

    1. 在内存中创建一个新对象
    2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性
    3. 构造函数内部的 this 被赋值为这个新对象
    4. 执行构造函数内部的代码
    5. 如果构造函数返回非空对象,则返回该对象;否则返回刚创建的新对象
    # 构造函数也是函数

    只有使用 new 操作符,就是构造函数调用,否则就是普通函数的调用。

    # 缺点

    定义的方法在每个实例上都创建一遍。请看:

    function Person(name, age) {
      this.name = name
      this.age = age
      // this.say = function () {
      //   console.log(this.name)
      // }
      // 等价于
      this.say = new Function('console.log(this.name)')
    }
    let person1 = new Person('nick', 20)
    let person2 = new Person('jack', 18)
    console.log(person1.say === person2.say) // false
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    没必要定义两个不同的 Function 实例。简单解决如下:

    function Person(name, age) {
      this.name = name
      this.age = age
      this.say = say
    }
    function say() {
      // 在全局定义一处方法
      console.log(this.name)
    }
    let person1 = new Person('nick', 20)
    let person2 = new Person('jack', 18)
    console.log(person1.say === person2.say) // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    不过这样一来,say()定义在了全局作用域,可能有命名冲突,由此引出了原型模式。

    # 原型模式

    每个函数都有一个 prototype 属性,它是一个对象,即原型对象。

    在它上面定义的属性和方法可以被对象实例共享。

    回到之前的问题,看原型模式如何解决:

    function Person(name, age) {
      this.name = name
      this.age = age
    }
    Person.prototype.say = function() {
      // 将方法挂载在原型对象上
      console.log(this.name)
    }
    
    let person1 = new Person('nick', 20)
    let person2 = new Person('jack', 18)
    person1.say() // nick
    person2.say() // jack
    console.log(person1.say === person2.say) // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 理解原型

    在构造函数创建时,原型对象默认拥有 constructor 属性,指向与之关联的构造函数。而构造函数的 prototype 属性则指向该原型,因此:

    console.log(Person.prototype.constructor === Person) // true
    
    1

    一般情况下,实例可以通过 __proto__属性来获取它对应的原型,这个不是标准方式,后来 ECMA 提供了访问原型对象的方法 getPrototypeOf,因此:

    console.log(person1.__proto__ === Person.prototype) // true
    console.log(Object.getPrototypeOf(person1) === Person.prototype) // true
    console.log(person1.__proto__ === person2.__proto__) // true
    
    1
    2
    3

    如何基于一个对象,并指定其为原型,创建对象

    let bird = {
      size: 'small',
    }
    let redBird = Object.create(bird)
    redBird.color = 'red'
    console.log(redBird.size) // small
    console.log(redBird.color) // red
    console.log(Object.getPrototypeOf(redBird) === bird) // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    # 原型层级

    当访问对象的 a 属性时,遵循如下规则:

    1. 该对象实例有 a 属性吗?有则返回,没有继续搜索
    2. 该对象的原型有 a 属性吗?有则返回,没有继续搜索
    3. 该对象的原型的原型有 a 属性吗?有则返回,没有继续搜索
    4. 以此类推,直到该原型指向 null 时,返回 undefined
    function Person() {}
    Person.prototype.name = 'jack'
    
    let person1 = new Person()
    let person2 = new Person()
    console.log(person1.name) // jack
    console.log(person2.name) // jack
    
    person1.name = 'zhangsan'
    console.log(person1.name) // zhangsan
    person1.name = undefined
    console.log(person1.name) // undefined
    
    person2.name = null
    console.log(person2.name) // null
    delete person2.name
    console.log(person2.name) // jack
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    另外,可以使用 hasOwnProperty()来判断属性是否在实例上。

    function Person() {}
    Person.prototype.name = 'jack'
    
    let person1 = new Person()
    let person2 = new Person()
    person1.name = 'zhangsan'
    console.log(person1.hasOwnProperty('name')) // true
    console.log(person2.hasOwnProperty('name')) // false
    
    1
    2
    3
    4
    5
    6
    7
    8
    # in 操作符在原型中的使用

    in 单独使用和 for-in 使用时,只要对象可以访问该属性,就返回 true,即它会追溯到原型上。

    function Person() {}
    Person.prototype.name = 'jack'
    Person.prototype.age = 20
    
    let person1 = new Person()
    let person2 = new Person()
    person1.name = 'zhangsan'
    console.log('name' in person1) // true
    console.log('name' in person2) // true
    
    for (let key in person1) {
      console.log(key) // name age
    }
    for (let key in person2) {
      console.log(key) // name age
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    如何判断一个属性是原型属性(注意,不是实例属性)

    function Person() {}
    Person.prototype.name = 'jack'
    
    let person1 = new Person()
    let person2 = new Person()
    person1.name = 'zhangsan'
    
    function hasPrototypeProperty(object, name) {
      return !object.hasOwnProperty(name) && name in object
    }
    console.log(hasPrototypeProperty(person1, 'name')) // false
    console.log(hasPrototypeProperty(person2, 'name')) // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    如果只想要遍历实例上可枚举的的属性,不想遍历到原型属性,可以使用 Object.keys()

    function Person() {}
    Person.prototype.name = 'jack'
    Person.prototype.age = 20
    
    let person1 = new Person()
    let person2 = new Person()
    person1.name = 'zhangsan'
    
    for (let item of Object.keys(person1)) {
      console.log(1, item) // name
    }
    for (let item of Object.keys(person2)) {
      console.log(2, item) // (no thing)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    如果想列出所有实例属性,不论是否可以枚举,可以使用 Object.getOwnPropertyNames

    function Person() {}
    Person.prototype.name = 'jack'
    Person.prototype.age = 20
    
    let person1 = new Person()
    let person2 = new Person()
    person1.name = 'zhangsan'
    
    let keys = Object.getOwnPropertyNames(Person.prototype)
    let keys1 = Object.getOwnPropertyNames(person1)
    let keys2 = Object.getOwnPropertyNames(person2)
    console.log(keys) // [ 'constructor', 'name', 'age' ]
    console.log(keys1) // [ 'name' ]
    console.log(keys2) // []
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 属性枚举顺序

    for-in和 Object.keys()是不确定的(取决于 js 引擎)。

    Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.assign()是确定的:先以升序枚举数值键,然后以插入顺序枚举字符串和符号键,对象字面量则以逗号分隔的顺序插入。

    let k1 = Symbol('k1')
    let k2 = Symbol('k2')
    
    let o = {
      1: 1,
      second: 'second',
      [k2]: 'k1k1',
      first: 'first',
      0: 0,
    }
    
    o[k1] = 'k2k2'
    o[3] = 3
    o.third = 'third'
    o[2] = 2
    
    console.log(Object.getOwnPropertyNames(o)) // [ '0', '1', '2', '3', 'second', 'first', 'third' ]
    console.log(Object.getOwnPropertySymbols(o)) // [ Symbol(k2), Symbol(k1) ]
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 其他原型语法

    上述代码中,为了挂载属性到原型,会重复写 Person.prototype,为了更好的封装,常常写成下面的方式

    function Person() {}
    
    Person.prototype = {
      name: 'jack',
      age: 20,
      say() {
        console.log(this.name)
      },
    }
    
    const person = new Person()
    console.log(person instanceof Person) // true
    console.log(person.constructor === Person) // false
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    不过这种写法有个问题,即不能依靠 constructor 来识别类型了,因为 prototype 指向了新的对象。

    因此加上 constructor。

    function Person() {}
    
    Person.prototype = {
      constructor: Person,
      name: 'jack',
      age: 20,
      say() {
        console.log(this.name)
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    不过还不是最完善的,因为这样一来,constructor 变成可枚举属性了,而原生 constructor 默认是不可枚举的。

    继续优化。

    function Person() {}
    
    Person.prototype = {
      name: 'jack',
      age: 20,
      say() {
        console.log(this.name)
      },
    }
    Object.defineProperty(Person.prototype, 'constructor', {
      enumerable: false,
      value: Person,
    })
    
    const person = new Person()
    console.log(person instanceof Person) // true
    console.log(person.constructor === Person) // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 原型的问题

    原型模式还有一些问题

    • 弱化了向构造函数传递初始化参数的能力
    • 由于其共享特性,当共享包含引用值的属性时,会造成实例属性间互相污染(本质在于共享同一个内存地址)
    function Person() {}
    
    Person.prototype.colors = ['black', 'white']
    
    const person1 = new Person()
    person1.colors.push('yellow')
    
    const person2 = new Person()
    person2.colors.push('red')
    
    console.log(person1.colors) // [ 'black', 'white', 'yellow', 'red' ]
    console.log(person2.colors) // [ 'black', 'white', 'yellow', 'red' ]
    console.log(person1.colors === person2.colors) // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    一般来说,不同的实例应该有属于自己的属性副本。

    # 继承

    # 原型链继承

    基本思想是:子类的原型指向父类的实例,因此子类的实例可以继承父类的属性和方法。

    缺点:

    • 子类实例的constructor会指向父类构造函数

    • 由于原型共享的特性,原型中包含的引用值会在所有实例间共享,造成数据污染。

    • 子类在实例化时不能给父类的构造函数传参。

    function SuperType() {
      this.property = 'super'
      this.colors = ['red','blue']
    }
    SuperType.prototype.getSuperValue = function () {
      return this.property
    }
    function SubType() {
      this.subProperty = 'sub'
    }
    SubType.prototype = new SuperType()
    SubType.prototype.getSubValue = function () {
      return this.subProperty
    }
    
    const sub1 = new SubType()
    sub1.colors.push('green')
    const sub2 = new SubType()
    console.log(sub2.colors) // [ 'red', 'blue', 'green' ]
    
    console.log(sub1.getSuperValue()) // sub
    console.log(sub1.getSubValue()) // super
    
    console.log(sub1.constructor === SuperType) // true 不符合预期
    console.log(sub1.constructor === SubType) // false
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    判断原型和实例的关系有2种方式:

    • instanceof

      console.log(sub instanceof SubType) // true
      console.log(sub instanceof SuperType) // true
      
      1
      2
    • isPrototypeOf()

      console.log(SuperType.prototype.isPrototypeOf(sub)) // true
      console.log(SubType.prototype.isPrototypeOf(sub)) // true
      
      1
      2

    # 盗用构造函数继承(经典继承)

    基本思想是:在子类的构造函数中调用父类构造函数,并修改this指向。

    优点:

    • 解决了实例的constructor指向问题
    • 解决了原型属性共享造成的引用数据互相污染问题
    • 子类构造函数中可以向父类构造函数传参

    缺点:

    • 必须在构造函数定义方法,即方法不能重用(共享)
    • 子类不能访问父类原型上的方法。
    function SuperType() {
      this.colors = ['red', 'blue']
    }
    function SubType() {
      SuperType.call(this) // 继承SuperType
    }
    // SubType.prototype = new SuperType()
    const sub1 = new SubType()
    sub1.colors.push('green')
    console.log(sub1.colors) // [ 'red', 'blue', 'green' ]
    
    const sub2 = new SubType()
    console.log(sub2.colors) // [ 'red', 'blue' ]
    
    console.log(sub1.constructor === SuperType) // false
    console.log(sub1.constructor === SubType) // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    # 组合继承

    盗用构造函数继承基本不能单独使用,于是结合原型和盗用构造函数,形成了组合继承。是使用最多的一种继承模式。

    基本思想是:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。

    优点:子类实例既可以使用原型方法,又可以使用父类原型方法。

    缺点:父类构造函数调用了2次。

    function SuperType(name) {
      this.colors = ['red', 'blue']
      this.name = name
    }
    SuperType.prototype.sayName = function () {
      console.log(this.name)
    }
    function SubType(name, age) {
      SuperType.call(this, name)
      this.age = age
    }
    SubType.prototype = new SuperType()
    SubType.prototype.sayAge = function () {
      console.log(this.age)
    }
    
    const sub1 = new SubType('jack', 10)
    sub1.colors.push('green')
    console.log(sub1.colors) // [ 'red', 'blue', 'green' ]
    sub1.sayName() // jack
    sub1.sayAge() // 10
    
    const sub2 = new SubType('rose', 20)
    console.log(sub2.colors) // [ 'red', 'blue' ]
    sub2.sayName() // rose
    sub2.sayAge() // 20
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    # 原型式继承(一种概念或思想)

    基本思路是:创建一个临时构造函数,将构造函数的原型指向传入的对象,返回构造函数的实例。。

    function object(o) {
      function F() {}
      F.prototype = o
      return new F()
    }
    
    1
    2
    3
    4
    5

    ES5新增了Object.create()方法规范了原型式继承的概念,可以理解为基于某个对象进行继承。

    # 寄生式继承

    基本思想:类似于寄生构造函数和工厂函数,创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

    function createAnother(original) {
      let clone = Object.create(original) // 以original为原型创建新对象
      // 增强对象
      clone.say = function () {
        console.log('hi')
      }
      return clone
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    # 寄生式组合继承

    完全体。基本思想是:通过构造函数继承属性,使用混合式原型链继承方法。

    优点:解决了组合继承调用2次父类构造函数的问题

    function SuperType(name) {
      this.colors = ['red', 'blue']
      this.name = name
    }
    SuperType.prototype.sayName = function () {
      console.log(this.name)
    }
    function SubType(name, age) {
      SuperType.call(this, name)
      this.age = age
    }
    // SubType.prototype = new SuperType()
    inheritPrototype(SubType, SuperType) // 子原型指向父原型
    SubType.prototype.sayAge = function () {
      console.log(this.age)
    }
    
    function inheritPrototype(subType, superType) {
      let prototype = Object.create(superType.prototype) // 基于父类原型创建对象
      prototype.constructor = subType // 增强对象,修正constructor
      subType.prototype = prototype // 子类原型指向该对象
    }
    
    const sub1 = new SubType('jack', 10)
    sub1.colors.push('green')
    console.log(sub1.colors) // [ 'red', 'blue', 'green' ]
    sub1.sayName() // jack
    sub1.sayAge() // 10
    
    const sub2 = new SubType('rose', 20)
    console.log(sub2.colors) // [ 'red', 'blue' ]
    sub2.sayName() // rose
    sub2.sayAge() // 20
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

    # 类

    # 类定义

    有2种:类声明和类表达式。

    // 类声明
    class Person{}
    
    // 类表达式
    const Animal = class {}
    
    1
    2
    3
    4
    5
    # 类的构成
    class Person {
      // 类的构造函数
      constructor(name) {
        this.name = name
      }
      // 类的访问器
      get age() {
        return 10
      }
      // 类方法
      say() {
        console.log(this.name)
      }
      // 类的静态方法
      static say() {
        console.log('construct say')
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 类构造函数

    constructor关键字用于在类定义块内部创建类的构造函数。默认是空函数。

    类实例化时,传入的参数会用作构造函数的参数,与函数一样,构造函数默认返回this对象。

    类不能单独调用,必须使用new来调用。

    类是一种特殊的函数。

    console.log(typeof Person) // function
    console.log(Person === Person.prototype.constructor) // true
    
    const p = new Person()
    console.log(p instanceof Person) // true
    console.log(p.constructor === Person) // true
    
    1
    2
    3
    4
    5
    6

    # 实例、原型和类成员

    # 实例成员

    实例成员一般在构造函数中定义,也可以在实例上继续添加。

    实例成员对象是独立的,不会在原型上共享。

    如上述this.name属性。

    # 原型方法与访问器

    类块中的方法就是原型方法,如上述say()方法。访问器同理,如get age()。

    # 静态类方法

    静态类方法使用static关键字作为前缀,里面的this指向类本身,而不是实例。

    # 非函数原型和类成员

    类的内部不支持显示定义数据成员,但外部可以手动添加(但不推荐,因为共享引用类型的数据会互相污染)。

    class Person {
      say() {
        console.log(`${Person.hello} ${this.name}`)
      }
    }
    Person.hello = 'nihao'
    Person.prototype.name = 'jack'
    const p = new Person()
    p.say() // nihao jack
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # 继承

    类继承使用的是新语法,但背后依旧是原型链。

    # 继承基础

    类使用extends实现单继承,它可以继承任何拥有[[Construct]]和原型的对象,即不仅可以继承类,也可以继承普通的构造函数。

    // 继承类
    class Vehicle {}
    class Bus extends Vehicle {}
    const bus = new Bus()
    console.log(bus instanceof Vehicle) // true
    console.log(bus instanceof Bus) // true
    
    // 继承普通构造函数
    function Person() {}
    class Engineer extends Person {}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # super

    派生类的方法可以通过super关键字引用到它们的原型。

    // 继承类
    class Vehicle {
      constructor(age = 10) {
        this.name = 'bmw'
        this.age = age
      }
      static color() {
        return 'red'
      }
      country() {
        console.log(this.name)
        return 'china'
      }
    }
    
    class Bus extends Vehicle {
      constructor() {
        // 不要在调用super()之前使用this 且super()一定要在第一行
        super(20)
        this.name = 'hongqi'
        console.log(this) // Bus { name: 'hongqi', age: 20 }
      }
    
      // 静态方法
      static sayColor() {
        console.log(super.color()) // red
      }
      // 实例方法
      sayCountry() {
        console.log(this.name) // hongqi
        console.log(super.country()) // hongqi china
      }
    }
    
    const bus = new Bus()
    
    Bus.sayColor()
    bus.sayCountry()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    注意事项:

    • super仅限于类构造函数、实例方法和静态方法内部使用。
    • 不能单独引用super关键字。要么调用构造函数,要么引用方法。
    • 调用super()会调用父类构造函数,可以传参,并将返回的实例赋值给this。
    • 如果没有定义构造函数,在实例化派生类时会默认调用super(),并将相关参数传入。
    • 如果派生类有构造函数,super()一定要在第一行调用,或者返回一个对象。
    # 抽象基类

    可能有这样一个类,它可供其他类继承,但本身不被实例化。使用new.target可以实现。

    class BaseClass {
      constructor() {
        console.log(new.target)
        if (new.target === BaseClass) {
          throw new Error('BaseClass cannot be instantiated directly')
        }
      }
    }
    
    class MyClass extends BaseClass {}
    const my = new MyClass() // [Function: MyClass]
    const b = new BaseClass() // [Function: BaseClass]
    // Error: BaseClass cannot be instantiated directly
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 继承内置类型

    有些内置类型的方法会返回新的实例。默认情况下,返回实例的类型与原始实例的类型是一样的。

    class SubArray extends Array {}
    
    const arr1 = new SubArray(1, 2, 3)
    const arr2 = arr1.map((item) => item * 2)
    console.log(arr1) // SubArray(3) [ 1, 2, 3 ]
    console.log(arr2) // SubArray(3) [ 2, 4, 6 ]
    console.log(arr1 instanceof SubArray) // true
    console.log(arr2 instanceof SubArray) // true 这里map返回的实例类型是原始实例类型
    
    1
    2
    3
    4
    5
    6
    7
    8

    这个默认行为可以被Symbol.species访问器修改。

    class SubArray extends Array {
      static get [Symbol.species]() {
        return Array
      }
    }
    
    const arr1 = new SubArray(1, 2, 3)
    const arr2 = arr1.map((item) => item * 2)
    console.log(arr2 instanceof SubArray) // false 这里是map返回的新对象
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 类混入

    通常使用Object.assign来混入多个对象的属性,可以通过“嵌套”的方式来混入类。

    思路是,定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。

    混入模式正在被抛弃,替代它的方案是复合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。

    
    class Base {}
    let FooMixin = (SuperClass) =>
      class extends SuperClass {
        foo() {
          console.log('foo')
        }
      }
    let BarMixin = (SuperClass) =>
      class extends SuperClass {
        bar() {
          console.log('bar')
        }
        foo() {
          console.log('bar-foo')
        }
      }
    let BazMixin = (SuperClass) =>
      class extends SuperClass {
        baz() {
          console.log('baz')
        }
      }
    
    // class SubClass extends BazMixin(BarMixin(FooMixin(Base))) {}
    
    function mix(BaseClass, ...Mixins) {
      return Mixins.reduce(
        (accumulator, current) => current(accumulator),
        BaseClass
      )
    }
    class SubClass extends mix(Base, FooMixin, BarMixin, BazMixin) {}
    
    const sub = new SubClass()
    sub.foo() // bar-foo 注意这里foo被bar覆盖了
    sub.bar() // bar
    sub.baz() // baz
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    上次更新: 2021/10/10, 18:41:52
    第7章 迭代器与生成器
    第9章 代理与反射

    ← 第7章 迭代器与生成器 第9章 代理与反射→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×