• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-04-12 19:20 Aet 隐藏边栏 |   抢沙发  2 
文章评分 1 次,平均分 5.0

Class 基本语法

概述

  1. 在日常开发中,我们经常需要创建许多相同类型的对象,例如用户(users)、商品(goods)或者任何其他东西
    1. new function 可以帮助我们实现这种需求
  2. 但在现代 JavaScript 中,还有一个更高级的“类(class)”构造方式,它引入许多非常棒的新功能,这些功能对于面向对象编程很有用

语法

  1. 基本语法

  1. 然后使用 new MyClass() 来创建具有上述列出的所有方法的新对象
    1. new 会自动调用 constructor() 方法,因此我们可以在 constructor() 中初始化对象
  2. new User("John") 被调用:
    1. 一个新对象被创建
    2. constructor 使用给定的参数运行,并将其赋值给 this.name

什么是 class

  1. JavaScript 中,类是一种函数

  1. class User {...} 构造实际上做了如下的事儿:
    1. 创建一个名为 User 的函数,该函数成为类声明的结果
      该函数的代码来自于 constructor 方法(如果我们不编写这种方法,那么它就被假定为空)
    2. 存储类中的方法,例如 User.prototype 中的 sayHi
  2. new User 对象被创建后,当我们调用其方法时,它会从原型中获取对应的方法

不仅仅是语法糖

  1. 人们常说 class 是一个语法糖(旨在使内容更易阅读,但不引入任何新内容的语法)
    1. 因为我们实际上可以在不使用 class 的情况下声明相同的内容:
    2. 这个定义的结果与使用类得到的结果基本相同
    3. 因此,这确实是将 class 视为一种定义构造器及其原型方法的语法糖的理由

  1. 尽管,它们之间存在着重大差异:
    1. 首先,通过 class 创建的函数具有特殊的内部属性标记 [[IsClassConstructor]]: true
      因此,它与手动创建并不完全相同
    2. 类方法不可枚举
      类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false
    3. 类总是使用 use strict
      在类构造中的所有代码都将自动进入严格模式

类表达式

  1. 就像函数一样,类可以在另外一个表达式中被定义,被传递,被返回,被赋值等

  1. 类似于命名函数表达式(Named Function Expressions),类表达式可能也应该有一个名字
    1. 如果类表达式有名字,那么该名字仅在类内部可见:

  1. 我们甚至可以动态地“按需”创建类,就像这样:

Getters/setters

  1. 就像对象字面量,类可能包括 getters/setters,计算属性(computed properties)等
  2. 这是一个使用 get/set 实现 user.name 的示例:
    1. 从技术上来讲,这样的类声明可以通过在 User.prototype 中创建 getterssetters 来实现

计算属性名称 […]

  1. 这里有一个使用中括号 [...] 的计算方法名称示例:

Class字段

  1. “类字段”是一种允许添加任何属性的语法
    1. 例如,让我们在 class User 中添加一个 name 属性:

  1. 类字段的重要区别在于,它们会被挂在实例对象上,而非 User.prototype 上:

  1. 也可以在赋值时使用更复杂的表达式和函数调用:

  1. 使用类字段制作绑定方法
    1. JavaScript 中的函数具有动态的 this。它取决于调用上下文
    2. 因此,如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则 this 将不再是对其对象的引用
    3. 例如,此代码将显示 undefined
    4. 这个问题被称为“丢失 this

  1. 有两种可以修复它的方式:
    1. 传递一个包装函数,例如 setTimeout(() => button.click(), 1000)
    2. 将方法绑定到对象,例如在 constructor
  2. 类字段提供了另一种非常优雅的语法:
    1. 类字段 click = () => {...} 是基于每一个对象被创建的,在这里对于每一个 Button 对象都有一个独立的方法,在内部都有一个指向此对象的 this
    2. 我们可以把 button.click 传递到任何地方,而且 this 的值总是正确的
    3. 在浏览器环境中,它对于进行事件监听尤为有用

类继承

概述

  1. 类继承是一个类扩展另一个类的一种方式
  2. 因此,我们可以在现有功能之上创建新功能

extends” 关键字

  1. 假设我们有 class Animal

  1. 然后我们想创建另一个 class Rabbit
    1. 因为 rabbitanimal,所以 class Rabbit 应该是基于 class Animal 的,可以访问 animal 的方法,以便 rabbit 可以做“一般”动物可以做的事儿
    2. 在内部,关键字 extends 使用了很好的旧的原型机制进行工作
    3. 它将 Rabbit.prototype.[[Prototype]] 设置为 Animal.prototype
    4. 所以,如果在 Rabbit.prototype 中找不到一个方法,JavaScript 就会从 Animal.prototype 中获取该方法

注意1

  1. extends 后允许任意表达式
    1. 类语法不仅允许指定一个类,在 extends 后可以指定任意表达式
    2. 例如,一个生成父类的函数调用:

重写方法

  1. 默认情况下,所有未在 class Rabbit 中指定的方法均从 class Animal 中直接获取
  2. 但是如果我们在 Rabbit 中指定了我们自己的方法,例如 stop(),那么将会使用它:

  1. 然而通常,我们不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能
    1. 我们在我们的方法中做一些事儿,但是在它之前或之后或在过程中会调用父类方法
    2. Class 为此提供了 "super" 关键字
    3. 执行 super.method(...) 来调用一个父类方法
    4. 执行 super(...) 来调用一个父类 constructor(只能在我们的 constructor 中)

注意2

  1. 箭头函数没有 super
    1. 如果被访问,它会从外部函数获取。例如:

  1. 箭头函数中的 superstop() 中的是一样的,所以它能按预期工作。如果我们在这里指定一个“普通”函数,那么将会抛出错误:

重写constructor

  1. 对于重写 constructor 来说,则有点棘手
    1. 到目前为止,Rabbit 还没有自己的 constructor
    2. 根据 规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor

  1. 正如我们所看到的,它调用了父类的 constructor,并传递了所有的参数。如果我们没有写自己的 constructor,就会出现这种情况
    1. 现在,我们给 Rabbit 添加一个自定义的 constructor
    2. 得到了一个报错,是什么地方出错了?
    3. 继承类的 constructor 必须调用 super(...),并且一定要在使用 this 之前调用
    4. 为什么呢

  1. JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的
    1. 派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived"
    2. 这是一个特殊的内部标签
    3. 该标签会影响它的 new 行为:
      当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this
      但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作
    4. 因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错

重写类字段

  1. 我们不仅可以重写方法,还可以重写类字段
    1. 不过,当我们在父类构造器中访问一个被重写的字段时,有一个诡异的行为,这与绝大多数其他编程语言都很不一样
    2. 这里,Rabbit 继承自 Animal,并且用它自己的值重写了 name 字段
    3. 因为 Rabbit 中没有自己的构造器,所以 Animal 的构造器被调用了
    4. 当父类构造器在派生的类中被调用时,它会使用被重写的方法。但对于类字段并非如此
    5. Rabbit 是派生类,里面没有 constructor()。正如先前所说,这相当于一个里面只有 super(...args) 的空构造器
    6. 所以,new Rabbit() 调用了 super(),因此它执行了父类构造器,并且(根据派生类规则)只有在此之后,它的类字段才被初始化
    7. 在父类构造器被执行的时候,Rabbit 还没有自己的类字段,这就是为什么 Animal 类字段被使用了

  1. 这里为什么会有这样的区别呢?
    1. 实际上,原因在于字段初始化的顺序
  2. 类字段是这样初始化的:
    1. 对于基类(还未继承任何东西的那种),在构造函数调用前初始化
    2. 对于派生类,在 super() 后立刻初始化

深入:内部探究和 [[HomeObject]]

  1. 在下面的例子中,rabbit.__proto__ = animal。现在让我们尝试一下:在 rabbit.eat() 我们将会使用 this.__proto__ 调用 animal.eat()
    1. (*) 这一行,我们从原型(animal)中获取 eat,并在当前对象的上下文中调用它
    2. 注意,.call(this) 在这里非常重要,因为简单的调用 this.__proto__.eat() 将在原型的上下文中执行 eat,而非当前对象
    3. 代码确实按照了期望运行:我们获得了正确的 alert

  1. 现在,让我们在原型链上再添加一个对象。我们将看到这件事是如何被打破的:
    1. 代码无法再运行了
    2. (*)(**) 这两行中,this 的值都是当前对象(longEar
    3. 这是至关重要的一点:所有的对象方法都将当前对象作为 this,而非原型或其他什么东西
    4. 因此,在 (*)(**) 这两行中,this.__proto__ 的值是完全相同的:都是 rabbit。它们俩都调用的是 rabbit.eat,它们在不停地循环调用自己,而不是在原型链上向上寻找方法

[[HomeObject]]

  1. 为了提供解决方法,JavaScript 为函数添加了一个特殊的内部属性:[[HomeObject]]
  2. 当一个函数被定义为类或者对象方法时,它的 [[HomeObject]] 属性就成为了该对象
  3. 然后 super 使用它来解析(resolve)父原型及其方法

方法并不是“自由”的

  1. 正如我们之前所知道的,函数通常都是“自由”的,并没有绑定到 JavaScript 中的对象
    1. 正因如此,它们可以在对象之间复制,并用另外一个 this 调用它
  2. [[HomeObject]] 的存在违反了这个原则,因为方法记住了它们的对象
    1. [[HomeObject]] 不能被更改,所以这个绑定是永久的
  3. JavaScript 语言中 [[HomeObject]] 仅被用于 super
    1. 所以,如果一个方法不使用 super,那么我们仍然可以视它为自由的并且可在对象之间复制
    2. 但是用了 super 再这样做可能就会出错
  4. 下面是复制后错误的 super 结果的示例:
    1. 调用 tree.sayHi() 显示 “I’m an animal”。这绝对是错误的
    2. 原因很简单:
    3. (*) 行,tree.sayHi 方法是从 rabbit 复制而来。也许我们只是想避免重复代码?
    4. 它的 [[HomeObject]]rabbit,因为它是在 rabbit 中创建的。没有办法修改 [[HomeObject]]
    5. tree.sayHi() 内具有 super.sayHi()。它从 rabbit 中上溯,然后从 animal 中获取方法

方法,不是函数属性

  1. [[HomeObject]] 是为类和普通对象中的方法定义的
    1. 但是对于对象而言,方法必须确切指定为 method(),而不是 "method: function()"
    2. 这个差别对我们来说可能不重要,但是对 JavaScript 来说却非常重要
  2. 在下面的例子中,使用非方法(non-method)语法进行了比较。未设置 [[HomeObject]] 属性,并且继承无效:

静态属性和静态方法

概述

  1. 我们还可以为整个类分配一个方法。这样的方法被称为 静态的(static
  2. 在一个类的声明中,它们以 static 关键字开头,如下所示:

  1. 这实际上跟直接将其作为属性赋值的作用相同:

  1. User.staticMethod() 调用中的 this 的值是类构造器 User 自身(“点符号前面的对象”规则)
    1. 通常,静态方法用于实现属于整个类,但不属于该类任何特定对象的函数
    2. 例如,我们有对象 Article,并且需要一个方法来比较它们
    3. 通常的解决方案就是添加 Article.compare 静态方法:

  1. 另一个例子是所谓的“工厂”方法
    1. 比如说,我们需要通过多种方式来创建一篇文章:
    2. 通过用给定的参数来创建(titledate 等)。
    3. 使用今天的日期来创建一个空的文章。
    4. 其它方法
  2. 第一种方法我们可以通过 constructor 来实现。对于第二种方式,我们可以创建类的一个静态方法来实现
    1. 现在,每当我们需要创建一个今天的文章时,我们就可以调用 Article.createTodays()
      再说明一次,它不是一个文章的方法,而是整个 class 的方法

注意

  1. 静态方法不适用于单个对象
    1. 静态方法可以在类上调用,而不是在单个对象上

静态属性

  1. 这是一个最近添加到 JavaScript 的特性
  2. 静态的属性也是可能的,它们看起来就像常规的类属性,但前面加有 static
    1. 这等同于直接给 Article 赋值:

继承静态属性和方法

  1. 静态属性和方法是可被继承的
  2. 例如,下面这段代码中的 Animal.compareAnimal.planet 是可被继承的,可以通过 Rabbit.compareRabbit.planet 来访问:

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享