JS面向对象总结

ES5和ES6的面向对象语法

Posted by Ray on 2017-12-12

面向对象

封装,继承,多态

对于JS的多态并不是那么明显的去满足多态的四大表现形式:接口,抽象类,重写,重载。
重载的概念大体上为具有相同函数名,体现在函数具有不同参数的形式,而重写表示的是重新构造具备相同的函数名和函数参数的实现方法,对于这两点上Js对函数参数不敏感,无法完全体现。

在Java之中,一个类不能实现多继承,但是可以去实现多个接口,接口里面定义的方法一旦要实现就要都实现,而抽象类就是要将好几个类存在着共同“个性”再网上层抽象出一个抽象类,让这些类分别的去继承该抽象类。

本文将着重表达封装和继承者两大面向对象最基本的特点。

将一组描述特定事物的属性以及一系列操作这些属性的方法,封装成一个对象(类)

构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Dog(name,size){
this.name = name || '二哈'
this.size = size || '中等'
}

Dog.prototype.bark = function(){
console.log(`${this.name}在叫!`)
}

let shinubi = new Dog('shinubi','小')
let peiqi = new Dog('peiqi','肥')
shinubi.bark()
console.log(shinubi.constructor==Dog)//true
peiqi.bark()
console.log(peiqi.bark === shinubi.bark)//true

“构造函数”一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。

每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。

需要注意的是与在构造函数中的 this 定义的属性会在每一个实例中被新创建一份不同,在 prototype 对象中增加的方法虽然可以被所有的实例继承到,但是所有的实例拿到的该方法其实都是一个引用,都指向同一个地址

每一实例都有一个 constructor 属性,它引用自父类的prototype上的constructor 该值为父类即构造函数,即指向父类,有
peiqi.constructor === Dog.prototype.constructor === Dog

此外:

  • 实例对象有一个__proto__属性,它指向父类的原型(prototype);
  • 父类的原型(prototype)中有一个公有方法isPrototypeOf()方法可以判断是不是该父类的实例.
  • hasOwnProperty()方法可以判断方法是不是实例自己本地的方法,而不是引用自父类的 prototype 中。
  • in 就可以判断包括原型链在内所有的方法;for in遍历对象所有的属性包括继承自原型链中的属性。
1
2
3
4
5
6
7
8
console.log(peiqi.__proto__ === Dog.prototype)//true
console.log(Dog.prototype.isPrototypeOf(peiqi))//true
console.log(peiqi.hasOwnProperty("bark"));
console.log("bark" in peiqi);

for (const key in peiqi) {
console.log(key);
}//name size bark

继承

下面的代码展示了基于构造函数五种继承方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
function Animal(){
this.species = 'animal'
}
Animal.prototype.showId = '动物'
/**
* 在构造函数中使用call或者apply的方式继承
*/
function Dog(name,size){
Animal.apply(this)
this.name = name || '二哈'
this.size = size || '中等'
}

let peiqi = new Dog('peiqi','肥')

console.log(peiqi.species);


/**
* 将子类的原型指向父类的实例
* 注意此种继承要多一步修正子类的原型上的 constructor
*/
function Pig(name,size){
this.name = name || '悟能'
this.size = size || '大'
}

Pig.prototype = new Animal()
Pig.prototype.constructor = Pig

let fatherPeiqi = new Pig('八戒','huge')

console.log(fatherPeiqi.species);

function Monkey(name,size){
this.name = name || "悟空"
this.size = size || '可小可大'
}
//此种方式直接让子类的原型指向父类的原型实现继承,缺点是变动子类的原型相当于变动父类的原型
Monkey.prototype = Animal.prototype

Monkey.prototype.constructor = Monkey

dasheng = new Monkey('大圣','大')
console.log(dasheng.showId);
console.log(Animal.prototype.constructor);//Mokey

/**
* 利用中间空白类来实现干净的继承并且不会占用创建实例的内存
* 综合了实例继承法的干净,和直接原型继承不耗费内存
*/

function midClass (){}

midClass.prototype = Animal.prototype

function People(name,size){
this.name = name || '唐僧'
this.size = size || '中等'
}

People.prototype = new midClass()
People.prototype.constructor = People

let tangseng = new People('玄奘','还好')

console.log(tangseng.showId);

/**
* 拷贝继承
*/

function copyExtend(Child,Parent){
for (const key in Parent.prototype) {
if (Parent.prototype.hasOwnProperty(key)) {
Child.prototype[key] = Parent.prototype[key]
}
}
}

function Cat(){}

copyExtend(Cat,Animal)

let tom = new Cat()
console.log(tom.showId);//动物

上面的代码里面已经基本上说明了继承的几种方式,那么JS 是否能实现多继承呢?

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function Animal(){
this.species = 'animal'
this.important = 'brain'
}
Animal.prototype.run = function(){
console.log('跑');
}

function People(sex){
this.important = 'people-brain'
this.sex = sex || 'YAO'
}
People.prototype.eat = function(){
console.log('吃饭');
}

function Man(name,age,sex){
Animal.call(this)
People.call(this,sex)
this.name = name || 'ray'
this.age = age || 18
}

for (const key in Animal.prototype) {
if (Animal.prototype.hasOwnProperty(key)) {
Man.prototype[key] = Animal.prototype[key]
}
}
for (const key in People.prototype) {
if (People.prototype.hasOwnProperty(key)) {
Man.prototype[key] = People.prototype[key]
}
}

let ray = new Man('ray',25,'man')

ray.run()
ray.eat()
console.log(ray.important);
console.log(ray.sex);
console.log(ray.species);

可见,Man 类实现了继承 Animal 类和 People 类,但是当面对两个父类都有的 important属性时,Man类会选择后继承的那个,本例中后调用的People.call(this,sex)所以 Man 从 People那里继承了 important

ES6 的 class

ES6中引入了 class 关键字,使得面向对象语法层面上更加清晰,本质与ES5没有变,但还是会多少有些不同的点了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Animal{
constructor(){
this.species = 'animal'
}
run(){
console.log('跑!');
}
}

class People extends Animal{
constructor(name,sex){
super()//必须在子类的构造函数中调用super()
this.name = name || 'ray'
this.sex = sex || 'man'
}
eat(){
console.log(`${this.name}正在吃饭`)
}
}

let ray = new People()

console.log(ray.species)// animal
console.log(ray.sex)// man
ray.run()// 跑!
ray.eat()// ray正在吃饭

上述就是 ES6 中 class 的基本用法,首先需要明确的是不管是 constructor 还是在类里面定义的方法,其实都定义在该对象的 prototype 上的,不一样的是用 ES5 那种直接 People.prototype.eat = ..的方式定义的方法是可枚举的,但是在 class 中所定义的方法都是不可枚举的

与ES5像定义函数(使用 function 关键字)一样定义一个“类”不同的是,ES5的那个“类”可以像普通函数一样调用,但是 class 关键字声明的类并不能不加 new 关键字的方式直接调用

Tips:为了使ES5中的构造函数不会遭受那种不用 new 关键字,直接函数调用的风险,ES6 中引入了 new.target 这个属性。如果构造函数没有使用 new 命令创建则 new.target 的值是undefined。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function People(name,age){
if (new.target !== undefined) {
console.log(new.target)
this.name = name || 'ray'
this.age = age || 18
} else {
throw new Error('必须用new来生成实例')
}
}

let ray = new People('ray',22)

console.log(ray.name);//ray

People('ray',18)// Error

class 定义的类不存在变量提升

类的静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class People{
constructor(name2,age){
this.name2 = name2 || 'ray'
this.age = age || 18
}
static run(){
console.log(`${this.name2}在跑!`);
}
run(){
console.log(`${this.name2}在跑!`);
}
}
People.name2="人"
People.run()//人在跑!

let ray = new People('ray',25)

ray.run()//ray在跑!

所谓静态方法,就是在 class 里面使用 stactic 关键字定义的,不会被实例对象继承到的方法。另外需要注意的是 静态方法中的 this 指向的是该类,而不是该类的实例,静态方法可以和非静态方法重名。

父类的静态方法可以被子类继承到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Man extends People{
constructor(){
super()
}
static run(){
super.run()
console.log(`更是${this.sex}人在跑`);
}
}
Man.sex = "男"

Man.run()
// 人在跑!
// 更是男人在跑

子类会继承到父类的静态 run 方法,并且重写时可以用 super.run的方式调用父类的静态方法。

Tips: 有了静态方法,我们自然希望能有静态属性,那种在类中使用而不被实例对象可见的那种,目前只用一种方式可以为一个类添加静态属性,Man.sex = "男",静态属性不能直接写在类里面。

利用 new.target 在子类继承父类的时候是其值返回的是子类这一特点,可以构造出只能继承后才使用的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Animal{
constructor(){
if (new.target === Animal) {
throw new Error('该类不能被实例化!')
}else{
this.species = 'animal'
}
}
}

class Wolf extends Animal{
constructor(kind){
super()
this.kind = kind || '狼'
}
bark(){
console.log(`${this.kind}在叫!`);
}
}
let wolf = new Wolf()

console.log(wolf.species);//animal
wolf.bark()//狼再叫

let king = new Animal()//Error

ES6 的继承

前面已经聊了一些关于ES6的继承相关的用法,可以很明显的发现一点,在子类的 constuctor 中纷纷都调用了super()方法,即调用父类的构造函数,并且这是一个必要的操作,原因在于,ES6的继承和ES5的继承呈现出不一样的过程。

在 ES5 的构造函数模式下,无论是否继承父类,都会先去创造一个 this 这个 this 就是指向该构造函数的实例对象,有了这个 this 之后才会把 父类的方法往这个 this 上加。而 ES6 不同的是,子类没有 this 对象,必须先要从父类那里获得 this 对象之后,再去加工这个 this 对象。所以才 ES6 的 class 语法中必须先要在构造函数中调用super()获得 this 对象之后才能有后续的加工,所以不调用 super()除非你不写 constructor使用默认的本身就有的,要不然实例化子类就会报错。

关于super:

  • 当调用super()函数时表示的是调用父类的构造函数,此种方法只能存下在子类的构造函数之中
  • 当super作为对象使用的时候,在普通方法中 super 指代父类的原型 prototype
  • 在子类静态方法中,super指代的是父类
  • 在子类中通过 super 调用父类方法的时候,该父类方法中的 this 指向的是子类的实例对象。

参考

《ES6标准入门》–阮一峰

本文为原创文章作为学习交流笔记,如有错误请您评论指教
转载请注明来源:https://isliulei.com/article/JS面向对象总结/