一、构造函数
在学习 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 */
通过调用实例对象的属性 name
、age
以及方法 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
类中,可以使用两个内部定义的函数,即 get
和 set
,语法为 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
表面上看,get
和 set
函数是方法,但其实并不是,我们可以用 Object.getOwnPropertyNames()
方法来验证一下
Object.getOwnPropertyNames(Person.prototype) // [ 'constructor', 'number' ]
我们可以看到,返回的数组中只有 class
类自带的 constructor
函数和 number
属性,并没有看到 get
和 set
函数。
了解ES5中对象概念的小伙伴应该知道,对象中有两个存储器属性,分别为 getter
和 setter
,它们是对象中某个属性的特性,并且可以通过 Object.getOwnPropertyDescriptor()
方法获得对象中某个属性的属性描述符
//查询Person.prototype中属性number的属性描述符 Object.getOwnPropertyDescriptor(Person.prototype, 'number') /* { get: [Function: get number], set: [Function: set number], enumerable: false, configurable: true } */
因此,我们在 class
类中写的 get
和 set
函数只是设置了某个属性的属性特性,而不是该类的方法。
(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
方法,实际调用的就是有关键字 static
的 show
方法;
然后我们生成了一个实例对象 person
,然后调用 person
实例对象上的 show
方法,实际调用的就是没有关键字 static
的 show
方法,从这我们可以看出,静态方法和非静态方法可以重名;
最后我们调用了 person
实例对象上的 hide
方法,但报错了,因为在 class
类中,我们定义的是静态方法,即有关键字 static
的 hide
方法,也就是此方法没有被写进类的原型中,因而实例对象 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
,调用 person
的 show
方法,因为在非静态方法 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
,即加了关键字 static
的 name
属性;
生成实例对象 person
,访问其 name
属性,实际访问的就是非静态属性 name
,即没有加关键字 static
的 name
属性;
最后我们访问实例对象 person
的 age
属性,返回了 undefined
。因为 age
是静态属性,是属于 Person
类的,而不会被实例对象 person
访问到。