ES6中的class对象和它的家人们

科技资讯 投稿 5500 0 评论

ES6中的class对象和它的家人们

1.什么是class

一个特性的诞生,总是为了解决某些问题的。而class的诞生还要从ES5中的构造函数说起。

构造函数模式 的方式创建对象。创建方式如下

function Animal( {}  // 构造函数
const animal = new Animal(
// 通过new的方式创建一个新的对象,该对象称为构造函数的实例对象

我们发现上述的构造函数在定义的时候和普通的函数定义是一模一样的。而事实上,上述所谓的构造函数本质就是一个函数,只是这个函数的作用是用于创建对象。这导致构造函数和普通的函数难以区分,这是ES5构造函数的一个弊端。另一个问题是,ES5中的构造函在实现继承的时候,代码冗长且混乱(下文中有举例说明)。在这样的背景下class诞生了。

ES5中的构造函数的语法糖,本质还是一个函数对象。用于高效的创建对象或实现继承。

2.创建一个class

const animal = new Animal(  // 可以成功创建实例
function Animal( {}

const animal = new Animal(  //抛错:Cannot access 'Animal' before initialization(不允许初始化前创建实例)
class Animal {}

而至于 左侧的变量式声明 和 右侧的类表达式声明,由于都是使用一个变量进行接收,所以都受变量提升影响。

3.class对象和他们的家人们

和class直接相关的有三个对象,分别是:实例对象(以下统称为实例),类对象(定义一个类,类本身就是一个对象),原型对象。这三个对象是怎么创建和使用的呢?三个对象之间有什么关系呢?接下来将分别阐述他们。

3.1 实例对象

方式一: 实例创建之后,手动添加属性和方法

class Animal {}

const animal = new Animal(
animal.name = 'lsm'    // 添加属性
animal.move = ( => {  // 添加方法
  console.log('moving ...'
}

但是这种方式,有个最大的弊端:当有些属性和方法需要每个实例都要有的时候,需要每次创建完实例之后都添加一遍。代码冗余度非常高,并且要是都这样写class就失去了它的意义。

方式二:在类中定义实例成员

    constructor是什么:一个方法。定义在每个类对象的原型对象上(这两个对象将在3.2、3.3中进行讲解)。可以在类的代码块中进行重写。

  • 初始化实例对象。

  • constructor特性:constructor函数体中的this指向实例,所以我们给this添加的成员,实际上就是添加在实例上。换而言之,给该this添加的成员就是实例成员。最终返回this。

  • 在我们通过new关键字创建实例的时候,默认的会调用定义在类中constructor函数。如果在类中没有显示的定义constructor函数,则会调用类的原型对象上的constructor函数。

class Animal {
  // 重写了Animal原型对象上的constructor方法
  constructor(name {   // 接收的name参数,用于初始化实例的name属性
    console.log("new 关键字调用"
    this.name = name    // 给实例添加name属性并赋值
    this.move = function(speed {   // 给实例添加move方法
      console.log('moving speed ' + speed + ' m/s'
    }
  }
}

const animal = new Animal('lsm'  //new 关键字调用
console.log(animal.name  // lsm
animal.move(10 // moving spead 10 m/s

在上述的案例中,我们了解了如何通过class定义一个实例成员。针对于方式一,实际上就是在给一个普通的对象添加属性和方法,如果我们想在某个实例上加上独属于自身的成员,就可以使用方式一。

3.2 类对象

ES5中的构造函数的语法糖,本质还是一个函数对象。 总结来说类是一个函数,验证的方式很简单: typeof关键字

class Animal {}
console.log(typeof Animal // funtion
const animal = new Animal(
Animal(  // Uncaught TypeError: Class constructor Animal cannot be invoked without 'new'

虽然类本质的是个函数,但是我们并不能像调用函数那样调用它,像Animal(就会报错, 需要通过new关键字进行调用, 。

class Animal {}
Animal.age = 25;
Animal.move = ( => {
  console.log("moving ...";
};

console.log(Animal.age  // 25
Animal.move(  // moving ...

但class作为ES5中构造函数的语法糖,ES6中对这种给类对象添加成员的需求提供了一种新的方式:将需要添加的类成员直接定义在类代码块中,并在定义的成员前面添加static修饰符。我们将这种通过static修饰的成员称之为静态成员。 具体的实现如下:

class Animal {
  static age = 25
  static move( {
    console.log('moving ...'
  }
}

console.log(Animal.age  // 25
Animal.move(   // moving ...

通过上述两个案例可以看出,虽然定义类成员的方式不同,但使用类成员的方式并没有区别。从结果而言,上述的两种定义类成员的方式是完全等价的。

给类对象自身添加的成员称之为静态成员。在ES6中提供了使用static修饰符创建静态成员的方式。

脱离实例。创建与类本身强绑定的成员。 总的来说就是我想创建一些属性和方法,但是这些属性和方法并不需要创建实例就能调用或者和实例本身就没啥关系。这句话可能不好理解,我用两个例子来说明下。

    Math.PI、Math.random(: Math中的这些成员都是静态成员。通过创建实例的方式去调用这些成员是毫无意义的(实际上也不能),因为这些属性的值或方法的结构全都是固定的。
  1. Array.isArray(: 这个方法的作用是判断所有类型的对象是不是数组,这和数组的实例没有一毛钱关系。
  2. ......
    为什么ES6中添加静态成员的时候需要添加static修饰符?
  1. 如果不加static修饰符,这个成员就不是静态成员了吗?
  2. 如果问题2成立,在3.1讲述constructor的时候,constructor这个函数是直接定义在class代码块中的,没有添加static,那我们创建实例的时候调用的constructor函数又是属于哪个对象的?

在回答这三个问题之前,我想重新带大家复习一遍,和class直接相关的三个对象:实例对象,类对象,原型对象。这很重要!

hasOwnProperty方法的作用:可以检测一个成员是否存在于对象自身中(不包括原型),返回布尔值。只有当成员存在于对象自身时才会返回true,否则返回false

class Animal {
  static age = 25  // 静态属性
  static move( {  // 静态方法
    console.log('moving ...'
  }
  speed = 10  // 普通的属性
  constructor( {}  // 构造方法
}

console.log(Animal.hasOwnProperty('age'  // true
console.log(Animal.hasOwnProperty('move'  // true
console.log(Animal.hasOwnProperty('speed'  // false
console.log(Animal.hasOwnProperty('constructor' // false

通过上述的测试,可以发现通过static修饰的成员确实属于类对象本身。而没有static修饰的成员则不属于类对象本身。这就是问题2的答案。而至于这些没有satic修饰的成员到底属于哪个对象,将在3.4中进行总结归纳。

为了区分类对象自身的成员和其他成员。 可能有一些小伙伴对作者提出的问题一,觉得莫名其妙。其实这里作者是想加固小伙伴的认知:所谓static静态成员,就是在类对象本身的一个成员而已,static只是一个语法糖。

原型对象

3.3原型对象

3.3.1 是什么:一个对象。会伴随类的声明而创建的一个对象。类中通过prototype属性指向的一个对象。举例如下:
class Animal {}
console.log(Animal.prototype  // 打印结果如下图

通过new关键字创建一个对象的时候,无论有没有在类中显示的声明constructor,调用的始终都是原型对象中的constructor方法。 并且针对上图中的打印结果我们发现一个有意思的点,原型对象上的constructor是一个属性,该属性指向的是类对象本身。 我们不妨打印看看

console.log(Animal.prototype.constructor === Animal  // true

结果为true。看到这,有些小伙伴可能就迷惑了,constructor不是一个用于初始化实例的函数吗?现在怎么又变成了一个属性? 并且还指向类本身? 这都是些什么乱七八糟的。

    constructor的作用是什么:初始化实例对象。我们要明白,在我们调用new关键字的那一刻就已经创建了一个对象,而constructor仅仅是初始化了这个对象,初始化完成再返回这个对象。我们可以简单的将constructor当成一个入口,供开发者初始化实例的入口。所以我们需要调用constructor而不是直接调用类
  • 我们知道所有的数组都是Array类的实例,那是怎么确定的呢?就是通过constructor。正是通过constructor的指向,我们才能确定实例对象属于哪个类(实现原理会在3.5中详讲。这也是为什么需要让constructor指向类本身。

言归正传,了解了原型对象是什么,接下来说说,具体怎么用。

3.3.2 怎么用
class Animal {
  move( {
    console.log('moving ...'
  }
  name = 'cat'
}

console.log(Animal.prototype.hasOwnProperty("move" // true
console.log(Animal.prototype.hasOwnProperty("name" // false

有意思的是,我们发现定义在类代码块中的方法确实是原型方法,但是定义在类代码块中的属性却不是。不是话又属于谁,接着验证。

const animal = new Animal(
console.log(Animal.hasOwnProperty("myName"  // false
console.log(animal.hasOwnProperty("myName"  // true

经过验证我们发现,直接定义在类代码块中的属性是实例属性。其实这个实例属性并没有多大意义,因为我们已经知道了可以在constructor初始化实例成员。所以在开发中这种定义方式相当少。

3.3.3 存在的意义

我们先看一段简单代码

class Animal {
  constructor(name {
    this.name = name
    this.move = ( => {
      console.log(this.name + ' moving ...'
    }
  }
}

const animal = new Animal(cat
animal.move( // cat moving ...
const animal1 = new Animal(cat2
animal.move( // cat2 moving ...

这段代码很简单,就是创建了一个类和两个类的实例。这段代码有问题吗,逻辑上来说没有问题,但是有一个弊端,就是在对方法的处理上冗余度过高。上述代码中,我们每创建一个实例,就会给这个实例添加一个move方法。但是move方法里面的处理逻辑是完全相同的,如果大量的创建对象,将会占用大量的内存空间,浪费资源。

我们可以将一些实例公用的方法抽取到原型对象上。而原型对象只会随着类的创建而创建,只会加载一次。 之后我们创建的实例可以直接调用这个原型对象上的方法。从而避免重复创建冗余的方法。至于实例为什么可以直接使用原型对象的上的方法将在3.5中介绍。改造一下上面的代码。

class Animal {
  move( {
    console.log(this.name + " moving ...";  // this指向方法的调动者
  }
  constructor(name {
    this.name = name;
  }
}

const animal = new Animal(cat
animal.move( // cat moving ...
const animal1 = new Animal(cat2
animal.move( // cat2 moving ...

上述代码值得注意的一点是:move方法中的this和constructor中的this没有任何关系。constructor中的this指向的是实例。move方法中的this指向的是方法的调用者。

大部分情况下我们可能希望每个对象都拥有自身的属性。这也回答了3.3.2中遗留的问题:为什么直接定义在类代码块中的属性当成是实例属性而不是原型属性呢? 因为在设计之初就并不希望开发者去定义原型属性。如果我们真的想定义原型属性,可以采用ES5的方式:

Animal.prototype.myName = 'lsm'
const animal = new Animal(cat
console.log(animal.name  // cat
console.log(animal.myName // lsm

看到这的小伙伴估计就会有一种感觉:属性和方法的定义好乱!没事接下来我给大家总结一下。

3.4 三个对象中的成员归纳

    想定义实例成员,可以在constructor方法中进行初始化。对于实例属性也可以直接定义在类的代码块中。
  1. 想定义静态成员,可以在类的代码块中的使用static 修饰符修饰属性和方法。也可以直接使用对象的形式添加(Obj.key=val)。
  2. 想定义原型成员,可以通过对象的形式在类的原型上添加成员。对于原型方法,可以直接定义在类的代码块中。
class Animal {
  name1 = 'lsm'  // 实例属性
  move1( {  // 原型方法
    console.log("moving1 ..."
  }

  constructor(name { // 原型方法
    this.name = name  // 实例属性
    this.move = ( => {  // 实例方法
      console.log("moving ...";
    }
  }

  static name2 = 'cat'  // 静态属性
  static move2( {  // 静态方法
    console.log("moving2 ..."
  }
}

Animal.name3 = "lion"  // 静态属性,推荐使用static的方式
Animal.move3 = ( => {  // 静态方法,同上
  console.log("moving3 ..."
}

Animal.prototype.name4 = "cattle"  // 原型属性,不推荐
Animal.prototype.move4 = ( => {  // 原型方法,推荐直接在类中定义
  console.log("moving4 ..."
}

下来我们来对比下ES5和ES6的类中定义不同对象成员的方式

讲解上述的三种对象时,我基本都是在说如何定义却没说使用。因为确实也没啥好说的。三种对象都可以使用自身的属性和方法,除此之外唯一需要注意的就是实例对象可以使用原型对象上的成员。但是为什么实例对象可以使用原型对象上的成员呢?接下来,让我们好好剖析下这三个对象之间的关系

3.5 实例对象,类对象,原型对象之间的关系。

    为什么通过constructor的指向,我们能确定实例对象属于哪个类。
  1. 为什么实例对象可以使用原型对象上的属性。

其实上述两个问题的答案是一致的。因为在实例对象中有一个默认的指针[[Prototype]]指向原型对象。不同浏览器对该指针有不同的实现方式。在chrome、Firefox等浏览器中的,对该指针的实现为__proto__属性。换而言之,我们可以通过__proto__属性访问到原型对象。 正是因为实例和原型对象之间存在这样的引用关系,我们才可以实现上述的两种操作。我们可以验证一波:

class Animal {
  move( {  // 定义了原型方法
    console.log('moving ...'
  }
}
Animal.prototype.myName = "cat"  // 定义了原型属性

const animal = new Animal(
console.log("animal = ", animal  // 打印结果见下图
console.log("animal.__proto__ = ", animal.__proto__

通过3.3.1可知:类对象通过prototype属性指向其原型对象。如果实例的__proto__属性和类对象的prototype属性相等,是不是就可以证明实例的__proto__属性指向的是原型对象。

console.log(animal.__proto__ === Animal.prototype  // true

验证的结果是肯定的。而在ES5中的instanceof方法正是通过这种方式来判断某个实例是否属于某构造函数。

在调用一个对象属性的时候,会从对象自身开始查找,查找不到会去对象的原型上查找,并依次向上进行查找,直到找到或查找到原型链的顶端null为止。

    实例对象的[[Prototype]]指针指向原型对象。
  1. 类对象的prototype属性指向其原型对象。

需要注意的是,虽然实例对象和类对象都有属性指向原型对象,但是这两个对象之间没有任何直接引用关系。

我用图例来展示这三个对象之间的引用关系

4.继承

开篇在,什么是class中我们提到:ES5中的构造函在实现继承的时候,代码冗长且混乱。那我们不妨先来看看ES5中的继承方式。ES5中的继承方式有很多,最常用的就是寄生式组合继承,我们就以寄生式组合继承为例:

function Animal(myName {  // 父类
  this.myName = myName
}
Animal.prototype.move = ( => {
  console.log("moving ..."
}

function Cat (myName, age {  // 子类
  // 1.继承父类实例成员。这里就是将Animal当成一个普通的函数,通过call调用,返回的结果就是父类中的实例成员
  Animal.call(this, myName 
  this.age = age
}

 // 2.继承父类原型对象成员。
 //  Object.create创建一个新对象,对象的原型是 Animal.prototype, 结果返回给子类的原型
Cat.prototype = Object.create(Animal.prototype 
 // 3.此时子类的原型是空对象,下面的操作是给子类的原型添加constructor属性并指向子类自身
Cat.prototype.constructor = Cat

const cat = new Cat("lsm", 25
cat.move( // moving ...

根据上述的代码可知,ES5中的寄生式组合继承大致分为三步:

    继承父类实例成员
  1. 继承父类原型对象成员(执行完这一步,其实子类的原型是一个空对象)
  2. 添加子类的constructor指向自己(用于确定实例属于哪个类)
class Animal {
  move ( {
    console.log("moving ..."
  }

  constructor (myName {
    this.myName = myName
  }
}

class Cat extends Animal {
  constructor(myName, age {
    super(myName
    this.age = age
  }
}

const cat = new Cat("lgt", 75
cat.move(  // moving ...

以上两种继承方式的结果几乎是相同的。不难看出,class的继承方式简洁很多,并且继承的步骤都是在类上执行的,比起ES5的继承方式更加内聚。

    extends 用于继承父类的原型对象成员。相当于ES5继承中的步骤2。除此之外,extends甚至还可以继承父类的静态成员当做子类的静态成员。这是ES5中的继承所不具备的。 示例代码如下
    super 用于继承父类的实例成员。相当于ES5继承中的步骤1。 super的使用有一些注意点,但在此之前我想先和大家讨论下super是什么。

已知的,我们在子类的constructor中调用super时候,父类的constructor被调用了。我们又知道constructor指向的其实就是类本身。所以其实super最终指向的就是父类本身。在了解这一点之后我们再来看看super使用的注意事项。

    super调用位置可以是cosntructor或静态方法中。
  1. 子类的cosntructor被显式定义时,也必须显式的调用super方法。super接收的参数用于传递给父类的cosntructor
  2. super方法调用之前不能使用this。 这一点很好理解。super调用的是父类的cosntructor,cosntructor的作用是初始化并返回的this。所以在super调用之前,压根就拿不到this。
  3. 在静态方法中super可以调用父类的静态成员。 这点也很好理解,因为super指向的就是父类,调用父类自身的属性是合理的。这一点带大家实践一波
class Animal {
  static myName = "lsm"
  static move( {
    console.log('moving ...'
  }
}

class Cat extends Animal {
  static useAnimal( {
    console.log(super.myName  // 调用父类的静态属性
    console.log(Cat.myName  // 调用继承来的静态属性
    super.move(  // 调用父类的静态方法
    Cat.move(  // 调用继承来的静态方法
  }
}

Cat.useAnimal( // lsm
                // lsm
                // moving ...
                // moving ...

最后,站在三个对象的角度怎么理解继承呢,来看张图吧

以上就是今天的全部内容啦,谢谢各位看官老爷的观看。不好的地方,还请包涵。不对的地方,还请指正。

参考文献:JavaScript高级程序设计(第四版)

编程笔记 » ES6中的class对象和它的家人们

赞同 (27) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽