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

2021-01-29 13:46:24 浏览数 (1)

首先,写这篇文章是因为我答应了一个粉丝要写一篇ES6的 class 相关知识的要求,哈哈我是不是特别宠粉呀~其实同时也是帮助我自己复习一下知识点啦

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

不相信的话,你们可以看看评论区,如果有学习过其它面向对象编程语言的,后来有学习过 JavaScript 的小伙伴可以在评论区分享一下自己对于构造函数这一概念的感想。

注意: 因为 class 语法涉及到大量的JavaScript中对象的概念,所以如果还没有了解过对象的小伙伴可以查看我之前写的一篇剖析对象概念的文章,下面放上文章链接,点击即可跳转

  • 充分了解JavaScript中的对象,顺便弄懂你一直不明白的原型和原型链

好了话不多说,我们开始讲解 class

  • 公众号:前端印象
  • 不定时有送书活动,记得关注~
  • 关注后回复对应文字领取:【面试题】、【前端必看电子书】、【数据结构与算法完整代码】、【前端技术交流群】

ES6——class语法

  • 一、构造函数
  • 二、class的语法
    • (1)体验class语法
    • (2)constructor
    • (3)类方法的定义
    • (4)get函数和set函数
    • (5) 静态方法
    • (6)实例属性的简易写法
    • (7)静态属性
  • 三、class的继承
    • (1)继承的概念
    • (2)ES5中实现继承
    • (3)ES6中class实现继承
    • (4)super
    • (5)prototype和__proto__
  • 四、class类的补充
    • (1)不存在变量提升
    • (2)new.target
  • 五、结束语

一、构造函数

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

代码语言:javascript复制
//定义构造函数 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) 等价于以下代码

代码语言:javascript复制
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语法引入以后,创建实例对象有何变化,这里我们就直接改写上述例子了,方便大家进行比较

代码语言:javascript复制
//用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 定义的类的类型

代码语言:javascript复制
class Person {
	
}
console.log(typeof Person)           // function

(2)constructor

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

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

代码语言:javascript复制
class Person {
	//不写constructor函数
	say() {
		console.log('hello world')
	}
}

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

代码语言:javascript复制
class Person {
	constructor() {
	
	}
	say() {
		console.log('hello world')
	}
}

以上两段代码是等价的

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

代码语言:javascript复制
class Person {

}
var person = Person()

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

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

(3)类方法的定义

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

代码语言:javascript复制
function Person() {}
Person.prototype.show = function () {
	console.log('hello world')
}

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

代码语言:javascript复制
class Person {
	//在Person类的原型上定义了方法 show
	show() {
		console.log('hello world')
	}
	//在Person类的原型上定义了方法 hide
	hide() {
		console.log('bye world')
	}
}

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

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

代码语言:javascript复制
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(): 返回一个数组,数组中的元素是对象中所有自有属性的名称,不管属性是否具有可枚举性都能被返回。

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

代码语言:javascript复制
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 定义的类中定义的方法的枚举性

代码语言:javascript复制
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函数接收一个参数,表示所设置的值

我们来看个例子

代码语言:javascript复制
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()方法来验证一下

代码语言:javascript复制
Object.getOwnPropertyNames(Person.prototype)

// [ 'constructor', 'number' ]

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

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

代码语言:javascript复制
//查询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 的方法就叫做非静态方法

我们来看一下具体的例子

代码语言:javascript复制
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指向的是类对象

我们来看一个例子

代码语言:javascript复制
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函数中的,例如

代码语言:javascript复制
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

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

代码语言:javascript复制
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函数中,而是直接写在外部,此时我们可以暂且称之为非静态属性

代码语言:javascript复制
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访问到。

三、class的继承

继承是面向对象编程中一个非常重要的概念,那什么是继承呢?

(1)继承的概念

继承就是使一个类获得另一个类的属性和方法。就好比一个手艺精湛的师傅传授给你他所有的毕生绝学,那么就相当于你继承了他,此时你既学会了你师傅教你的技能,同时你也一定有属于自己的技能,这不是从你师傅那学来的。

(2)ES5中实现继承

其实在ES5中是通过修改原型链实现继承的,我们可以来看一下简单的例子

代码语言:javascript复制
// 创建构造函数 Parent
function Parent() {
	// 定义了实例对象属性 name1
	this.name1 = 'parent'
}

// 为 Parent原型定义方法 show1
Parent.prototype.show1 = function() {
	console.log('我是Parent的show1方法')
}

// 创建构造函数 Child
function Child() {
	this.name2 = 'child'
}

// 将构造函数 Child的原型设置成 Parent的实例对象
Child.prototype = new Parent()

// 为Child原型定义方法 show2
Child.prototype.show2 = function() {
	console.log('我是Child的show2方法')
}

// 生成实例对象 child
var child = new Child()

console.log(child.name1)          // parent
console.log(child.name2)          // child
child.show1()                     // 我是Parent的show1方法
child.show2()                     // 我是Child的show2方法

我们可以看到,我们通过改变构造函数 Child的原型 prototype为构造函数 Parent生成的实例对象,实现了继承,即通过构造函数 Child生成的实例对象具有 Parent中定义的属性name1和方法show1,同时也具有属于自己的属性name2和方法show2

(3)ES6中class实现继承

ES5中实现继承的写法显然有些麻烦,所以在 class类中,我们可以通过关键字 extends来实现继承

我们来改写一下ES5中的继承实现

代码语言:javascript复制
class Parent{
    constructor() {
        this.name1 = 'parent'
    }
    show1() {
        console.log('我是Parent的show1方法')
    }
}

// Child类 继承 Parent类
class Child extends Parent{
    constructor() {
        super();
        this.name2 = 'child'
    }
    show2() {
        console.log('我是Child的show2方法')
    }
}

var child = new Child()

console.log(child.name1)          // parent
console.log(child.name2)          // child
child.show1()                     // 我是Parent的show1方法
child.show2()                     // 我是Child的show2方法

继承得实现整体上看上去非常得简洁

在上述代码中,我们看到了,我们在定义 Child类时用到了关键字 extends,申明了 Child类继承Parent类,同时在 Child类得 constructor函数中调用了 super函数。仅仅用两个关键字就实现了继承,这里我们要对 super进行详细得讲解

(4)super

在ES6中规定了,在子类继承了父类以后,必须先在子类的 constructor函数中调用 super函数,其表示的就是父级的 constructor函数,作用就是为子类生成 this对象,将父类的属性和方法赋值到子类的 this上。因此,若没有调用 super函数,则子类无法获取到 this对象,紧接着就会报错

代码语言:javascript复制
class A{
    constructor() {
        this.name1 = 'A'
    }
}

class B extends A{
    constructor() {
        this.name2 = 'B'
    }
}

var b = new B()

/*
        this.name2 = 'B'
        ^
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

*/

上述代码中,B类继承 A类,但 B类的 constructor函数中没有调用 super函数,因此没有生成 this对象,所以 this.name2 = 'B'就报错了

若子类省略了 constructor函数,则默认会帮你调用 super函数的

代码语言:javascript复制
class A{
    constructor() {
        this.name1 = 'A'
    }
}

class B extends A{

}

var b = new B()

// 没有报错

super()代表的是父类的构造函数,其实 super还可以作为对象使用,即不作为函数调用。当 super在子类的普通方法内时,指向的是父类的原型对象;在子类的静态方法内时,指向的时父类

代码语言:javascript复制
class A{
	show1() {
		console.log('我是A类的show1方法')
	}
}

class B extends A{
	constructor() {
		super()
	}
	show2() {
		super.show1()
	}	
}

var b = new B()

b.show2()              // 我是A类的show1方法

上述代码,B类继承 A类,其中 A类有一个 show1方法,是写在其原型 A.prototype上的,而在 B类的 show2方法中调用了 super.show1(),我们说过 super在普通的方法中指向的是父类的原型对象,所以 super.show1() 相当于 A.prototype.show1()

我们再来看一个 super在子类的静态方法中的例子

代码语言:javascript复制
class A{
    static hide1() {
        console.log('我是A类的hide1方法')
    }
}

class B extends A{
    constructor() {
        super()
    }
    static hide2() {
        super.hide1()
    }
}

B.hide2()                     // 我是A类的hide1方法

上述代码,B类继承 A类,B类在其静态方法 hide2中调用了 super.hide1(),因为 super在静态方法中指向的是父类,所以 super.hide1() 就相当于 A.hide1()

说到静态方法,其实类的继承,也是可以继承静态方法的

代码语言:javascript复制
class A{
	static show() {
		console.log('我是A类的show方法')
	}
}

class B extends A{
	
}

B.show()                    // 我是A类的show方法

还需要注意的是,当我们在子类的普通方法中通过 super调用父类的方法时,方法中的 this指向的是当前子类的实例对象

代码语言:javascript复制
class A {
    constructor() {
        this.name = 'Jack'
    }
    show1() {
        console.log(this.name)
    }
}

class B extends A{
    constructor() {
        super();
        this.name = 'Lpyexplore'
    }
    show2() {
        super.show1()
    }
}

var b = new B()

b.show2()                 // Lpyexplore

那么,当我们在子类的静态方法中通过 super调用父类的方法时,方法中的 this指向的是子类,而不是子类的实例对象

代码语言:javascript复制
class A {
    constructor() {
        this.x = 1
    }
    static show1() {
        console.log(this.x)
    }
}

class B extends A{
    constructor() {
        super();
        this.x = 2
    }
    static show2() {
        super.show1()
    }
}

B.show2()                 // undefined

B.x = 3
B.show2()                 // 3

上述代码中,我们在 B类的静态方法 show2中通过 super调用了 A类的静态方法 show1,执行代码 console.log(this.x),此时的 this指向的是 B类,但因为 B类的 constructor函数中定义的属性 x是定义在 B类的实例对象上的,所以 this.x 返回的是 undefined

所以我们在 B类上定义一个属性 x并且值为 3,此时再此调用 B.show2(),返回的就是 3了。

(5)prototype和__proto__

class类中有两个属性,分别表示着一条继承链,即 prototype__proto__

子类的__proto__总是指向父类; 子类的 prototype__proto__总是指向父类的原型

我们来验证一下

代码语言:javascript复制
class A{}

class B extends A{}

console.log(B.__proto__ === A)                     // true
console.log(B.prototype.__proto__ === A.prototype) // true

四、class类的补充

对于 class类还有几点需要补充以下

(1)不存在变量提升

构造函数本身就是个函数,存在变量提升,所以通过构造函数生成实例对象时,可以将构造函数写在生成实例对象的代码后面

代码语言:javascript复制
var person = new Person()

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

// 没有报错

虽然 class类的数据类型也属于 function,但是它是不存在变量提升的,即不可以在申明类之前生成实例对象,否则就会报错

代码语言:javascript复制
var person = new Person()

class Person{}

/*
报错:
var person = new Person()
             ^
ReferenceError: Cannot access 'Person' before initialization

*/

(2)new.target

class类必须通过 new来生成实例对象,因此ES6引入了一个新的属性 new.target,该属性一般用于 constructor函数中,表示通过关键字 new作用的构造函数的名称,若不是通过 new命令调用的,则返回 undefined

代码语言:javascript复制
class A{
	constructor() {
		if(new.target === 'undefined') {
			console.log('请通过关键字new调用')
		} else {
			console.log(new.target)
		}
	}
}


var a = new A()              // [class A]

当子类继承父类,并调用父类的 constructor函数时,new.target返回的是子类的构造函数名以及继承自哪个父类

代码语言:javascript复制
class A{
	constructor() {
		console.log(new.target)
	}
}

class B extends A{
	constructor() {
		super()
	}
}

var b = new B()               // [class B extends A]

五、结束语

好了,ES6的 class语法就讲到这里,希望这篇文章能帮助到大家。

创作不易,喜欢的加个关注,点个收藏,给个赞~ 带你们在Python爬虫的过程中学习Web前端

0 人点赞