ES6中的类知识梳理

发现项目代码中没有人用到类,有些可惜了ES6新增的这个属性,所以我准备梳理出一篇文档,希望自己和小伙伴们不让ES6提供给我们的方法蒙灰。

ES6(ECMAScript 6)引入了类的概念,这是一种基于原型的继承的语法糖。

ES6的类实际上是基于JavaScript原型链的继承的封装。

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。新的class写法是为了让对象原型的写法更加清晰,更新面向编程的写法。

什么是ES6中的类

ES6中的类是一种语法结构,用于创建对象的模板。

它是一种基于原型的继承方式,使用关键字class进行定义。

ES6的类允许我们定义构造函数、方法和访问器,以及通过extends关键字实现继承。

它提供了一种更简洁、更易于理解的方式来创建和管理对象,同时也支持面向对象编程的特性,如封装、继承和多态。

举个栗子:

javascript 复制代码
class MyClass {  
  constructor(name) {  
    this.name = name;  
  }  
  
  sayHello() {  
    console.log(`Hello, my name is ${this.name}`);  
  }  
}  
  
let instance = new MyClass('Feng');  

instance.sayHello();  // 输出: "Hello, my name is Feng"

在这个栗子中,MyClass是一个类,它有一个构造函数constructor和一个方法sayHello

通过使用new关键字创建类的新实例,然后可以调用这个实例的方法。

ES6的类也支持继承,这使得我们可以创建一个类,然后从另一个类继承其属性和方法。

我们可以通过extends关键字实现这个功能,如下所示:

scala 复制代码
class MyChildClass extends MyClass {  
  sayGoodbye() {  
    console.log(`Goodbye, my name is ${this.name}`);  
  }  
}  
  
let childInstance = new MyChildClass('Feng');  

childInstance.sayHello();  // 输出: "Hello, my name is Feng"  

childInstance.sayGoodbye();  // 输出: "Goodbye, my name is Feng"

在这个例子中,MyChildClass继承自MyClass,所以MyChildClass的实例有sayHellosayGoodbye两个方法。

这里看不明白没关系,下面会根据这个栗子仔细讲解。

详细解释下ES6中的类

首先,我们再看一遍上面的代码:

javascript 复制代码
class MyClass {  
  constructor(name) {  
    this.name = name;  
  }  
  
  sayHello() {  
    console.log(`Hello, my name is ${this.name}`);  
  }  
}  
  
let instance = new MyClass('Feng');  

instance.sayHello();  // 输出: "Hello, my name is Feng"

类的constructor函数

在ES6的类中,constructor是一个特殊的方法,用于初始化新创建的对象。

它是在使用new关键字创建类的新实例时被自动调用的。

constructor方法通常用于设置对象的初始状态,例如为对象的属性赋值。在constructor方法中,我们可以访问this关键字,它引用当前正在创建的对象实例。

在上面的例子中,MyClassconstructor接受一个参数name,并将其赋值给对象的name属性。

这样,当创建MyClass的新实例时,就可以通过constructor为对象的属性设置初始值。

因为new MyClass('Feng')时将Feng这个值传给了constructor中的name参数,然后再赋值到this.name

类的方法

sayHelloMyClass 类中的一个方法。这个方法在被调用时,会打印出一条带有对象名字 "Hello, my name is [对象名字]" 的消息。

在上面的例子中,sayHello 方法被用于 instance 对象,所以会打印出 "Hello, my name is Feng"。

具体来说,当创建 MyClass 的一个实例(在这里,实例名为 instance),就可以调用这个实例的 sayHello 方法。

这个方法会通过 console.log 打印出一条消息,消息的内容是由 this.name(即这个实例的 name 属性)决定的。

这种在类中定义方法的方式,让我们可以在一组相关的对象(这里是 MyClass 的实例)之间共享行为(这里是 sayHello 方法)。

这句话可能有小伙伴也听不明白,意思是通过在类中定义方法(例如 sayHello),我们可以让该类的所有实例共享相同的行为。

也就是说,不论你创建多少个 MyClass 的实例,它们都可以调用 sayHello 方法,并且这个方法的行为(即打印出 "Hello, my name is [对象名字]")是一样的。

这种方式让我们可以更方便地管理和组织代码,因为我们可以把相关的行为(方法)封装在一个类里,而不是在每个对象实例中单独定义。这也使得代码更具可读性和可维护性。

类的new

ini 复制代码
let instance = new MyClass('Feng');  

可以看到我们是new 了一个 MyClass

为什么一定要用new呢,不能直接这样吗:

ini 复制代码
let instance = MyClass('Feng');  

在ES6中使用new关键字来创建类的实例是因为new关键字会执行以下操作:

  1. 创建一个新的空对象。
  2. 将这个新对象的原型指向构造函数的prototype属性。
  3. 将构造函数的this关键字绑定到新创建的对象上,并执行构造函数的代码(添加属性和方法)。
  4. 如果构造函数返回一个对象,则返回这个对象;否则,返回新创建的对象。

通过new关键字,可以确保我们创建一个新的独立对象,并将该对象与类进行关联,以便可以访问类的方法和属性。

如果不使用new关键字,则无法创建类的实例,并且无法正确调用类的方法和访问类的属性。

类的实例

ES6中类的实例是通过使用new关键字创建的对象,这些对象基于类的定义和构造函数进行初始化。

类的实例可以访问类中的方法和属性,并且可以根据需要进行自定义操作。

在上述示例中,通过new MyClass('Feng')创建了一个MyClass的实例,并将其分配给变量instance

然后,可以通过instance访问该实例的属性和方法,例如instance.nameinstance.sayHello()

类的原型

类的原型是JavaScript中对象继承机制的核心,它允许对象从类的原型对象中继承属性和方法。

JavaScript中对象继承机制的核心是原型链

原型链是JavaScript中实现对象继承的主要方式,它通过对象内部的[[Prototype]]属性形成了一个链式结构。

当试图访问一个对象的属性或方法时,如果该对象自身不存在这个属性或方法,JavaScript就会沿着[[Prototype]]链向上查找,直到找到这个属性或方法,或者达到链的末尾。

这样,对象就可以从它的原型对象中继承属性和方法,实现了代码的复用和抽象。

解释了下还是很难理解,举个栗子:

类的所有方法都定义在类的prototype属性上面。

javascript 复制代码
class Person{
  constructor() {
    // ...
  }
  toString() {
    // ...
  }
  toValue() {
    // ...
  }
}

// 等同于

Person.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

也就是说:其实在类的实例上面调用方法,其实就是调用原型上的方。

ini 复制代码
class B {}
const b = new B();

b.constructor === B.prototype.constructor // true

prototype对象的constructor属性,直接指向"类"的本身,这与 ES5 的行为是一致的。

注意:toString()方法是Person类内部定义的方法,它是不可枚举的。另外,类的内部所有定义的方法,都是不可枚举的。

什么叫不可枚举的?

在JavaScript中,不可枚举的属性是指那些不能被for...in循环遍历到的属性。这些属性的enumerable值为false,因此它们不会被列举出来。

当一个类实例化一个对象时,这个对象会包含一个指向类原型的内部链接,即[[Prototype]]属性。

当试图访问对象的某个属性或方法时,如果该对象自身不存在这个属性或方法,JavaScript引擎就会沿着[[Prototype]]链向上查找,直到找到这个属性或方法,或者达到链的末尾。

类的取值函数(getter)和存值函数(setter)

类的取值函数(getter)和存值函数(setter)是ES6中类的特性之一,它们允许我们在类中定义属性的读取和设置行为。

取值函数(getter)是一种特殊的方法,它会在试图获取某个属性的值时自动调用。通过getter函数,可以对属性的读取进行自定义操作,例如进行数据校验、转换或计算等。

存值函数(setter)也是一种特殊的方法,它会在试图修改某个属性的值时自动调用。通过setter函数,可以对属性的设置进行自定义操作,例如进行数据验证、更新相关状态或触发回调函数等。

以下是一个使用getter和setter的栗子:

javascript 复制代码
class Person {  
  constructor(name) {  
    this._name = name;  
  }  
  
  get name() {  
    return this._name; // getter函数,返回_name属性的值  
  }  
  
  set name(value) {  
    if (typeof value !== 'string') {  
      throw new Error('Name must be a string'); // setter函数,对value进行验证  
    }  
    this._name = value;  
  }  
}

const person = new Person('Feng'); // 创建一个Person对象  
console.log(person.name); // 输出:Feng
  
person.name = 'Bob'; // 修改name属性  
console.log(person.name); // 输出:Bob  
  
person.name = 30; // 修改name属性
会报错  

在上述示例中,Person类定义了一个name属性,并使用getter和setter函数对该属性进行读取和设置。

通过getter函数,可以直接访问name属性,而setter函数则会在修改name属性时进行验证,确保输入的值是一个字符串。

类的表达式

类的表达式是JavaScript中定义类的一种方式,它允许使用简洁的语法来创建类。类的表达式可以包含类名、构造函数、方法和属性等类的组成部分。

以下是一个使用类的表达式的栗子:

javascript 复制代码
const Person = class {  
  constructor(name) {  
    this.name = name;  
  }  
  
  sayHello() {  
    console.log(`Hello, my name is ${this.name}`);  
  }  
};  
  
const person = new Person('Feng'); // 创建一个Person对象  
person.sayHello(); // 输出:Hello, my name is Feng

在上述示例中,使用类的表达式定义了一个名为Person的类。这个类包含一个构造函数和一个sayHello方法。然后,通过new关键字创建了一个Person对象,并调用了对象的sayHello方法。

类的表达式提供了一种简洁的方式来定义类,可以在需要动态创建类或将类作为参数传递等场景下使用。

ES6中的静态属性、静态方法和静态块

在ES6中,静态属性、静态方法和静态块是类的特性之一。

允许在类中定义与类本身相关,而不是与类的实例相关的属性和方法。

静态属性是指在类中定义的属性,它们属于类本身而不是类的实例。可以使用static关键字来声明静态属性。静态属性可以通过类名直接访问,而不需要创建类的实例。

以下是一个使用静态属性的栗子:

arduino 复制代码
class MyClass {  
  static myStaticProperty = 'Hello, world!';  
}  
  
console.log(MyClass.myStaticProperty); // 输出:Hello, world!

静态方法是指在类中定义的方法,它们也可以在不创建类的实例的情况下通过类名直接调用。可以使用static关键字来声明静态方法。静态方法可以访问静态属性和其他静态方法。

以下是一个使用静态方法的栗子:

javascript 复制代码
class MyClass {  
  static myStaticMethod() {  
    console.log('This is a static method.');  
  }  
}  
  
MyClass.myStaticMethod(); // 输出:This is a static method.

静态块是在类中定义的特殊块,它们在类被加载时执行,而不是在创建类的实例时执行。静态块用于执行一些类的初始化操作。

以下是一个使用静态块的栗子:

vbnet 复制代码
class MyClass {  
  static {  
    console.log('This is a static block.');  
  }  
}

类的私有属性和方法

类的私有方法和私有属性是类的特性之一,它们允许在类中定义一些只能在类内部访问的方法和属性,而不能从类的外部直接访问。

私有属性是指在类的内部定义的属性,它们的名称通常以下划线(_)开头。私有属性只能在类的内部访问,不能从类的外部直接访问。以下是一个使用私有属性的栗子:

javascript 复制代码
class MyClass {  
  constructor() {  
    this._myPrivateProperty = 'Hello, world!';  
  }  
  
  getMyPrivateProperty() {  
    return this._myPrivateProperty;  
  }  
}  
  
const myInstance = new MyClass();  
console.log(myInstance.getMyPrivateProperty()); // 输出:Hello, world!  
console.log(myInstance._myPrivateProperty); // 输出:undefined

需要注意的是,在JavaScript中,并没有真正的私有方法和私有属性,因为它们仍然可以通过一些技巧从类的外部访问。

但是,使用下划线命名约定可以给人一种私有的感觉,并且可以避免一些不必要的干扰和误操作。

比如:

javascript 复制代码
class MyClass {  
  constructor() {  
    this.__myPrivateProperty = 'Hello, world!';  
  }  
  
  __myPrivateMethod() {  
    console.log('This is a private method.');  
  }  
}  
  
const myInstance = new MyClass();  
  
// 通过原型链访问私有方法  
MyClass.prototype.__myPrivateMethod.call(myInstance); // 输出:This is a private 
  
// 通过改变原型链上的属性访问私有属性  
MyClass.prototype.__myPrivateProperty = 'Hello, everyone!';  

console.log(myInstance.__myPrivateProperty); // 输出:Hello, everyone!

在上述示例中,我们通过原型链来访问私有方法和私有属性。

通过调用构造函数创建的实例会继承构造函数的原型对象,因此我们可以通过修改原型对象上的属性或方法来访问私有成员。

但是,这种方法并不是推荐的做法,因为它破坏了类的封装性,并可能引起一些不可预见的问题。

新写法(ES2022)

ES2022正式为class添加了私有属性和私有方法,是只能在类的内部访问的方法和属性,外部不能访问,不可以直接通过 Class 实例来引用。

在方法或者是属性名之前使用#表示。

arduino 复制代码
class Person{
  #name= '黑色的枫'; // #personName就是私有属性,只能在类的内部使用(this.#personName)。如果在类的外部使用,就会报错。
  get value() {
    return this.#name;
  }
}
const getPersonName= new Person();
gName.#name// 报错
gName.#name= 'Feng' // 报错

私有方法只能内部调用,在外部调用就会报错。

类的实例对象销毁

在ES6中,销毁一个类的实例对象需要手动进行。通常可以通过在类中定义一个destroy方法或自定义方法来实现。该方法可以执行一些清理操作,例如取消事件监听、释放资源等,并最终将对象置为null以便垃圾回收。例如:

javascript 复制代码
class Person {  
  constructor(name, age) {  
    this.name = name;  
    this.age = age;  
  }  
  
  destroy() {  
    // 执行清理操作...  
    this = null; // 将对象置为null以便垃圾回收  
  }  
}  
  
const person = new Person('Feng', 20);  
person.destroy(); // 销毁实例对象

在上述代码中,destroy方法用于销毁Person类的实例对象。

在销毁过程中,可以执行一些必要的清理操作,最后将对象置为null以便JavaScript的垃圾回收机制能够回收其占用的内存空间。

类的继承

ES6中类的继承是通过extends关键字和super关键字实现的。

extends用于创建一个新的类,并从已有的类中继承属性和方法。

super用于调用父类的构造函数或方法。

以下是一个ES6中类的继承的例子:

javascript 复制代码
class Parent {  
  constructor() {  
    console.log('Parent constructor');  
  }  
  
  greet() {  
    console.log('Hello from Parent');  
  }  
}  
  
class Child extends Parent {  
  constructor() {  
    super(); // 调用父类的构造函数  
    console.log('Child constructor');  
  }  
  
  greet() {  
    super.greet(); // 调用父类的方法  
    console.log('Hello from Child');  
  }  
}  
  
const child = new Child();  
child.greet();

在这个例子中,Child类继承了Parent类,super关键字用于调用父类的构造函数和方法。

当创建Child类的实例并调用greet方法时,首先会调用父类的greet方法,然后再调用子类自己的greet方法。

类的核心特性

类的继承、封装和多态是面向对象编程的三个核心特性。

继承 允许我们创建一个新的类,并从已有的类中继承属性和方法。这样可以避免代码重复,提高代码的可重用性。在ES6中,可以使用extends关键字实现类的继承。

封装 是将类的属性和方法封装在一个独立的单元中,隐藏内部的实现细节,只暴露必要的接口给外部使用。这样可以提高代码的模块化和可维护性。在ES6中,可以使用访问控制修饰符(如public、private、protected)来实现封装性。

多态 是指在不同的类中实现相同的方法名,但具有不同的实现和行为。多态性允许我们使用相同的代码来处理不同类型的对象,提高代码的灵活性和可扩展性。在ES6中,可以通过方法的重写和重载来实现多态性。

ES6的类对比ES5的类

以下是ES6和ES5的类对比的表格:

ES6类

ES5类

语法

使用class关键字定义类

使用函数构造函数和原型链实现类的功能

继承

使用extends关键字实现类的继承

通过原型链和借用构造函数实现继承

属性和方法

可以在类中直接定义属性和方法

将属性和方法挂载在类的原型对象上

静态属性和方法

可以在类中定义静态属性和方法

将静态属性和方法定义在类的构造函数上

举个栗子:

ES6:

javascript 复制代码
class Person {  
  constructor(name) {  
    this.name = name;  
  }  
  
  sayHello() {  
    console.log(`Hello, my name is ${this.name}`);  
  }  
  
  static getGreeting() {  
    return 'Hello, everyone!';  
  }  
}  
  
const person = new Person('Feng');  
person.sayHello(); // 输出:Hello, my name is Feng  
console.log(Person.getGreeting()); // 输出:Hello, everyone!

ES5:

javascript 复制代码
function Person(name) {  
  this.name = name;  
}  
  
Person.prototype.sayHello = function() {  
  console.log('Hello, my name is ' + this.name);  
};  
  
Person.getGreeting = function() {  
  return 'Hello, everyone!';  
};  
  
const person = new Person('Feng');  
person.sayHello(); // 输出:Hello, my name is Feng  
console.log(Person.getGreeting()); // 输出:Hello, everyone!

ES6中的类和其他语言的类有什么区别

ES6中的类和其他编程语言的类在概念上是类似的,都是用于创建对象的模板。

然而,具体的实现细节和使用方式可能会有所不同。以下是ES6中的类和其他语言的一些区别:

  1. 基于原型的继承:JavaScript中的类是基于原型的继承,与其他基于类的语言有所不同。这意味着ES6的类实际上是通过原型链来实现继承的。
  2. 语法糖:ES6的类实际上是一种语法糖,它提供了一种更简洁、更易于理解的方式来创建和管理对象。与其他语言相比,ES6的类语法更加简洁明了。
  3. 动态性:JavaScript是一种动态类型语言,因此ES6的类也具有很高的动态性。可以在运行时动态添加或修改类的属性和方法。

总之,虽然ES6的类和其他编程语言的类在概念上类似,但在具体实现和使用上可能会有一些差异。

类与构造函数之间的关系

类可以看作是构造函数的另一种写法。在JavaScript ES6中,类本质上就是构造函数的一种语法糖,它们的作用都是为了创建对象。

使用类语法,可以更清晰、更简洁地表达对象的结构和行为。

类和构造函数的主要区别在于语法和语义。类的语法更加直观和清晰,它使用class关键字来定义类,使用constructor方法定义构造函数,使用method定义方法。而构造函数则是使用function关键字来定义的。

但是,无论使用哪种方式,最终的目的都是为了创建具有特定属性和行为的对象。

使用构造函数定义对象:

ini 复制代码
function Person(name, age) {  
  this.name = name;  
  this.age = age;  
  
  this.sayHello = function() {  
    console.log("Hello, my name is " + this.name);  
  };  
}  
  
const person1 = new Person("Feng", 25);  
person1.sayHello(); // 输出:Hello, my name is Feng

**使用类定义对象:

**

javascript 复制代码
class Person {  
  constructor(name, age) {  
    this.name = name;  
    this.age = age;  
  }  
  
  sayHello() {  
    console.log("Hello, my name is " + this.name);  
  }  
}  
  
const person2 = new Person("Father", 30);  
person2.sayHello(); // 输出:Hello, my name is Father

在上面的代码中,使用构造函数和类都可以定义一个名为Person的对象,具有相同的属性和方法。

但是类的语法更加简洁和清晰,它使用constructor关键字定义构造函数,并使用方法定义对象的行为。

ES6中类的优势与劣势

ES6中的类的优势主要包括:

  1. 简洁明了的语法:ES6的类语法更加简洁明了,易于理解和使用,提高了代码的可读性和可维护性。
  2. 面向对象编程:类提供了一种面向对象编程的方式,可以更好地组织和管理代码,实现更高级别的抽象和代码复用。
  3. 继承和多态:ES6的类支持继承和多态等面向对象编程的特性,使得代码更加灵活和可扩展。

而ES6中的类的劣势主要包括:

  1. 语法糖的本质:ES6的类实际上是一种语法糖,其底层仍然是基于原型的继承,可能会掩盖JavaScript继承的本质。

  2. 强制使用new关键字:类的实例化必须使用new关键字,否则会报错,这可能会限制一些灵活性的使用。

  3. 私有属性的不私有性: 私有属性可以被外部访问到,可能引起一些不可预见的问题。

ES6中类的使用场景

在前端开发中,以下场景应该考虑使用类:

  1. 创建具有相同属性和行为的对象:当我们需要创建一组具有相同属性和行为的对象时,可以使用类作为模板来定义这些对象的结构和行为。通过类的实例化,你可以方便地创建具有相同特征的对象。
  2. 面向对象编程:类提供了一种面向对象编程的方式,通过定义类和对象,我们可以更好地组织和管理代码。我们可以使用类的继承、封装和多态等特性来实现更高级别的抽象和代码复用。
  3. 复用代码:类允许我们定义可复用的代码块,避免在多个地方重复编写相同的逻辑。我们可以将公共的方法和属性放在一个类中,然后在需要的地方通过继承或实例化来使用这些方法和属性。

总的来说,当我们需要在前端开发中创建具有相同特征和行为的对象,并希望更好地组织和管理代码时,应该考虑使用类。

源码示例

tapable

小伙伴们应该都知道tapable,是一个类似于 Node.js 中的 EventEmitter的库,但更专注于自定义事件的触发和处理。webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在。

其中的同步钩子:

ini 复制代码
class SyncHook {  
  constructor(args) {  
    this.taps = [];  
    this._args = args;  
  }  
  
  tap(name, fn) {  
    this.taps.push({ name, fn });  
    return this;  
  }  
  
  call(...args) {  
    const tapArgs = [...this._args, ...args];  
    let index = 0;  
    const taps = this.taps;  
    while (index < taps.length) {  
      const { fn } = taps[index];  
      fn(...tapArgs);  
      index++;  
    }  
  }  
}

上述源码定义了一个 SyncHook 类,它具有以下功能:

  1. constructor(args):构造函数用于初始化实例,接收一个参数 args,用于存储传递给钩子的参数。
  2. tap(name, fn):该方法用于注册钩子函数。它接收两个参数,name 表示钩子名称,fn 是实际的钩子函数。注册的钩子函数会被存储在一个数组 taps 中。
  3. call(...args):该方法用于触发钩子函数。它接收任意数量的参数,并将这些参数与构造函数中传递的参数合并为一个数组 tapArgs,然后依次调用注册的钩子函数,并将 tapArgs 作为参数传递给它们。

以最简单的 SyncHook 为例:

javascript 复制代码
const { SyncHook } = require('tapable');
const hook = new SyncHook(['name']);
hook.tap('hello', (name) => {
    console.log(`hello ${name}`);
});
hook.tap('hello again', (name) => {
    console.log(`hello ${name}, again`);
});

hook.call('Feng');
// hello Feng
// hello Feng, again

可以看到当我们执行 hook.call('Feng') 时会依次执行前面 hook.tap(name, callback) 中的回调函数。通过 SyncHook 创建同步钩子,使用 tap 注册回调,再调用 call 来触发。这是 tapable 提供的多种钩子中比较简单的一种,通过 EventEmitter 也能轻松的实现这种效果。

Vue

Vue源码中使用类来定义Vue实例,并通过引入初始化混合函数来扩展Vue类的功能。

这只是Vue源码中使用类的一个简单示例,实际上Vue的源码中还有更多复杂和高级的使用类的场景。

javascript 复制代码
// 定义Vue类  
class Vue {  
  constructor(options) {  
    this._init(options);  
  }  
  
  _init(options) {  
    // 初始化操作  
  }  
}  
  
// 引入初始化混合函数  
import { initMixin } from './init';  
  
// 将初始化混合函数应用到Vue类上  
initMixin(Vue);  
  
export default Vue;

可能有小伙伴好奇initMixin是如何扩展Vue类的,我们再看下initMixin的源码。

scss 复制代码
export function initMixin(Vue) {  
  Vue.prototype._init = function (options) {  
    const vm = this;  
  
    // a uid  
    vm._uid = uid++;  
  
    // a flag to avoid this being observed  
    vm._isVue = true;  
  
    // merge options  
    if (options && options._isComponent) {  
      // optimize internal component instantiation  
      // since dynamic options merging is pretty slow, and none of the  
      // internal component options needs special treatment.  
      initInternalComponent(vm, options);  
    } else {  
      vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm);  
    }  
  
    /* istanbul ignore else */  
    if (process.env.NODE_ENV !== 'production') {  
      initProxy(vm);  
    } else {  
      vm._renderProxy = vm;  
    }  
  
    // expose real self  
    vm._self = vm;  
    initLifecycle(vm);  
    initEvents(vm);  
    callHook(vm, 'beforeCreate');  
    initState(vm);  
    callHook(vm, 'created');  
    initRender(vm);  
  };  
}

这段代码定义了一个名为initMixin的函数,该函数接受一个Vue类作为参数。

函数内部为Vue类的原型对象定义了一个_init方法,用于初始化Vue实例。

该方法执行了一系列初始化操作,包括合并选项、初始化生命周期、初始化事件系统、调用beforeCreate钩子、初始化状态、调用created钩子和初始化渲染函数等。

这段代码是Vue实例化过程中非常重要的一部分。

肯定有小伙伴很好奇,vue的类中不是有_init 吗,为什么initMixin又定义了_init。

在Vue的源码中,_init方法确实在Vue的类中定义了,而initMixin函数又定义了一个_init方法。

这是因为initMixin函数的作用是将一些公共的初始化逻辑抽取出来,形成一个混入对象,然后将其与Vue类进行混合,以扩展Vue类的功能。

通过这种方式,Vue的源码实现了代码的模块化和可维护性,将不同功能的初始化逻辑分离开来,使得代码更加清晰和易于管理。同时,使用混入的方式也使得这些初始化逻辑可以更加方便地被复用和扩展。

因此,虽然在Vue的类中已经有了_init方法,但initMixin函数仍然需要定义一个_init方法,以便将其混入到Vue类中并扩展其功能。

React

再举一个React的栗子:

kotlin 复制代码
class ReactComponent {  
  constructor(props, context, updater) {  
    this.props = props;  
    this.context = context;  
    this.refs = emptyObject;  
    this.updater = updater || ReactNoopUpdateQueue;  
  }  
  
  isReactComponent() {  
    return true;  
  }  
  
  setState(partialState, callback, queueName) {  
    this.updater.enqueueSetState(this, partialState, callback, queueName);  
  }  
  
  forceUpdate(callback) {  
    this.updater.enqueueForceUpdate(this);  
  }  
}

以上代码片段展示了React源码中如何定义一个简单的React组件类ReactComponent,其中包括构造函数、isReactComponent方法以及setStateforceUpdate等方法。

这个类为React组件提供了基本的属性和方法,以实现React框架的核心功能。

ReactComponent是一个抽象基类,不应该被直接使用。

相反,我们应该使用React.ComponentReact.PureComponent来定义我们的React组件。这两个类都继承自ReactComponent,并添加了一些额外的方法和生命周期钩子。

scala 复制代码
// ReactComponent是一个抽象基类,不应该被直接使用  
class ReactComponent {  
  constructor(props) {  
    this.props = props;  
  }  
  
  // 这是一个抽象方法,需要在子类中实现  
  render() {  
    throw new Error('render method not implemented');  
  }  
}  
  
// React.Component是一个继承自ReactComponent的具体类,我们可以使用它来定义组件  
class React.Component extends ReactComponent {  
  constructor(props) {  
    super(props);  
    this.state = {};  
  }  
  
  // React.Component添加了一些额外的方法和生命周期钩子,例如setState和componentDidMount等  
  setState(newState) {  
    this.state = { ...this.state, ...newState };  
    // 在这里可以触发重新渲染等逻辑  
  }  
  
  componentDidMount() {  
    // 组件挂载后执行的逻辑  
  }  
}  
  
// 我们应该使用React.Component或React.PureComponent来定义我们的React组件  
class MyComponent extends React.Component {  
  render() {  
    return <div>Hello, world!</div>;  
  }  
}
相关推荐
沉登c11 分钟前
Javascript客户端时间与服务器时间
服务器·javascript
持久的棒棒君14 分钟前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_8572979125 分钟前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋1 小时前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者3 小时前
React 19 新特性详解
前端
小程xy3 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6323 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6323 小时前
WebGL编程指南之进入三维世界
前端·webgl
寻找09之夏4 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
非著名架构师4 小时前
js混淆的方式方法
开发语言·javascript·ecmascript