简单易懂的带你初探class和function

77 阅读15分钟

如果你点进了这篇文章,说明(大概率)你和我一样,是一个对class知之甚少的前端菜鸟。本文是我根据自己的学习笔记整理而得,我会在文章中尽量用我们菜鸟听的懂的方式来说清楚class和function的异同,与此同时,会列出相关的知识来源,供大家分析判断我的讲解是否有误,并辅助大家更好的理解相关内容,如果你嫌文字太多,不看知识来源部分也完全ok。好了,不说废话,开冲!

一些前置知识点

对象的构造函数

知识来源

constructor: A class or function that specifies the type of the object instance

new - JavaScript | MDN (mozilla.org)

The constructor property is writable, non-enumerable, and configurable.

Function: prototype - JavaScript | MDN

我们生成实例时, 用于说明该实例类型的 classfunction ,就是构造函数。比如

function Person(name) {
   this.name = name
}

const aZhen = new Person('阿珍')
const aQiang = new Person('阿强')
console.log(aZhen.constructor === Person) // true

阿珍和阿强就是实例,用于指定阿珍、阿强类型的Person函数, 就是构造函数。

对象的 [[Prototype]]

知识来源

JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null 作为其原型的对象上。

备注: 指向对象原型的属性并是 prototype。它的名字不是标准的,但实际上所有浏览器都使用 __proto__。访问对象原型的标准方法是 Object.getPrototypeOf()

对象原型 - 学习 Web 开发 | MDN

每个对象实例“天生”就有一个内置的原型对象([[Prototype]]),但是一定要注意,[[Prototype]] 并非存储在prototype字段上,现有的浏览器都 [[Prototype]] 通过__proto__字段暴露出来,但是最最最官方的方法,是通过Object.getPrototypeOf(objInstance)访问 [[Prototype]]。(你可以理解为,__proto__是浏览器厂商们自己商量,创造出来的第三方属性,而非js的原生属性)

function Person(name) {
  this.name = name
}

const aZhen = new Person('阿珍')
const aQiang = new Person('阿强')

console.log(aZhen.prototype) // undefined (实例中没有这个字段)
console.log(Object.getPrototypeOf(aZhen) === aZhen.__proto__) // true

说到 [[Prototype]],就不得不提大名鼎鼎的原型链,顾名思义,多个相关的 [[Prototype]] 构成了原型链:

当访问一个对象实例的某个属性时,首先会先在这个对象实例本身上找,如果没有找到,则会去他的 [[Prototype]] 上找,还没有找到,就再去 [[Prototype]][[Prototype]] 上去找,这样一层一层向上查找,直到找到值为null[[Prototype]] 为止。

构造函数的prototype

知识来源

The prototype data property of a Function instance is used when the function is used as a constructor with the new operator. It will become the new object's prototype.

Function: prototype - JavaScript | MDN

A function's prototype property, by default, is a plain object with one property: constructor, which is a reference to the function itself. The constructor property is writable, non-enumerable, and configurable.

Function: prototype - JavaScript | MDN

每个构造函数“天生”也有个内置的prototype属性。对象实例的 [[Prototype]],指向的就是生成该实例的构造函数的prototype属性。

console.log(Object.getPrototypeOf(aZhen) === Person.prototype); // true

函数的prototype默认是一个只有constructor属性的对象,constructor指向该构造函数本身,且不可枚举。

console.log(Person.prototype.constructor === Person) // true
知识来源

If the prototype of a function is reassigned with something other than an Object, when the function is called with new, the returned object's prototype would be Object.prototype instead. (In other words, new ignores the prototype property and constructs a plain object.)

Function: prototype - JavaScript | MDN

若我们对函数的prototype重新赋值,情况将会发生变化:

  1. 赋值后的prototype不是Object

    此时构造函数所生成对象实例的 [[Prototype]] 将不再指向构造函数的prototype,而是直接指向Object.prototype

    Person.prototype = 'string'
    // aZhen是在prototype重新赋值后生成的。
    // 若aZhen的生成发生在prototype重新赋值之前,又是另一种情况了,怕说太多怕增加文章的理解难度,本文就不再讨论了
    const aZhen = new Person('阿珍')
    console.log(Object.getPrototypeOf(aZhen) === Object.prototype)  // true
    
  2. 赋值后的prototypeObject

    此时构造函数所生成对象实例的 [[Prototype]] 仍然指向构造函数的prototype,但构造函数的prototype.constructor将不再指向构造函数本身。我们可以通过以下方法,手动对prototype.constructor重新赋值。

    Person.prototype = {
      type: 'person'
    }
    
    console.log(Object.getPrototypeOf(aZhen) === Person.prototype); // true
    console.log(Person.prototype.constructor) // function Object() { [native code] }
    
    // 手动对constructor重新赋值
    Object.defineProperty(Person.prototype, "constructor", {
        enumerable: false,
        value: Person
    })
    

new运算符

知识来源

The new operator lets developers create an instance of a user-defined object type or of one of the built-in object types that has a constructor function

new - JavaScript | MDN (mozilla.org)

When a function is called with the new keyword, the function will be used as a constructor. new will do the following things:

1. Creates a blank, plain JavaScript object. For convenience, let's call it newInstance.

2. Points newInstance's [[Prototype]] to the constructor function's prototype property, if the prototype is an Object. Otherwise, newInstance stays as a plain object with Object.prototype as its [[Prototype]].

3. Executes the constructor function with the given arguments, binding newInstance as the this context (i.e. all references to this in the constructor function now refer to newInstance).

4. If the constructor function returns a non-primitive, this return value becomes the result of the whole new expression. Otherwise, if the constructor function doesn't return anything or returns a primitive, newInstance is returned instead. (Normally constructors don't return a value, but they can choose to do so to override the normal object creation process.)

new - JavaScript | MDN (mozilla.org)

new运算符用于创建指定类型的对象实例,当使用new运算符创建对象实例时,会依次发生以下事件:

  1. 生成一个空对象O
  2. 如果构造函数的prototypeObject,O的[[Prototype]]将指向构造函数的prototype, 如果构造函数的prototype不是对象, O的[[Prototype]]将指向Object.prototype
  3. 执行构造函数,并且执行过程中构造函数内的this指向O
  4. 若构造函数返回值为引用类型, 则将其作为本次new表达式的返回值,若构造函数返回值为简单类型,或者构造函数没有返回值,生成的对象O将作为new表达式的返回值

class和funciton的对比

知识来源

Classes in JS are built on prototypes but also have some syntax and semantics that are unique to classes.

Classes - JavaScript | MDN

Classes are in fact "special functions"

Classes - JavaScript | MDN

class其实就是function和原型链的语法糖,你可以理解为开发者们觉得通过function和 [[Prototype]] 来定义类型太麻烦且可读性差,因此把这些他们觉着麻烦的东西封装起来,包装成用起来更方便、更易于理解的class。

下面我带大家看一下,要如何通过class来改写基于function的旧代码,从而帮助大家更好的理解class。

类型定义及创建实例

function

function Person(name, gender = '女') {
   this.name = name
   this.gender = gender
   this.report = () => { console.log(`我的名字叫${this.name}, 我的性别是${this.gender}`) }
}

const aZhen = new Person('阿珍', '女')
const aQiang = new Person('阿强', '男')

class

知识来源

a class can be defined in two ways: a class expression or a class declaration.

Classes - JavaScript | MDN

class有两种定义类型的方法:表达式定义和声明定义 ,对于实例属性(挂载在生成实例本身,而非挂载在构造函数上的属性)的定义,我们可以选择是/否前置声明。

知识来源

The fields can be declared with or without a default value. Fields without default values default to undefined. By declaring fields up-front, class definitions become more self-documenting, and the fields are always present, which help with optimizations.

Classes - JavaScript | MDN

虽然不对属性前置声明,也完全不影响功能的实现,但推荐大家前置声明所有属性,这样可以让我们的类型定义更加完整,增强代码的可读性和可维护性(比如可以清晰明了的看到这个类都有哪些实例属性,以及每个实例属性的默认值是什么)。

// 表达式定义
const Person = class {
  // 前置声明属性,并给定初始值
  gender = '女';
  report = () => { console.log(`我的名字叫${this.name},性别是${this.gender}`) };
  // 未给定初始值,属性默认为undefined
  name;

  constructor(name, gender) {
    this.name = name
    this.gender = gender || this.gender
  }
}

// 声明定义
class Person {
  // 和表达式定义内的代码一样
}

const aZhen = new Person('阿珍')
const aQiang = new Person('阿强','男')
知识来源

The constructor method is a special method of a class for creating and initializing an object instance of that class.

constructor - JavaScript | MDN

A constructor enables you to provide any custom initialization that must be done before any other methods can be called on an instantiated object.

If you don't provide your own constructor, then a default constructor will be supplied for you. If your class is a base class, the default constructor is empty

If your class is a derived class, the default constructor calls the parent constructor, passing along any arguments that were provided:

Classes - JavaScript | MDN

class中声明的constructor用于创建对象实例,对于对象实例的访问行为,一定会发生在constructor函数执行之后,因此用户可以在constructor函数内定义需要前置执行的所有初始化行为。

若开发者未声明constructor函数,constructor的默认值有两种情况:

  1. 当前class未继承任何父类

    constructor默认为空函数

    constructor(){}
    
  2. 当前class继承自其他父类

    constructor默认为调用父类constructor函数的函数

    constructor(...args){
        super(...args)
    }
    

结合前面对new运算符的介绍,通过function和class创建实例的过程如下:

  1. 生成一个空对象
  2. 空对象的[[Prototype]]指向Person.prototype
  3. 执行function/class代码,并且执行过程中class/function内的this指向上述空对象
  4. 将最终的得到的对象赋值给aZhen / aQiang
// 所有实例的[[Prototype]]都指向构造函数的prototype
console.log(Object.getPrototypeOf(aZhen) === Person.prototype) // true
console.log(Object.getPrototypeOf(aZhen) === Object.getPrototypeOf(aQiang)) // true
aZhen.report() // 我的名字叫阿珍,性别是女
aQiang.report() // 我的名字叫阿强,性别是男

共享属性

按照上面的定义方式,我们会为每个对象实例都创建一个report函数,100个实例,就会创建100个report函数,毫无疑问,这是一种无意义的浪费,此时共享属性就派上用场了,只需要创建一个report函数作为共享属性,就可以供所有同类型的对象实例共同访问了,共享属性有以下两种类型:

  1. 挂载在构造函数prototype上的共享属性
  2. 挂载在构造函数本身的共享属性(静态属性)

挂载在构造函数的prototype

根据前面的内容,我们可知:

  1. 当访问一个对象实例的某个属性时,首先会先在这个对象实例本身上找,如果没有找到,则会去他的  [[Prototype]] 上找
  2. 对象实例的 [[Prototype]] 指向构造函数的prototype

因此将report函数挂载在Person.prototype上,所有通过Person创建的对象实例都可以访问到该函数。

function
function Person(name, gender = '女') {
   this.name = name
   this.gender = gender
}

// 注意:不能用箭头函数,否则this将不再指向对象实例
Person.prototype.report = function() {
  // this的指向不是本篇文章的重点,后面会单开一篇文章讲
  // 读者在这里只需要知道原型对象的函数调用时,this指向调用函数的对象实例
  console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
}
Person.prototype.type = 'person'

aZhen.report() // 我的名字叫阿珍,性别是女
aQiang.report() // 我的名字叫阿强,性别是男
console.log(aZhen.type) // person
class

class同样可以用上面的方式挂载属性到prototype

class Person {
  gender = '女'
  name;
  constructor(name, gender) {
    this.name = name
    this.gender = gender || this.gender
  }
}

Person.prototype.report = function() {
  console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
}
Person.prototype.type = 'person'

除此之外,class还可以通过如下方式挂载函数到prototype

知识来源

Methods are defined on the prototype of each class instance and are shared by all instances. Methods can be plain functions, async functions, generator functions, or async generator functions.

Classes - JavaScript | MDN

class Person {
  gender = '女'
  name;
  constructor(name, gender) {
    this.name = name
    this.gender = gender || this.gender
  }
  report () {
    console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
  }
}

挂载在构造函数本身

function
function Person(name, gender = '女') {
   // ...
}

Person.introduce = person => {
  console.log(`这是一位名字叫${person.name}${person.gender}性`)
}
Person.description = '人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来'

console.log(Person.description) // 人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来
Person.introduce(aZhen) // 这是一位名字叫阿珍的女性
// 构造函数生成的对象实例只能访问构造函数的prototype,无法访问构造函数本身
console.log(aZhen.description) // undefined
class
知识来源

The static keyword defines a static method or field for a class. Static properties (fields and methods) are defined on the class itself instead of each instance

Classes - JavaScript | MDN

class除了可以用与function相同的方式挂载属性到构造函数外,还可以通过static关键字实现相同的功能。

class Person {
  // ...
  static introduce(person) {
    console.log(`这是一位名字叫${person.name}${person.gender}性`)
  }
  static description = '人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来'
}

继承

知识来源

In programming, inheritance refers to passing down characteristics from a parent to a child so that a new piece of code can reuse and build upon the features of an existing one. JavaScript implements inheritance by using objects. Each object has an internal link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. By definition, null has no prototype and acts as the final link in this prototype chain.

Inheritance and the prototype chain - JavaScript | MDN

在Javascript中,继承是通过原型链来实现的,通过继承,可以将父级构造函数的特性传递给子级构造函数,从而基于已有代码实现功能的复用和改写。

function

function实现继承相对复杂一些,详细说明可以看看这篇文章

function inheritPrototype(subType, superType){
  const prototype = Object.create(superType.prototype); // 创建父类原型的一个副本
  prototype.constructor = subType;                    // 指定constructor属性指向构造函数本身
  subType.prototype = prototype;                      // 将新创建的对象赋值给子类的原型
}

// 父类初始化
function Person(name, gender = '女') {
   this.name = name
   this.gender = gender
}

Person.prototype.report = function() {
  console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
}

// 子类初始化
function Child(name, gender, mom){
  Person.call(this, name, gender);
  this.mom = mom
}

// 基于父类prototype,创建子类prototype
inheritPrototype(Child, Person);
Child.prototype.momReport = function(){
  console.log(`我的名字叫${this.name},我的妈妈是${this.mom.name}`)
}

const mom = new Person("美伢");
const aZhen = new Child("阿珍", '女', mom);

aZhen.report() // 我的名字叫阿珍,我的性别是女
aZhen.momReport() // 我的名字叫阿珍,我的妈妈是美伢

class

知识来源

The extends keyword is used in class declarations or class expressions to create a class as a child of another constructor (either a class or a function).

Classes - JavaScript | MDN

class对于继承的实现要简单的多,通过extends关键字来实现子类的创建,其父类可以是function,也可以是class。

function Person(name, gender = '女') {
   //...
}

class Child extends Person {
  constructor(name, gender, mom) {
    // 调用父类构造函数
    super(name, gender)
    this.mom = mom
  }
  momReport(){
    console.log(`我的名字叫${this.name},我的妈妈是${this.mom.name}`)
  }
}
知识来源

If there is a constructor present in the subclass, it needs to first call super() before using this. The super keyword can also be used to call corresponding methods of super class.

Classes - JavaScript | MDN

关于super,还有以下两个知识点:

  1. 如果子类声明了constructor函数,constructor内调用this之前一定要调用super函数
  2. super还可以用于调用父类的共享属性
class Person {
  constructor(name, gender) {
    // ...
  }
  report () {
    console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
  }
  static introduce(person) {
    console.log(`这是一位名字叫${person.name}${person.gender}性`)
  }
  static description = '人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来'
}

class Child extends Person {
  constructor(name, gender, mom) {
    this.mon = mom // ReferenceError,super 需要先被调用!
    super(name, gender)
    this.mom = mom
  }
  report(){
    super.report()
    console.log(`我的妈妈是${this.mom.name}`)
  }
  static introduce(child) {
    super.introduce(child)
    console.log(`他的妈妈名字是${child.mom.name}`)
  }
  static description = `${super.description},儿童泛指18岁以下的任何人`
}

const mom = new Person("美伢");
const aZhen = new Child("阿珍", '女', mom);
aZhen.report() // 我的名字叫阿珍,我的性别是女  我的妈妈是美伢
Child.introduce(aZhen)  //  这是一位名字叫阿珍的女性 他的妈妈名字是美伢
console.log(Child.description) // 人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来, 儿童泛指18岁以下的任何人

私有属性

知识来源

It's an error to reference private fields from outside of the class; they can only be read or written within the class body. By defining things that are not visible outside of the class, you ensure that your classes' users can't depend on internals, which may change from version to version

Classes - JavaScript | MDN

私有属性只能在class内使用,在class外调用会报错。

想象一下这种场景,我实现了一个工具函数准备发布到npm上公开,但是有一些属性,我已经预期到,随着功能迭代会发生变动(比如属性名从name1改为name2),为了降低维护成本,我不希望其他人可以访问到这些属性,那么我就可以将这些属性定义为私有属性。

私有属性通过#前缀来创建,只有class支持该前缀。也就是说如果我们通过function定义类型的话,是不能通过#前缀来创建私有属性的。

class Rectangle {
  #height = 0;
  constructor(height) {
    this.#height = height;
  }
  getHeight () {
    console.log(this.#height)
  }
}

const rectObj = new Rectangle(100)
rectObj.getHeight() // 100
rectObj.#height // 报错,class外不能调用
知识来源

Private fields can only be declared up-front in a field declaration. They cannot be created later through assigning to them, the way that normal properties can.

Classes - JavaScript | MDN

注意:私有属性必须前置声明,不能像实例属性那样,在constructor内在赋值的同时创建。

class Rectangle {
  #height = 0;
  constructor(height, width) {
    this.#height = height;
    // 没有前置声明,Uncaught SyntaxError
    this.#width=width
  }
}

结束语

好了,本菜鸡关于class和function只学习到这里,只能帮各位到这了,欢迎大家在评论区沟通交流,不过最近可能回复的不会很及时(因为公司的要求,要去研究其他东西了),大家先评论着,或者在评论区互相交流,等有时间了我再回来加入,我一定会回来的!(手动加载灰太狼语音包)哦对了,如果文章有帮助到你,帮我点个赞吧(谄媚脸 )~