class 关键字定义类
在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类;但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;
怎么定义一个类:(和 function 关键字很类似)
- 类声明:
class Object {}
- 类表达式:
let Object = class {}
```javascript // 类的声明 class Person { }
// 类的表达式 var Animal = class { }
// es6的类和构造函数非常类似,因为class其实是语法糖,所以类拥有构造函数一切的原型操作 console.log(Person.prototype) // {} console.log(Person.prototype.proto) // [Object: null prototype] {} console.log(Person.prototype.constructor) // [class Person]
// 因为 typeof 中没有类的类型,所以还是识别出是函数 console.log(typeof Person) // function
var p = new Person() console.log(p.proto === Person.prototype) // true
<a name="vk6wR"></a>
# 类的构造函数
class 定义类,我们会发现一个问题。之前构造函数就是类,函数可以接收参数,然后来初始化对象。现在变成 class 了,还怎么接收参数初始化对象呢?<br />为了解决这个问题,所以就在类中设计了一个固定方法`constructor()`,把它当“构造函数”。每次实例化对象都会自动调用这个方法。并且每个类只能有一个`constructor()`函数。这个设计从外表上看和 Java 的构造函数是很像的。
new 实例化 class 定义的类时,会调用这个 constructor 函数,具体的执行细节:(和之前 new 构造函数一模一样)
1. 在内存中创建一个新的对象(空对象);
2. 这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;
3. 构造函数内部的this,会指向创建出来的新对象;
4. 执行构造函数的内部代码(函数体代码);
5. 如果构造函数没有返回非空对象,则返回创建出来的新对象;
```javascript
class Person {
// 构造函数
constructor(name, age) {
this.name = name,
this.age = age
}
}
类中的方法
类的实例方法
es6 之前实例方法需要定义到类的原型上,现在也是一样,只是简化了代码,可以直接写了。
class Person {
// 构造函数
constructor(name, age) {
this.name = name,
this.age = age
}
// 实例方法(Java 中的成员方法)
// 方法直接写,不用再这样赋值 Person.prototype.eat = function() {}
eat() {
console.log(this.name + ' eating');
}
}
new Person('zs', 18).eat() // zs eating
console.log(Person.prototype.eat === new Person().eat); // true
类的访问器方法(setter,getter)
属性描述符时有讲过定义类时可直接添加 setter 和 getter 函数,class 定义的类中也可以,并且语法也一样。
class Person {
constructor(name) {
this.name = name,
this._gender = undefined // _gender 私有
}
// “方法名”被注册为一个向外暴露的实例属性
set gender(value) {
console.log('_gender 被赋值了 ' + value);
this._gender = value
}
get gender() {
console.log('_gender 被读取了');
return this._gender;
}
}
let p = new Person()
p.gender = 'man'
console.log(p.gender)
// _gender 被赋值了 man
// _gender 被读取了
// man
类的静态方法(类方法)
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义
class Person {
constructor(name) {
this.name = name
}
// 静态方法
static createObj(password) { // 定义一个工厂方法来掩盖 new 产生对象过程
return password == 123 ? new Person('hhh') : '爬爬爬'
}
}
console.log(Person.createObj(123)); // Person { name: 'hhh' }
ES6类的继承 = extends + super
es6 提供了 extends
关键字完成了继承,相当于9. 原型链与继承中封装的inheritPrototype()
函数,只是继承了方法,属性继承得用 super
关键字。
super
关键字有两个作用:
- 在子类的构造函数中调用父类的构造函数,继承属性
在子类的实例方法或静态方法中调用父类的实例方法或静态方法,复用父类逻辑 ```javascript class Person { constructor(name) { this.name = name }
eat() { console.log(this.name + ‘ eating’); }
static happy() { return ‘放开玩吧’ } }
class Student extends Person { constructor(sno, name) { super(name) // 相当于 Person.call(this, name),属性还是定义到了Student实例对象自己身上 this.sno = sno }
// 重写父类方法 eat() { console.log(this.name + ‘今晚吃啥啊’); }
// 复用父类方法逻辑 sleep() { super.eat() // 调用了父类的方法 console.log(‘这个年纪你睡得着觉哒?’); } static unhappy() { console.log(‘来不及了,’ + super.happy()); } }
let stu = new Student(111, ‘zs’)
console.log(stu); // Student { name: ‘zs’, sno: 111 } —拥有了父类的属性
stu.eat() // zs今晚吃啥啊 —调用的是子类重写的方法
stu.sleep() // zs eating,这个年纪你睡得着觉哒?—复用了父类方法的逻辑 Student.unhappy() // 来不及了,放开玩吧
[babel官网](https://babeljs.io) 中 Try it out 模块提供了代码的实时转换,有兴趣可以看看这些继承语法糖是怎么实现的。
阅读源码可能会遇到的问题:
1. 浮躁
2. 看到后面忘记前面的东西
1. 位置打标记
2. 函数写注释,提醒参数
3. 读完一个函数不知道函数要干嘛,读得多了就知道了
4. debugger
<a name="xjkX9"></a>
## 继承内置类
可能会要去增强内置类的功能,这时候就要去继承内置类
```javascript
class MyArray extends Array {
firstItem() { // 获取数组第一个元素
return this[0]
}
lastItem() { // 获取数组最后一个元素
return this[this.length-1]
}
}
var arr = new HYArray(1, 2, 3)
console.log(arr.firstItem()) // 1
console.log(arr.lastItem()) // 3
类的混入 mixin
JavaScript 的类和 Java 一样只支持单继承:也就是只能有一个父类。当我们想要获得两个类的功能时,就可以使用类的混入。
有些语言原生提供了类的混入能力,而 JavaScript 没有。但我们可以利用一些手段达到目的。比如类C 想要继承 类A 和类B 的部分功能,既然不能同时继承,那就一个一个继承就完了。类C 继承类B,类B 继承类A,这样类C 就能同时拥有另外两个类的功能。
所以JavaScript 实现类的混入,就是在一个方法中设置中间类的继承,并将中间类返回。
- 这个思想其实和9. 原型链与继承中道格拉斯大佬实现原型式继承的思想一样,利用了一个中间人作转接。 ```javascript // 假设 Student 只能继承 Person,而Person是个第三方类,无法被更改,而Student 又需要 fly 方法 class Person { constructor(name) { this.name = name } eat() { console.log(this.name + ‘ eating’) } }
// 在 Person 类中混入 fly 方法 function mixinFly(BaseClass) { return class extends BaseClass { // 匿名中间类 fly() { console.log(‘flying~’) } } }
// Student 类继承混入 fly 方法后的 Person 类 class Student extends mixinFly(Person) { constructor(name) { super(name) } }
let p = new Student(‘zs’)
p.fly() // flying,子类对象拥有了混入的 fly 的方法 p.eat() // zs eating,也拥有父类的属性和方法
但是这种方法有个很大的缺点,因为方法内部的继承只是用了 extends,没有用 super,所以只继承了方法,没有继承属性。也就只能混入方法,无法混入属性。
react 的高阶组件中就有类似的设计场景<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22919157/1649260624402-e0c7c0af-daef-4924-9a94-9094df3de11f.png#clientId=uf53578d2-938f-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=430&id=ufe4d6b30&margin=%5Bobject%20Object%5D&name=image.png&originHeight=537&originWidth=531&originalType=binary&ratio=1&rotation=0&showTitle=false&size=230571&status=done&style=none&taskId=ub032e3f8-05e3-4ac4-a232-224c694a1d6&title=&width=424.8)
<a name="VzPVG"></a>
# JavaScript 中的多态
维基百科对多态的定义:多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一<br />个单一的符号来表示多个不同的类型。<br />说人话就是:不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现。
传统的面向对象语言体现多态,有三个前提
1. 继承
2. 子类重写父类的方法
3. 子类实例由父类类型来引用
```java
class Shape {
getArea() {
System.out.println(100);
}
}
class Rectangle extends Shape { // 继承
getArea() { // 重写
System.out.println(200);
}
}
class Circle extends Shape { // 继承
getArea() { // 重写
System.out.println(300);
}
}
Shape shape1 = new Rectangle();
Shape shape2 = new Circle();
shape1.getArea(); // 200
shape2.getArea(); // 300
// 不同的数据类型 Rectangle和Circle,进行同一个操作,getArea(),表现出不同的行为,200和300
而 JavaScript 的多态没有那么严格,比较松散,但也能达到多态概念的描述。JavaScript 的多态主要是体现在 js 的函数没有限制参数类型。
function foo(n, m) {
return n + m
}
console.log(foo(1, 2))
console.log(foo('月满', '西楼'))
// 不同的数据类型,数字和字符串,进行同一操作 foo,表现出不同行为,一个相加一个拼接
class Car {
getArea() {
return 100
}
}
class Person {
getArea() {
return 200
}
}
// 核心还是参数不限制类型,这样两个类都能被接收
function calcArea(foo) {
console.log(foo.getArea())
}
var person = new Person()
var car = new Car()
calcArea(person) // 200
calcArea(car) // 100