ES6新特性实现面向对象编程,详解class语法定义类(上)

简介: ES6中出现 class 语法,只是创建构造函数的一种语法糖,那为何要新增一种语法去实现同一个功能呢?其实目的还是为了跟上一些主流编程语言的脚步,例如 java 、C++ 、Python,他们内部都是用 class 语法来实现的面向对象编程,所以咱们的 JavaScript 也不能落后,不然很多学习过 java c++ python 的小伙伴跑来学习 js时,就很难理解构造函数这一概念了。

一、构造函数


在学习 class 之前,我们先来回顾在ES6之前,创建一个实例对象是通过构造函数来实现的


//定义构造函数 Person
function Person(name, age) {
 this.name = name
 this.age = age
}
//在构造函数原型上定义方法 show
Person.prototype.show = function() {
 console.log('姓名:' + this.name)
 console.log('年龄:' + this.age)
}
//创建了一个Person类的实例
var person = new Person('Jack', 18)
console.log(person.name)              // Jack
console.log(person.age)               // 18
person.show()                         /* 姓名:Jack
              年龄:18      */


我们通过 new 关键字调用构造函数,即可生成一个实例对象。不妨我们再来回顾一下 new 关键字的作用过程,即 var person = new Person('Jack', 18) 等价于以下代码


var person = function (name='Jack', age = 18) {
 // 1.创建一个新的空对象赋值给this
 var this = {}
 // 2.执行构造函数里的所有代码
 this.name = name
 this.age = age
 // 3.返回this
 return this
}()


通过以上代码我们可以得知,构造函数中的 this 指向的是新生成的实例对象,下文会讲到,在 class 语法中,this 在不同情况下会有不同的含义


二、class的语法


(1)体验class语法


接下来,我们来看看 class语法引入以后,创建实例对象有何变化,这里我们就直接改写上述例子了,方便大家进行比较


//用class定义一个类
class Person {
 constructor(name, age) {
  this.name = name
  this.age = age
 }
 show() {
  console.log('姓名:' + this.name)
  console.log('年龄:' + this.age)
 }
}
//生成Person类的一个实例对象person
var person = new Person('Jack', 18)
console.log(person.name)              // Jack
console.log(person.age)               // 18
person.show()                         /* 姓名:Jack
              年龄:18      */


通过调用实例对象的属性 nameage 以及方法 show,我们可以看到,跟构造函数没有任何的区别,所以说 class 语法就是构造函数的一个语法糖,即构造函数的另一种写法,这两者并无本质区别


其实我们还可以通过 typeof 来验证一下 class 定义的类的类型


class Person {
}
console.log(typeof Person)           // function


(2)constructor


当我们用 class 定义了一个类,然后用关键字 new 调用该类,则会自动调用该类中的 constructor函数,最后生成一个实例对象。constructor函数内部的 this 指向的也是新生成的实例对象。


如果要生成一个不需要任何属性的实例对象,则我们不需要在 constructor函数里写任何代码,此时可以省略它,例如


class Person {
 //不写constructor函数
 say() {
  console.log('hello world')
 }
}


上述代码省略了 constructor函数,此时JavaScript会默认生成一个空的 constructor函数,例如


class Person {
 constructor() {
 }
 say() {
  console.log('hello world')
 }
}


以上两段代码是等价的


也正是因为 constructor函数的存在,class 定义的类必须通过 new 来创建实例对象,否则就会报错


class Person {
}
var person = Person()
/*
报错
var person = Person()
             ^
TypeError: Class constructor Person cannot be invoked without 'new'
*/


而传统的构造函数就可以不通过 new 来调用,因为其本身就是一个函数,若不加关键字 new,则相当于直接执行该函数


(3)类方法的定义


在传统的构造函数中,为了使每个实例对象都拥有共同的方法,在构造函数的原型上进行方法的定义,例如


function Person() {}
Person.prototype.show = function () {
 console.log('hello world')
}


因此,class语法定义的方法也是在原型上的,不过这里称之为类的原型上,同时省略了大量的代码,直接将方法写在 class 内即可


class Person {
 //在Person类的原型上定义了方法 show
 show() {
  console.log('hello world')
 }
 //在Person类的原型上定义了方法 hide
 hide() {
  console.log('bye world')
 }
}


细心的小伙伴肯定发现了,虽然方法都是写在 {} 内的,但是每个方法之间无需用 , 隔开,否则就会报错,这个一定要注意一下


其实以上定义类方法的代码等价于以下代码


class Person {}
//在Person类的原型上定义了方法 show
Person.prototype.show = function () {
 console.log('hello world')
}
//在Person类的原型上定义了方法 hide
Person.prototype.hide = function () {
 console.log('bye world')
}


这其实跟为构造函数定义方法一样,但是整体看上去代码量就非常得大


虽说构造函和类两者定义的方法都是定义在其原型上的,但还是有略微的区别,即前者定义的方法具有 可枚举性;而后者定义的方法具有 不可枚举性


为了验证两者区别,我们要用到ES5中提供的两个新方法


  • Object.keys(): 会返回一个数组,数组中的元素就是对象中可枚举的自有属性名


  • Object.getOwnPropertyNames(): 返回一个数组,数组中的元素是对象中所有自有属性的名称,不管属性是否具有可枚举性都能被返回。


首先我们来验证一下构造函数定义的方法的枚举性


function Person() {}
Person.prototype.show = function () {
 console.log('hello world')
}
Person.prototype.hide = function() {
 console.log('bye world')
}
Object.keys(Person.prototype)   // [ 'show', 'hide' ]
Object.getOwnPropertyNames(Person.prototype)   // [ 'constructor', 'show', 'hide' ]


我们可以看到,Object.keys()方法返回 [ 'show', 'hide' ],证明这定义的两个方法是自有属性且是可枚举的;Object.getOwnPropertyNames()方法返回 [ 'constructor', 'show', 'hide' ],说明构造函数内有一个自有属性方法 constructor,且不可枚举。


接下来我们再来看一下 class 定义的类中定义的方法的枚举性


class Person {
 show() {
  console.log('hello world')
 }
 hide() {
  console.log('bye world')
 }
}
Object.keys(Person.prototype)   // []
Object.getOwnPropertyNames(Person.prototype)   // [ 'constructor', 'show', 'hide' ]


我们看到 Object.keys() 返回 [],说明 class类定义的方法具有不可枚举性;Object.getOwnPropertyNames()方法返回 [ 'constructor', 'show', 'hide' ],可以看到同样也具有一个不可枚举的自有属性 constructor方法。


(4)get函数和set函数


class类中,可以使用两个内部定义的函数,即 getset,语法为 get/set 属性名() {},分别表示读取属性/设置属性时调用此函数,其中 set函数接收一个参数,表示所设置的值


我们来看个例子


class Person {
 get number() {
  return 18
 }
 set number(value) {
  console.log('现在的number值为:' + value)
 }
}
var person = new Person()
//访问属性number
person.number  //   18
//设置属性number为20
person.number = 20  // 打印:现在的number值为:20


当我们访问属性 number时,会调用 get number() {}函数,故返回 18;当设置属性 number的值为 20时,会调用 set number() {}函数,故打印了 现在的number值为:20


表面上看,getset函数是方法,但其实并不是,我们可以用 Object.getOwnPropertyNames()方法来验证一下


Object.getOwnPropertyNames(Person.prototype)
// [ 'constructor', 'number' ]


我们可以看到,返回的数组中只有 class类自带的 constructor函数和 number属性,并没有看到 getset 函数。


了解ES5中对象概念的小伙伴应该知道,对象中有两个存储器属性,分别为 gettersetter,它们是对象中某个属性的特性,并且可以通过 Object.getOwnPropertyDescriptor()方法获得对象中某个属性的属性描述符


//查询Person.prototype中属性number的属性描述符
Object.getOwnPropertyDescriptor(Person.prototype, 'number')
/*
{
  get: [Function: get number],
  set: [Function: set number],
  enumerable: false,
  configurable: true
}
*/


因此,我们在 class类中写的 getset 函数只是设置了某个属性的属性特性,而不是该类的方法。


(5) 静态方法


class类中的方法都是写在原型上的,因此生成的实例对象可以直接调用。现在有一个关键字 static,若写在方法的前面,则表示此方法不会被写在原型上,而只作为该类的一个方法,这样的方法叫做静态方法;相反,若没加关键字 static 的方法就叫做非静态方法


我们来看一下具体的例子


class Person {
 show() {
  console.log('我是非静态方法show')
 }
 static show() {
  console.log('我是静态方法show')
 }
 static hide() {
  console.log('我是静态方法hide')
 }
}
Person.show()    // 我是静态方法show
var person = new Person()
person.show()    // 我是非静态方法show
person.hide()    /* person.hide()
                           ^
     TypeError: person.hide is not a function
    */


我们分析一下这个例子:


首先我们直接调用 Person类的 show方法,实际调用的就是有关键字 staticshow方法;


然后我们生成了一个实例对象 person,然后调用 person实例对象上的 show方法,实际调用的就是没有关键字 staticshow方法,从这我们可以看出,静态方法和非静态方法可以重名


最后我们调用了 person实例对象上的 hide方法,但报错了,因为在 class类中,我们定义的是静态方法,即有关键字 statichide方法,也就是此方法没有被写进类的原型中,因而实例对象 person无法调用此方法。


我们都知道,类中定义的方法内的 this指向的是实例对象,但在静态方法中的 this指向的是类对象


我们来看一个例子


class Person {
 constructor() {
  this.name = 'Lpyexplore'
 }
 show() {
  console.log(this.name)
 }
 static cite() {
  this.show()
 }
 static show() {
  console.log('我是非静态方法show')
 }
}
Person.cite()     // 我是非静态方法show
var person = new Person()
person.show()     // Lpyexplore


我们来分析一下这段代码:


首先我们直接调用 Person类的静态方法 cite,执行代码 this.show(),因为静态方法中的 this指向 Person类,所以其实调用的就是静态方法 show,所以打印了 我是非静态方法show


然后我们生成了一个实例对象 person,调用 personshow方法,因为在非静态方法 show中,this指向的是实例对象 person,因此打印了 Lpyexplore


(6)实例属性的简易写法


原先我们为实例对象定义的属性都是写在 constructor函数中的,例如


class Person {
 constructor() {
  this.name = 'Lpyexplore'
  this.age = 18
 }
 show() {
  console.log('hello world')
 }
}
var person = new Person()
console.log(person.name)        // Lpyexplore
console.log(person.age)         // 18


现在我们用实例对象的属性新写法来改写以上代码


class Person {
 name = 'Lpyexplore'
 age = 18
 show() {
  console.log('hello world')
 }
}
var person = new Person()
console.log(person.name)        // Lpyexplore
console.log(person.age)         // 18


这种写法就是将 constructor函数中的属性定义放到了外部,同时不需要写 this,因为此时的属性定义与其他方法也处于同一个层级。因此这样的写法看上去就会比较一目了然,一眼就能看到实例对象有几个属性有几个方法。


虽然这样的写法比较简便,但也有一定的缺点,那就是用这种写法定义的属性是写死的。


我们都知道在生成实例对象时,可以传入参数,传入的参数会作为 constructor函数的参数,所以我们在 constructor函数中定义的属性的值就可以动态地根据参数改变而改变。


而实例属性的简易写法就无法根据参数的改变而改变,所以用这种写法的时候需要稍微注意一下。


(7)静态属性


既然有静态方法,那怎么能少了静态属性呢?其实,原本的 class类中是没有静态属性这个概念的,后来才加上的。静态属性就只属于 class类的属性,而不会被实例对象访问到的属性。


同样的,静态属性的申明就是在属性的前面加关键字 static。上面我们刚讲到,实例对象的属性的定义可以不写在 constructor函数中,而是直接写在外部,此时我们可以暂且称之为非静态属性


class Person {
 name = '我是实例对象的name属性'
 static name = '我是Person类的name属性'
 static age = 18
}
console.log(Person.name)   // 我是Person类的name属性
var person = new Person()
console.log(person.name)   // 我是实例对象的name属性
console.log(person.age)    // undefined


这段代码中,定义了非静态属性 name 、静态属性 name 和 静态属性 age


因此我们在访问 Person类的 name属性时,访问的是静态属性 name,即加了关键字 staticname属性;


生成实例对象 person,访问其 name属性,实际访问的就是非静态属性 name,即没有加关键字 staticname属性;


最后我们访问实例对象 personage属性,返回了 undefined。因为 age是静态属性,是属于 Person类的,而不会被实例对象 person访问到。

相关文章
|
5月前
|
JavaScript 前端开发 Java
TypeScript 类 第一章【类,继承,修饰符】
TypeScript 类 第一章【类,继承,修饰符】
39 1
|
5月前
|
JavaScript 前端开发 程序员
TypeScript 类 第三章 【抽象类,类的高级技巧】
TypeScript 类 第三章 【抽象类,类的高级技巧】
54 2
|
5月前
|
Java 编译器
你一定要学会的Java语法 -- 【继承】
你一定要学会的Java语法 -- 【继承】
48 0
|
4月前
|
存储 JavaScript
TypeScript 类的基础:从定义到实例化,让你快速掌握(二)
TypeScript 类的基础:从定义到实例化,让你快速掌握
|
4月前
|
缓存 JavaScript 前端开发
TypeScript 类的基础:从定义到实例化,让你快速掌握(一)
TypeScript 类的基础:从定义到实例化,让你快速掌握
|
4月前
|
存储 设计模式 JavaScript
TypeScript 类的基础:从定义到实例化,让你快速掌握(三)
TypeScript 类的基础:从定义到实例化,让你快速掌握
|
5月前
ES6学习(十一)—Class 的基本语法和继承
ES6学习(十一)—Class 的基本语法和继承
|
5月前
继承语法详解
继承语法详解
41 0
|
6月前
ES6系列笔记-面向对象/继承
ES6系列笔记-面向对象/继承
21 1
|
7月前
|
JavaScript
TypeScript 具有可选的静态类型和基于类的面向对象编程,具体应用案例解析
TypeScript 具有可选的静态类型和基于类的面向对象编程,具体应用案例解析
45 0