类是面向对象的基础,是我们组织代码的一大利器,学好使用类会使我们可以更好的理解面向对象编程的思想,下边就整理一些JavaScript中类的知识点,看看类在JavaScript语言中是个怎么样的存在。
es5中的类
JavaScript语言早期是没有class这个概念,只能通过一些手段去模拟类。
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function () {
console.log(this.name);
};
const pt = new PersonType('lilei');
pt.sayName();
es6中的类
考虑到类实在使用的很多,es6中加入了类(class
),使用class来工作比es5中的方式要方便很多,代码如下所示:
class PersonType {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
const pt = new PersonType('lilei');
pt.sayName();
es6中类的底层实现机制
es6中的类其实底层还是使用es5中的原型链在工作,只是在其上面封装了一层语法糖。
上面的代码如果使用es5重写就是如下的样子
let PersonType = (function () {
'use strict';
const PersonType = function (name) {
if (typeof new.target === 'undefined') {
throw new Error('函数只能new');
}
this.name = name;
};
Object.defineProperty(PersonType.prototype, 'sayName', {
value: function () {
if (typeof new.target !== 'undefined') {
throw new Error('函数不能new');
}
console.log(this.name);
},
enumerable: false,
configurable: true,
writable: true
});
return PersonType;
})();
const pt = new PersonType('lilei');
pt.sayName();
从上面的代码可以看出es6的类存在如下特性:
1.类中的代码都是严格模式
2.类变量不会变量提升(因为考虑会影响到类的继承,父类一定要在子类面前先声明
)
3.类的构造函数如果用不是new的形式调用会报错。
4.类中定义的方法都是不可枚举的(typescript中类的方法都是可枚举的)
5.类中非构造函数使用new调用会报错
6.类名在类的内部不能修改(在类的外部是可以修改的)
类表达式
类除了基本声明方式定义类之外还有表达式形式,表达式有两种表达方式:
基本表达式方式
const PersonType = class{
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
};
命名表达式方式
const PersonType = class PersonType2{
constructor(name) {
this.name = name;
}
sayName() {
console.log('inner',PersonType2)
console.log(this.name);
}
};
console.log('outer',PersonType2)
命名表达式定义类时的名字(PersonType2)只能在类内部使用,外部无法识别
。代码中outer
处会报错,inner
处就可以正确读取到类。
注
:使用类表达式的方式有一大好处就是,可以使用立即调用函数的方式直接生成对象如下所示:
const pt = class PersonType2{
constructor(name) {
this.name = name;
}
sayName() {
console.log('inner',PersonType2)
console.log(this.name);
}
}();
console.log('outer',PersonType2)
这样,pt就直接是PersonType对象的实例,这种方式可以直接实现单例模式
。
可计算成员变量
es6中的类和对象字面量一样,属性和方法名可以动态计算。例如:
let methodName = 'sayName';
class PersonType {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
}
const pt = new PersonType('lilei');
pt.sayName();
类中的属性或者方法加上[]后会先解析变量,最终结果和之前的es6类例子结果是一样的。
生成器方法
es6中引入了生成器的概念,在类中可以直接使用生成器方法,定义的方法也很简单在方法名前加上*
就可以,代码如下:
class PersonType {
name;
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}
除了生成器方法,也可以给类定义默认迭代器,形式如下:
class PersonType {
name;
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
}
es6中定义默认迭代器就是使用Symbol.iterator
,Symbol是es6新引入的类型,Symbol.iterator是es6设定好的表示对象默认迭代器的标志。
静态成员
静态成员表示可以直接通过类来调用,不一定必须通过实例来调用。
es5中的静态成员
在es5中静态成员通过直接给函数上设置属性来模拟,代码如下:
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function () {
console.log(this.name);
};
PersonType.create = function (name) {
return new PersonType(name);
};
// const pt = new PersonType('lilei');
const pt = PersonType.create('lilei');
pt.sayName();
es6中的静态成员
es6中定义静态成员就非常简单了,在属性或者方法前加上static
就可以了。
class PersonType {
name;
constructor(name) {
this.name = name;
}
static create(name) {
return new PersonType(name);
}
sayName() {
console.log(this.name);
}
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
}
注意:
不可以在实例中访问静态成员,只能通过类来访问。
继承
es5中的继承
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getAreas = function () {
return this.length * this.width;
};
function Square(length) {
Rectangle.call(this, length,length);
}
Square.prototype = Object.create(
Rectangle.prototype,{
constructor:{
value: Square,
enumerable: true,
writable: true,
configurable: true
}
}
);
const square = new Square(3);
const result = square.getAreas();
console.log(square,result);
es6中的继承
对比es5的方式,使用es6的方式实现继承就非常简单了。
class Rectangle {
length;
width;
constructor(length, width) {
this.length = length;
this.width = width;
}
getAreas () {
return this.length * this.width;
}
}
export class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
和java语言一样,es6中继承一样使用extends
操作,使用子类
extends父类
的方式来完成继承。
这里需要注意的地方是super
方法,必须要在子类构造方法中通过super方法来调用父类的构造方法来完成初始化,尤其是手动写了构造方法,那必须手动添加super方法,如果你在派生类中没有添加构造函数,那么类会自动在构造函数中添加super方法。这里还有一点需要注意的是,派生类中的super方法必须要在访问this之前调用super方法,如果在super方法之前访问了this就会报错
。
如果当真不想调用super方法,只有让构造函数直接返回一个对象,一般不要这么干。
类方法遮蔽
继承中派生类会遮蔽掉父类的方法,这个和原型链继承的表现是一样的。
假如在Square类中也有个getAreas方法,那么Rectangle中的getAreas方法在Square类的实例中将无法调用。
如果有特殊需求希望直接调用父类的方法,那么也可以在子类中直接通过super
调用父类的方法。
class Rectangle {
length;
width;
constructor(length, width) {
this.length = length;
this.width = width;
}
getAreas () {
return this.length * this.width;
}
}
export class Square extends Rectangle {
constructor(length) {
super(length, length);
}
getAreas () {
return super.getAreas();
}
}
静态成员的继承
父类的静态成员也同样继承到派生类中,如下例子
export class Rectangle {
length;
width;
static create(length, width) {
return new Rectangle(length, width);
}
constructor(length, width) {
this.length = length;
this.width = width;
}
getAreas () {
return this.length * this.width;
}
}
export class Square extends Rectangle {
constructor(length) {
super(length, length);
}
getAreas () {
return super.getAreas();
}
}
const rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); //true
console.log(rect instanceof Square); //false
console.log(rect.getAreas()); //12
派生自表达式的类
es6中继承不一定只能继承类,只要表达式可以被解析为一个函数并且具有construct属性和原型,那么就可以用extends进行继承。
例如下边两种情况
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getAreas = function () {
return this.length * this.width;
};
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
getAreas () {
return super.getAreas();
}
}
Rectangle是一个函数可以被解析为构造函数,所以Square可以继承它。
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getAreas = function () {
return this.length * this.width;
};
function getBase(){
return Rectangle
}
class Square extends getBase() {
constructor(length) {
super(length, length);
}
getAreas () {
return super.getAreas();
}
}
getBase是类声明的一部分,可以执行函数然后取得返回值来继承。
这种特性可以让我们写出很漂亮的代码,可以根据需要动态设定类的继承关系,活用可以玩出很多令人眼前一亮的代码,比如下边这样。
const SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
const AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mix(...mixins) {
const base = function () {};
Object.assign(base.prototype, ...mixins);
return base;
}
export class Square extends mix(AreaMixin, SerializableMixin) {
constructor(length) {
super(length, length);
}
}
内建对象的继承
活用继承可以改写很多JavaScript固有的对象,让它可以完成更多满足我们要求的功能,比如数组。
使用es5的方法继承重写数组几乎是不可能
export function MyArray() {
Array.call(this, ...arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
enumerable: true,
writable: true,
configurable: true,
}
});
const arr = new MyArray();
arr[0] = 1;
arr.length = 0 ;
console.log(arr[0]);//1
上面的代码 MyArray并没有表现出Array该有的效果,至于原因本人目前也不是很清楚,大体解释是this初始化时指向的问题。
使用es6的继承方式就可以很容易在Array类基础上进行增强。
export class MyArray extends Array{
}
const arr = new MyArray();
arr[0] = 1;
arr.length = 0 ;
console.log(arr[0]);//undefined
Symbol.spcies属性
返回函数的静态访问器属性,默认返回this
通过实例来生成类新的实例默认调用[Symbol.species]接口
export class MyArray extends Array {
constructor(...args) {
super(...args);
}
static get [Symbol.species]() {
return Array;
}
}
const arr = new MyArray(1, 2, 3, 4);
// arr[0] = 1;
// arr.length = 0 ;
const sli = arr.slice(2);
console.log(sli);
console.log(sli instanceof MyArray);
console.log(sli instanceof Array);
如果上边的代码不加Symbol.species接口slice方法返回的实例就是MyArray实例,加了后返回的就是Array,实例中生成类新的实例默认调用的接口都是Symbol.species接口
。
new target
我们都知道通过new.target属性可以知道当前对象是如何调用的,因此可以通过new.target属性模拟抽象类。
export class Shape {
constructor() {
if (new.target === Shape) {
throw Error('该类不能直接实例化');
}
}
}
export class Rectangle extends Shape {
length;
width;
static create(length, width) {
return new Rectangle(length, width);
}
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
getAreas () {
return this.length * this.width;
}
}
上边的例子,如果直接实例化Shape就会报错,只能通过继承的方式来调用,间接的模拟了抽象类。
网友评论