ES6 类的语法
四、类的其他书写方式
1. 可计算的成员名
2. getter和setter
3. 静态成员
4. 字段初始化器(ES7)
5. 类表达式
6. [扩展]装饰器(ES7)
1、可计算的成员名
比如,
方法的名字 print,来自一个变量 printName,
我们希望用变量的值作为方法的名字,以前可以用 [ 属性表达式 ] 赋值
const printName = "print"; // 变量printName的值是print function Animal(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } Animal.prototype[printName] = function(){ // 在原型上用属性表达式,添加一个叫print的属性 var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } const a = new Animal('狗', '旺财', 3, '公'); a[printName](); // 这里用属性表达式
现在就用前面学过的ES6的 [ 可计算的属性表达式 ] ,把表达式的值作为类成员的名字
const printName = "print"; class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this.age = age; // Ps: 如果这里面就更简单了,跟以前的写法是一样的,比如this[sex] = sex; this.sex = sex; } [printName](){ // 可计算的属性表达式,用表达式的值作为成员名子 var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } const a = new Animal('狗', '旺财', 3, '公'); a[printName](); // 调用方法的时候的,一样的用这样的方式调用
2、getter 和 setter
getter 和 setter 对应到的,ES5里面的 Object.defindProperty 可定义某个对象成员属性的读取和设置
我们希望控制一下年龄age属性
1. 必须是数字
2. 不能为负数
3. 限制在一个范围内
写两个函数对年龄age属性,读取和设置进行控制
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; // this._age = age; 设置一个特殊的 _age 属性 this.setAge(age); // 可以改成 setAge(age) 设置年龄,把参数age放进去 this.sex = sex; } /** * 读取age年龄,不直接读取年龄,要经过一段代码,在后面加一个中文“岁”字,达到控制的目的 * 用下划线的_age,因为下面赋值的时候就保存在this._age属性里面 * */ getAge(age){ return this._age + "岁"; } /** * 通过这个setAge(age)函数设置年龄,就可以保障年龄在一个有效的范围内 * 1. 传一个age参数 * 2. 判断一下age * 3. 然后给一个特殊的属性名_age赋值 * */ setAge(age){ if(typeof age !== "number"){ throw new TypeError("age property must be number"); } if(age < 0){ age = 0; }else if(age > 1000){ age = 1000; } this._age = age; // 赋的值保存到 this._age 里面 } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this._age} // 这里也用_age 【性别】${this.sex}`; console.log(str); } } var a = new Animal('狗', '旺财', 3, '公'); console.log(a); // Animal {type: '狗', name: '旺财', _age: 3, sex: '公'}
给age属性赋值要调用 setAge( -100 ) 函数
a.setAge(-100); console.log(a); // Animal {type: '狗', name: '旺财', _age: 0, sex: '公'} console.log(a.getAge()); // 0岁
Ps:
_age 为什么加下划线,不加下划线也可以,
下划线或特殊字符的目的是,提醒是这是一些特殊的属性,
一般不希望外面直接使用的东西,一般在外面加一个下划线,或者是一些特殊的属性
有的时候我们对属性值的设置和读取,有时候要进行一些限制,会经过一些代码来处理,普通属性是控制不了的,可以写成函数的方式,
如果写过Java,这样的代码就是java代码,java的属性不能直接访问,全部都要变成函数
而且我们发现这两个函数有一个特点,
读取属性值是不需要任何参数 getAge()
而数组赋值的时候是单参,需要传一个参数 setAge( 100 )
但是这样我们感觉不像一个属性了,
给年龄赋值要调用一个 setAage(100) 函数,从语法的角度上它不像是一个属性了,因为属性赋值是这样的 a.age = 24 形式
我们希望赋值和读取不调用函数,但是又希望进行控制,
这时候可以通过ES5中的 Object.defindProperty 设置属性的读取器和赋值器
如果在class类里面达到同样的怎么办呢?
在类里面设置读取器 getter ,赋值器 setter ,统称为访问器
1. 把控制的 age 属性写成一个函数 age()
2. 函数前面加上 get 就是读取器 get age(){}
函数前面加上 set 就是赋值器 set age(age){}
3. 最后要赋值给 this._age
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this.age = age; // 2.对象里多了一个age属性,给age属性直接赋值就完事了,赋值的时候会运行age()函数 this.sex = sex; } // 1.创建一个age属性,并给它加上getter,读取该属性的时候会运行该函数 get age(){ return this._age + "岁"; } // 1.创建一个age属性,并给它加上setter,给该属性赋值的时会运行该函数 set age(age){ if(typeof age !== "number"){ throw new TypeError(age + " property must be number"); } if(age < 0){ age = 0; }else if(age > 1000){ age = 1000; } this._age = age; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } const a = new Animal('狗', '旺财', 3, '公'); console.log(a); // Animal {type: '狗', name: '旺财', _age: 3, sex: '公'} console.log(a.age); // 3岁
对象里多了一个 age: (...) 属性, 注意,使用getter、setter控制的属性,控制的 age 属性不在原型上,在实例上
赋值的时候会运行 set age(-100) 函数
读取会运行这个 get age() 函数,把函数的返回结果"0岁",当做属性值
a.age = -100; console.log(a.age()); // 0岁
也可以只写一个 get age,不写 set age ,就只能读不能赋值
如果通过 Object.defineProperty() 写成ES5代码,这样略显麻烦
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this._age = age; Object.defineProperty(this, "age", { set(age){ if(typeof age !== "number"){ throw new TypeError(age + " property must be number"); } if(age < 0){ age = 0; }else if(age > 1000){ age = 1000; } this._age = age; }, get(){ return this._age + "岁"; } }); this.sex = sex; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } const a = new Animal('狗', '旺财', 3, '公'); console.log(a); // Animal {type: '狗', name: '旺财', _age: 3, sex: '公'} a.age = -100; console.log(a.age); // 0岁 a.print(); // 【种类】狗 // 【名字】旺财 // 【年龄】0岁 // 【性别】公 for(key in a){ console.log(key); } // type // name // _age // sex
Object.defineProperty 方法(详解)
https://blog.csdn.net/weixin_46726346/article/details/115913752
3、静态成员
因为函数的本身也是对象,我们给构造函数本身加的一些成员就叫静态成员
实例成员
1. 通过构造函数构造出对象上面的成员叫实例成员,比如 this.type、this.name、this.age、this.sex
2. 对象原型上(__proto__)的成员也叫做实例成员,比如 print(),
3. 都是通过对象点来访问的
const a = new Animal('狗', '旺财', 3, '公'); console.dir(a);// Animal {type: '狗', name: '旺财', _age: 3, sex: '公'}
静态成员
构造函数本身的成员,静态成员只能直接通过“构造函数”本身来访问的(不能通过创建出的对象来访问)
console.dir(Animal);
因为函数也是对象可以通过 console.dir() 来访问,构造函数里面的成员 length、name、arguments 这些叫做静态成员
什么情况下会用到静态成员呢?
比如做一个象棋游戏,有一个棋子的类 Chess,每个棋子都有自己的属性,
棋子名 name
棋子宽 width
棋子高 height
class Chess{ constructor(name){ this.name = name; this.width = 50; this.width = 50; } } // 创建棋子 const = new Chess('车'); const = new Chess('车'); const = new Chess('将');
每个棋子的宽高都是一样的,
这样不仅浪费内存,还觉得有点怪,要得到棋子的宽高,先要创建一个棋子的对象,
宽高不应该作为实例属性,应该当做为静态属性,而静态成员不需要依赖对象
以前,直接在函数上定义静态成员,但是这种写法不好,又和类分开了
class Chess{ constructor(name){ this.name = name; } } // 直接在函数上定义静态成员 Chess.width = 50; Chess.height = 50; // 获取静态成员 console.log(Chess.width); // 50 console.log(Chess.height); // 50 // 构造出来的对象上没有静态属性 console.log(new Chess('车').width); // undefined console.log(new Chess('车').height); // undefined
ES6中静态成员写在类里面,使用关键字 static 定义静态成员(static是静态的意思)
class Chess{ constructor(name){ this.name = name; } // 定义静态成员 static width = 50; static height = 50; // 也可以定义成静态方法 static method(){ } }
调用静态属性
console.log(Chess.width); // 50 console.log(Chess.height); // 50
调用静态方法
Chess.method();
通过 congsole.dir 打印,静态成员有 width、height、length、name、arguments: (...),
console.dir(Chess);
而对象里面没有静态成员,返回undefined
console.log(new Chess('车').width); // undefined
4、字段初始化器(ES7)
字段初始化器是ES7的语法,就是写了一个属性,然后直接写等号赋值 static width = 50;
class Chess{ constructor(name){ this.name = name; } static width = 50; // 这是字段初始化器(es6不可以这样写,es7提供了字符初始化器,可以支持赋值) static height = 50; // 这是字段初始化器 static method(){ } } console.log(Chess.width); // 50 console.log(Chess.height); // 50
ES6里面可以写静态方法,但是不能写静态属性,但是在ES7里面可以写静态属性了,因为ES7里面提供字符初始化器,就是写成员的时候可以直接赋值
比如,
这三个属性本身就具有默认值,不需要在构造函数里面对它进行初始化,这样的属性可以用ES7的语法字段初始化器的语法来写
class Test{ constructor(){ this.a = 1; this.b = 2; this.c = 3; } }
ES7的 字段初始化器 的语法,
属性直接写在 class 类里,就相当于写在 constructor 构造函数里面
class Test{ static a = 1; b = 2; c = 3; // constructor(){} 构造函数没有内容,可以不写 } const t = new Test(); console.log(t); // Test {b: 2, c: 3} console.log(Test.a); // 1
1. Test {b: 2, c: 3} 创建出的对象 t 里面有 b属性 和 c属性,是对象的实例成员,不在原型里面在对象里面的
2. Test.a a是静态成员在 Text 里面
对于一些成员一开始就有固定的值,用字段初始化器的是非常好用的,不用管兼容性问题,兼容性问题以后用 Babe 来解决
其实最终是在创建 new Test() 对象的时候,在构造函数里面自动完成赋值了,只是写的时候可以上在构造函数外面写
在构造构造函数里面加一条 this.d = this.b + this.c 这是一道面试题问 d 输出的是多少?
1. ES7提供的字段初始化器,语法上 b 和 c 两个属性写在外面,相当于放到构造器里了
2. 所以d是 5
class Test{ static a = 1; b = 2; c = 3; constructor(){ // 字段初始化器语法,相当于把属性b和c,这两句放在构造器里 // this.b = 2; // this.c = 3; this.d = this.b + this.c; } } const t = new Test(); console.log(t.d); // 5
要非常特别注意的两个点
1. 使用 static 修饰的字段初始化器,添加的是静态成员(这点没什么好说的,上面已经看到了)
2. 另外没有使用 static 的字段初始化器,添加的成员位于对象上(位于这个构造函数创建的对象上面)
为什么要特别注意呢?
1. 这样写是没有什么问题的
class Test{ constructor(){ this.a = 123; } print(){ console.log(this.a); // this指向当前对象 } } const t = new Test(); t.print(); // 123
2. 如果有人这样下面这样调用 print() 方法,this指向就出了问题,因为在类里面语法都在严格模式下,严格模式下函数里this是undefined,它在不指向window
class Test{ constructor(){ this.a = 123; } print(){ console.log(this.a); // Uncaught TypeError: Cannot read properties of undefined (reading 'a') } } const t = new Test(); const p = t.print; // 把print函数体赋值给变量p p();
担心的到不是上面,
以后在一些特殊场景下,this可能会出现问题,于是我们会这样写
3. 我们利用了字段初始化器,给 print 赋值一个箭头函数
class Test{ constructor(){ this.a = 123; } print = () => { // 箭头函数在字段初始器的位置上,this指向当前对象 console.log(this.a); } } const t = new Test(); const p = t.print; p();
箭头函数在字段初始器的位置上指向当前对象,也就是说this指向当前对象
4. 为什么字段初始化器位置上,this指向的是当前对象?因为字段初始化器相当于在构造函数里面写了 this.print 属性
class Test{ constructor(){ // this.print = () => { 2. 字段初始化化器,相当于在构造器里写this.print属性,这个位置的this就是当前对象 // console.log(this.a); // } this.a = 123; } print = () => { // 1.字段初始化化器, console.log(this.a); } } const t = new Test(); const p = t.print; p(); // 123
因为箭头函数没有this,它this一定外层位置的this,指向定义箭头函数位置的this,所以这个时候的this指向的是当前对象
但是一定注意,这样一来跟之前是有区别的,
之前写正常写的 print() 方法在原型上面,
现在的 print() 方法不在原型上了,在对象上面,也就是说通过 Test 创建的对象,每一个对象里都会有一个print
class Test{ constructor(){ this.a = 123; // this.print = () => {} 每创建一个对象上,都新建了一个print()属性,就想成员a一样 } print = () => { console.log(this.a); } } const t1 = new Test(); const t2 = new Test(); console.log(t1.print === t2.print); // // 返回false表示不一样,每一个对象都有自己的print
坏处,占用内存空间,因为每个对象都有一个print方法
好处,在于绑定了this
以后有的时候回出现这种用法
5、类表达式
由于 类 本质上是构造函数,在JS底层会把类转化成函数,因此类也是可以写表达式的
把整个类赋值给变量A,等号后面这块 class {} 就是一个类表达式,相当于一个匿名类(表达式也不需要名字,当然写名字也可以)
const A = class { }
类表达式里面的写法是一样
const A = class{ a = 1; constructor(b){ this.b = b; } } const a = new A(2); console.log(a); // {a: 1, b: 2}
有时候一个函数调用过后,会返回一个类,所以类和函数本质一样,也是JS的一等公民,
我们可以把一个类当做一个参数传递,也可以把一个类当成一个返回值,
可以实现很多灵活的应用,因为类的本质上在JS里面就是函数。
6、[扩展]装饰器(ES7)
装饰器(Decorator)是ES7的知识,还没有成为正式标准
class Test{ @Obsolete(Test, print, {}) // 1.@装饰器名,装饰器本身是一个函数 print(){ console.log('print方法'); } } /** * @param {} target 类名Test * @param {} methodName 方法名print * @param {} descriptor */ functon Obsolete(target, methodName, descriptor){ // 2.这里要写一个对应的函数 console.log(target, methodName, descriptor); }
Object.defineProperties(obj, prop, {}); 第三个参数就是 descriptor
console.warn()
五、类的继承
在面向对象里面有一种“类之间的关系”叫做继承 extent
类是一个什么东西呢?
类就是一个类型,
JS里面本身就提供了一些类型,比如字符串、数字、布尔、对象...,而实际上这些类型是不够的,
比如我们要做一个文章管理系统,那还要有文章类型Article,我们上面还有写过一个 Animal 动物类型,
类型与类型之间,有一种关系的就叫继承
继承描述的是一种什么关系呢?
如果两个类A和B,如果可以描述为:B 是 A,则,A和B形成继承关系
比如说,
猫是动物,猫和动物之间是一个继承关系
狗是动物,狗和动物之间是一个继承关系
再比如说,
男人是人,女人是人,
男人和人之间形成继承关系,女人和人之间形成继承关系
继承关系有什么意义呢?
狗是动物,狗里面有动物所有的特征,
动物该有的东西狗都应该有,因为狗是一个动物
具体到代码层面,如果形成继承关系,
B是A,B里面就拥有A里面的所有东西
之前,
成哥的公益课里面的 圣杯模式 ,就已经提到了继承
继承关系在面向对象领域还有很多说法
如果B是A(如果狗是动物),则有下面的一系列说法,
1. B继承自A (狗是继承自动物)
2. A派生B (动物派生狗)
3. B是A的子类 (狗是动物的子类)
4. A是B的父类 (动物是狗的父类)
这些说法全是一样的意思
以上这些关系形成之后,
如果A是B的父类,则B会自动拥有A中的所有实例成员,包括方法,这是继承需要在代码层面表达出来的含义,
逻辑上也是这样,
狗既然是动物,动物该有的东西,狗都一定有
动物有类型,狗也得有类型,
动物有名字,狗也得有名字,
动物有年龄,狗也得有年龄,
但是狗特有的东西,动物里面就不一定有了
这种继承关系,在面向对象开发的时候特别重要,
有很多类的话,可以把一些共同的成员抽象出来,特别是在游戏里特别明显,
比如做一个飞机大战,有各种各样的飞机,不同的飞机是一个类,这些飞机共同的特征,可以提取出一个父类
罗翔老师:“一个知识越贫乏的人,越是拥有一种莫名奇怪的勇气,和一种莫名奇怪的自豪感。
1、以前在构造函数里如何做一个子类呢?
Dog继承Animal,
既然Dog是Animal的自类,我们希望子类里面拥有父类里面的成员,
以前的做法是,借用父类 Animal 的构造函数,把父类的构造函数运行一下,就不用写重复代码了
function Animal(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } Animal.prototype.print = function(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } function Dog(name, age, sex){ Animal.call(this, '犬类', name, age, sex); // 借用父类的构造函数,把this绑定进去,把父类的构造函数运行一次,就不用写重复代码了 } const d = new Dog('旺财', 3, '公'); console.log(d); // Dog {type: '犬类', name: '旺财', age: 3, sex: '公'}
但是现在有一个问题,
狗对象的原型链上没有 print() 方法,因为 Animal 原型上的东西没有继承过来
狗对象的原型链是这样,
狗对象的 d.__proto__ 隐式原型指向 -> 构造函数Dog的原型 Dog.prototype
构造函数Dog的原型 Dog.prototype 的隐式原型 __proto__ 指向 -> Object.prototype
Object.prototype 里面的隐式原型 __proto__ -> 指向 null
想要形成继承关系
要把狗对象的隐式原型 Dog.prototype 指向 -> Animal.prototype ,就形成了继承关系
这样才能叫 Dog 继承自在 Aniam
1. 狗对象可以顺着原型链找到 print 方法,
2. 到时候给狗对象 Dog.prototype 原型上加特别的函数,不会影响父类 Animal.prototype ,因为 Dog.prototype 是狗特有的东西,动物 Animal.prototype 只提供动物公共的东西
这种继承关系该怎么设置呢?
把Dog构造函数原形 Dog.prototype 的隐式原型 __proto__ -> 设置为Animal的原型 Animal.prototype ,以前要用圣杯模式写一大堆代码
现在用专门设置隐式原型的 Object.setPrototypeOf() 方法把第一个参数的对象的隐式原型,设置为第二个参数
function Animal(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } Animal.prototype.print = function(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } function Dog(name, age, sex){ Animal.call(this, '犬类', name, age, sex); } Object.setPrototypeOf(Dog.prototype, Animal.prototype); // 把构造函数Dog.prototype的隐式原型 __proto__ 设置为Animal.prototype const d = new Dog('旺财', 3, '公'); console.log(d.__proto__); // Animal {constructor: ƒ} console.log(d.__proto__.__proto__); // {print: ƒ, constructor: ƒ} d.print(); // d对象调用print()就没问题了
对象狗的隐式原型__proto__ 是 Animal,
在Animal的隐式原型上 __proto__ 有print方法,对象狗调用 d.print() 就没有问题了
应该是这个意思,自己琢磨,修改的是 __proto__
Dog.prototype.__proto__ = Animal.prototype;
2、ES6的语法格式可以高度聚合某一个类,同样继承也特别方便
新关键字
1. extends 表示继承,用于类的定义(它的父类是谁)
2. super 两种用法,1). 直接当做函数调用,表示父类构造函数。2). 第二种,如果当做对象使用,则表示父类的原型
语法非常简洁,一眼就看出是继承关系
1. class Dog extends Animal {} Dog 继承自 Animal
2. 子类的构造函数里,要调用父类的构造函数,依然可以用 Animal.call( this, '犬类', name, age, sex ) 不好恶心,
现在使用class类的语法,直接调用 super('犬类', name, age, sex) 表示父类的构造函数,不用绑定this
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } class Dog extends Animal { constructor(name, age, sex){ super('犬类', name, age, sex); // super表示父类Animal的构造函数,不用绑定this了 } } const d = new Dog('旺财', 3, '公'); console.log(d.__proto__); // Animal {constructor: ƒ} console.log(d.__proto__.__proto__); // {constructor: ƒ, print: ƒ} d.print();
注意:
1. ES6要求,如果是定义了 constructor ,并且该类是子类,则必须在 constructor 的第一行,手动调用父类的构造函数
2. 如果子类不写constructor,则会有默认的构造器,该构造器需要的参数和父类一致,并且自动调用父类的构造器
如果子类写了constructor,不调用 super() 会报错
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } class Dog extends Animal { constructor(name, age, sex){ // super('犬类', name, age, sex); 如果构造器constrcutor里面不调用super } } const d = new Dog('旺财', 3, '公');
报错信息,
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new Dog
Must call super constructor 必须调用父类的构造函数,
整个报错语言的意思是,必须在子类构造函数最开始的位置,用super调用父类的构造函数
如果Dog子类不写构造函器constructor呢?
1. 子类不写构造函器,就不会报错
2. 子类会有默认的构造器,就相当于写了一个和父类一模一样的构造器,
3. 然后在构造器里只做了一件事,就是调用 super(type, name, age, sex) 把参数传间,所以数据错位了
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } class Dog extends Animal { // constructor(name, age, sex){ 子类不写构造函数,不会报错 // super("犬类", name, age, sex); // } } const d = new Dog('旺财', 3, '公'); d.print(); // 少了一个默认类型"犬类",所以print()方法输出的数据会错位 // 【种类】旺财 // 【名字】3 // 【年龄】公 // 【性别】undefined
可以不写,但是要知道不写的默认清空,是什么情况
ES6的继承,
会自动搞定原型链,a.print() 方法打印没有问题
console.log(d.__proto__); // Animal {constructor: ƒ} console.log(d.__proto__.__proto__); // {constructor: ƒ, print: ƒ} d.print();
继承是单线的,不能继承多个父类,既只有一个父类,
父类可以再有父类,形成一条链,但是不能一类继承多个父类
在子Dog类里面,加一些自己特有的属性
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } class Dog extends Animal { constructor(name, age, sex, loves){ super('犬类', name, age, sex); this.loves = loves || "吃骨头"; // 子类特有的属性 } shout(){ // 子类特有的方法 return "旺旺"; } } const d = new Dog('旺财', 3, '公'); console.log(d); // Dog {type: '犬类', name: '旺财', age: 3, sex: '公', loves: '吃骨头'} console.log(d.shout()); // 旺旺
在狗的原型上 Dog.prototype 上加一个shout() 方法
如果父类 Animal.prototype 上面也有一个同名的 shout() 方法呢?
因为狗对象的原型链先找到 Dog.__proto__ 上的 shout() 方法,这叫方法覆盖
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } shout(){ throw new Error("动物怎么叫?Animal是一个高度抽象的父类,所以这里抛出一个错误"); } } class Dog extends Animal { constructor(name, age, sex, loves){ super('犬类', name, age, sex); this.loves = loves || "吃骨头"; } shout(){ // 会覆盖掉父类的同名方法 return "旺旺"; } } const d = new Dog('旺财', 3, '公'); d.print(); console.log(d.shout()); // 对象调用的是子类的shout()方法
子类同名 shout() 方法会覆盖父类的 shout() 方法,
覆盖不是不存在了,是要通过子类生成的对象d,调用才能达到覆盖的效果
super 的第二种用法,如果当做对象使用,则表示父类的原型
子类的增加了一个 loves 新属性,希望在 print 方法里的属性一起输出,如果在子类里把 print 方法代码重新写一遍,会造成代码的重复
print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex} 【爱好】${this.loves}`; // 把Dog特有的loves属性加上 console.log(str); }
super 表示父类的原型 Animal__proto__ ,super调用父类 print 方法里的内容,既没有重复代码,又可以解决问题
class Animal{ constructor(type, name, age, sex){ this.type = type; this.name = name; this.age = age; this.sex = sex; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } class Dog extends Animal { constructor(name, age, sex, loves){ super('犬类', name, age, sex); this.loves = loves || "吃骨头"; } print(){ var str = super.print(); // super当做对象使用,表示父类的原型,super.print()调用父类上方法里面的内容 str = `【爱好】${this.loves}`; console.log(str); } } const d = new Dog('旺财', 3, '公', '狗粮罐头'); d.print();
如果在子类的方法里,想调用父类方法的内容用 super.父类的方法名
【种类】犬类
【名字】旺财
【年龄】3
【性别】公
【爱好】狗粮罐头
六、冷知识
1、用JS模拟抽象类 [冷知识一]
什么是抽象类,
抽象类一般是父类,不能通过该类创建对象
Animal动物是一个高度抽象的概念,它是一个统称,不具体存在,不存在没有动物,
世界上存在着一只狗、或一只猫,不存在动物
所以不能创建一个动物的对象,Animal类不应该创建对象,不希望能调用父类的构造函数,
在很多语言里,有语法支持不能让一个类创建对象,目前在JS里还没有这样的语法,将来可能会有
用JS模拟一个抽象类的概念,
如果 new Aniaml() 是能够运行,但是不符合逻辑的,所以我们先用 new.target 判断一下,
1. 如果是 Animal 抛出一个错误
2. 创建 new Dog() 不会报错
class Animal{ constructor(type, name, age, sex){ if(new.target === Animal){ throw new TypeError("你不能创建Aimal对象,应该通过子类创建"); } this.type = type; this.name = name; this.age = age; this.sex = sex; } print(){ var str = ` 【种类】${this.type} 【名字】${this.name} 【年龄】${this.age} 【性别】${this.sex}`; console.log(str); } } class Dog extends Animal { constructor(name, age, sex, loves){ super('犬类', name, age, sex); this.loves = loves || "吃骨头"; } print(){ var str = super.print(); str = `【爱好】${this.loves}`; console.log(str); } } // new Animal(); // Uncaught TypeError: 你不能创建Aimal对象,应该通过子类创建 new Dog('动物', '旺财', 3, '公').print();
冷知识二
正常情况下,this始终指向具体类的对象