类和类的继承
一、类的声明
JS中到处都是对象,它是一门基于对象的语言,JS代码的执行都依托于对象,利用对象产生各种具备关联性的上下文环境,JS代码才能正常运行。
但是我们都知道在ES5中,没有class,我们只是利用函数、new关键词、原型等方式实现类的概念和基本使用,但是随着技术的发展和编程语言的互相靠拢,只使用构造函数或者工厂模式去实现类的封装调用很不现实,代码不明确,容易让学习者和开发者产生误会。
所以 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
ES5声明类
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
let p = new Point(3, 4);
ES6声明类
class Point {
// 构造函数
constructor(x, y) {
this.x = x;
this.y = y;
}
// 原型函数
toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
}
上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。
Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
ES6的类,完全可以看做构造函数的另一个写法:
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
注:
- 依旧用new关键词从类里实例化对象;
- 类必须使用new调用,否则会报错(ES5的构造函数可以被当做一个函数调用);
由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。
class Point {
}
// 有时候根据代码逻辑的需求,我们在声明了类之后的某一处可能要为这个类添加若干个原型函数,如果使用 类名.prototype的方式添加,代码冗余会很高,所以可以使用Object.assign的方式
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
二、类的constructor函数
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
constructor方法默认返回实例对象(即this),也可以指定返回另外一个对象。
class Point {
constructor() {
return {
name: 'tom'
};
}
}
let p = new Point(); // {name: 'tom'}
注:
- 构造函数的prototype属性,在 ES6 的“类”上面继续存在;事实上,类中声明的所有方法都定义在类的prototype属性上面;
- 类的prototype对象的constructor属性,直接指向“类”的本身,这与 ES5 的行为是一致的;
三、类的属性和方法
class Point {
legs = 2; // 实例属性
_mouth = 1; // 私有属性
static eyes = 2; // 静态属性
constructor(name, sex) {
// 实例属性
this.name = name;
this.sex = sex;
// 实例方法
this.saysomething = function() {
console.log('saysomething');
}
this._getMoney(); // 调用私有方法
}
// 原型方法
run() {
console.log('run');
}
_getMoney () {
console.log(this._mouth);
}
// 静态方法
static eat() {
console.log('eat');
}
}
// 实例化一个对象
let p = new Point();
console.log(p.legs,p.name,p.sex); // 实例属性
console.log(Point.eyes); // 静态属性
p.saysomething(); // 实例方法
p.run(); // 原型方法
Point.eat(); // 静态方法
注:
- 静态属性和静态方法是类直接调用的属性和方法,类的实例无法使用。
- 私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。
- 私有属性和私有方法的
练习:封装一个分数类,可以进行约分、加、减、乘、除运算。
四、取值函数(getter)和存值函数(setter)
有时候类的某些属性需要进行特殊的处理之后才能被取值或者赋值。这个时候就要用到getter和setter函数。
如:
class People {
a = 1000; // 私有属性
get money() {
return this.a - 100;
}
set money(val) {
this.a = val * 2;
}
}
let p = new People();
// 取出money的值
console.log(p.money);
// 设置money的值
p.money = 3000;
console.log(p.money);
五、类的继承
- Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class Point {}
// ColorPoint通过extends继承Point
class ColorPoint extends Point {}
如果子类继承父类时,父类中有某些属性需要实参来确定,那必须要在子类的constructor构造函数中,用super函数向父类传参,否则浏览器会直接报错。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
// ColorPoint类继承Point
class ColorPoint extends Point {
constructor(color, x, y) {
// 通过super函数向父类传参
super(x, y);
// 绑定子类自己的属性
this.color = color;
}
}
// 从ColorPoint类中实例化一个对象
let cp = new ColorPoint('#000', 100, 45);
console.log(cp);
- Object.getPrototypeOf方法可以用来从子类上获取父类,可以用这个方法判断一个类是否继承了另一个类。
class Point {}
class ColorPoint extends Point {}
Object.getProtorypeOf(Point);
Object.getPrototypeOf(ColorPoint);
- super函数除了可以在子类中代表父类的构造函数之外,还可以作为对象使用,当它作为对象时,在类的普通函数中指父类的原型对象,在静态方法中,指向父类。
class Point {
// 实例属性
x = 0;
y = 0;
// 原型方法
move() {
this.x += 1;
this.y += 1;
}
// 静态方法
static print() {
console.log('父类的静态方法');
}
}
// ColorPoint继承Point
class ColorPoint extends Point {
color = '#000';
change() {
// 在原型方法中通过super调用父类的原型方法
super.move();
this.color = '#f00';
}
static printout() {
// 在静态方法中通过super调用父类的静态方法
super.print();
console.log('子类的静态方法');
}
}
let cp = new ColorPoint(); // 创建实例
cp.change(); // 调用实例的change函数
console.log(cp);
ColorPoint.printout(); // 调用类的静态方法