面向对象 封装,继承,多态
对于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)peiqi.bark() console .log(peiqi.bark === shinubi.bark)
“构造函数”一个普通函数,但是内部使用了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)console .log(Dog.prototype.isPrototypeOf(peiqi))console .log(peiqi.hasOwnProperty("bark" ));console .log("bark" in peiqi);for (const key in peiqi) { console .log(key); }
继承 下面的代码展示了基于构造函数五种继承方式:
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 = '动物' function Dog (name,size ) { Animal.apply(this ) this .name = name || '二哈' this .size = size || '中等' } let peiqi = new Dog('peiqi' ,'肥' )console .log(peiqi.species);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);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 () this .name = name || 'ray' this .sex = sex || 'man' } eat(){ console .log(`${this .name} 正在吃饭` ) } } let ray = new People()console .log(ray.species)console .log(ray.sex)ray.run() ray.eat()
上述就是 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);People('ray' ,18 )
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()
所谓静态方法,就是在 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);wolf.bark() let king = new Animal()
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面向对象总结/