es6中增加了类的定义,使得JS语言在面向对象编程时更便捷。
面向对象
面向对象 一种编程思想,跟具体的语言没有关系,
- 面向过程 思考的切入点是功能的步骤
- 面向对象 思考的切入点是对象的划分
我们用一个例子【大象装冰箱】通过面向过程和面向对象的比较中了解面向对象编程的思想:
面向过程的编程思想指导下,我们把【大象装冰箱】分成3个步骤(过程)来分别定义函数:
// 1.冰箱门打开
function openFrige() {
}
openFrige();
// 2.大象装进去
function elephantIn() {
}
elephantIn();
// 3.冰箱门关上
function closeFrige() {
}
closeFrige();
面向对象,通常要先找名词(对象)
/**
* 大象
*/
function Elephant() {
} //构造函数
/**
* 冰箱
*/
function Frige() {
}
Frige.prototype.openDoor = function() {
}
Frige.prototype.closeDoor = function() {
}
Frige.prototype.join = function(something) {
}
它还是会用到面向过程,但它的切入点是对象
// 1.冰箱门打开
var frige = new Frige();
frige.openDoor();
// 2.大象装进去
var ele = new Elephant();
frige.join(ele);
// 3.冰箱门关上
frige.closeDoor();
面对不同的场景,不同的复杂度的程序里面,它们各有各的优势
- 一些小的功能模块比较适合运用面向过程
- 大型的项目,经常需要维护修改的项目,比较适合运用面向对象的编程思想它更便于组合和拆分
面向过程的编程思想在小的功能模块显得小巧精简。如果在大型的项目里,考虑到可维护性,可扩展性,则面向对象的编程思想占优势
es6-类和传统的构造函数的对比
构造函数的语法糖 面向对象中,将下面对一个对象的所有成员的定义,统称为类
传统的构造函数的问题
- 属性和原型方法定义分离,降低了可读性
- 原型成员可以被枚举
- 默认情况下,构造函数仍然可以被当作普通函数使用
类的特点
- 类声明不会被提升,与let和const一样,存在暂时性死区 要先定义再使用
- 类中的所有代码均在严格模式下执行
- 类的所有方法都是不可枚举的
- 类的所有方法都无法被当作构造函数使用 (构造函数的方法当作构造函数使用时,不会报错)类的方法当作构造函数使用时会报错
- 类的构造器必须使用new来调用
// 构造函数 构造器
function Animal(type, name, age, sex) {this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
// 中间存在别的代码,一千行
// 定义实例方法(原型方法)
Animal.prototype.print = function() {
console.log(`【种类】:${this.type}`);
console.log(`【名字】:${this.name}`);
console.log(`【年龄】:${this.age}`);
console.log(`【性别】:${this.sex}`);
}
const a = new Animal("狗", "旺财", 3, "公");
a.print();
for(const prop in a){
console.log(prop)
}//原型成员的方法可以被枚举出来 这是我们不希望看到的
这是用传统的构造函数的例子,上面构造器和定义实例方法的代码可能会被分开离得很远从而降低了可读性。同时原型上的方法可以用for…in…循环遍历出来,降低了安全性。
es6中的类使用关键字class 定义,对应于构造函数的构造器使用关键字constructor,定义的实例方法、原型方法被class包裹,更有整体性
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
print() {
console.log(`【种类】:${this.type}`);
console.log(`【名字】:${this.name}`);
console.log(`【年龄】:${this.age}`);
console.log(`【性别】:${this.sex}`);
}
}
const a = new Animal("狗", "旺财", 3, "公");
a.print();
console.log(a); //print方法自动定义到原型上面了
for (const prop in a) {
console.log(prop)
} //遍历不到原型上的成员,已被屏蔽
类名不能当做普通函数来调用,调用会报错
更多的es6提供的类里面的语法支持
- 可计算的成员名
- getter和setter
- 静态成员
- 字段初始化器(ES7)
- 类的表达式
- 装饰器(ES7)
const printName = "print";
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
[printName]() {
console.log(`【种类】:${this.type}`);
console.log(`【名字】:${this.name}`);
console.log(`【年龄】:${this.age}`);
console.log(`【性别】:${this.sex}`);
}
}
const a = new Animal("狗", "旺财", 3, "公");
a[printName]();
可计算的成员名可以用中括号括起来,里面可以放变量或表达式。
getter和setter
我们想对类里的定义到实例上的属性的读取,和书写(设置)做一些限定。
例如:
const printName = "print";
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
// this.age = age;
this.setAge(age);
this.sex = sex;
}
setAge(age) {
if (typeof age !== "number") {
throw new TypeError("age property must be a number");
}
if (age < 0) {
age = 0;
} else if (age > 200) {
age = 200;
}
this._age = age;
}
[printName]() {
console.log(`【种类】:${this.type}`);
console.log(`【名字】:${this.name}`);
console.log(`【年龄】:${this._age}`);
console.log(`【性别】:${this.sex}`);
}
}
const a = new Animal("狗", "旺财", -3, "公");
a[printName]();
这里this.setAge(age);
代替了this.age=age
使得通过这个类创建的实例都调用了这个方法setAge(age),这个方法会对age有一系列的判断,然后赋值给this._age,这样a[printName]();
就能执行console.log(
【年龄】:${this._age});
这个语句,显示限制后的age值。
在控i制台调用a.setAge(200)
返回undefined,是因为setAge函数没有返回值,没写return语句,默认为undefined。但这个方法是可以调用的,他会影响后续a._age
的调用。如果传参不是number类型的,根据限定条件,控制台也会报错。
这样一来,定义属性和给属性赋值,和原来写的不一样,更像一个函数。如果和原来定义属性和给属性赋值那样的写法又能给这一属性的读写做限定设置,在es5中,对象的属性进行读写的限定可以用Object.defineProperty
来处理
Object.defineProperty可定义某个对象成员属性的读取和设置,类似于如下写法。
const printName = "print";
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
Object.defineProperty(this,"age",{
set(val){...
},
get(){ ...
}
})
this.sex = sex;
}
在类里,es6给出了setter和getter。如上面的情形可以写作:
const printName = "print";
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
// 创建一个age属性,并给它加上getter,读取该属性时,会运行该函数
get age() {
return this._age + "岁";
}
// 创建一个age属性,并给它加上setter,给该属性赋值时,会运行该函数
set age(age) {
if (typeof age !== "number") {
throw new TypeError("age property must be a number");
}
if (age < 0) {
age = 0;
} else if (age > 100) {
age = 100;
}
this._age = age;
}
[printName]() {
console.log(`【种类】:${this.type}`);
console.log(`【名字】:${this.name}`);
console.log(`【年龄】:${this.age}`);
console.log(`【性别】:${this.sex}`);
}
}
const a = new Animal("狗", "旺财", 180, "公");
a[printName]();
getter 和setter是在普通对象上(实例上)而不在原型上
这样可以直接像原来的属性一样,直接赋值 this.age = age; getter无参;setter单参
构造函数本身的属性叫作静态属性
例如有一个象棋类,棋子的宽高各是50,它是每个棋子具有的共性。
如果定义在constructor里,则每个棋子创建都要定义一下自己的宽高,会有重复的操作,占用更多的内存。
如果定义为类的静态方法可以在类外面书写Chess.width = 50; Chess.height = 50;
es6 引入了关键字static,便可以在类内部定义这样的方法
使用static关键字定义的成员即静态成员 在ES7里面规定static定义的属性,可以直接给它赋值
class Chess {
constructor(name) {
this.name = name;
// this.width = 50;
// this.height = 50; 在此处设定,需要创建一个棋子才能访问的到
// 每个棋子的宽高是一样的,不应该是实例属性,而应该定义为静态属性
}
static width = 50;
static height = 50;
static method() {
}
}
// 写为静态属性
// Chess.width = 50;
// Chess.height = 50;
const ch1 = new Chess("马")
const ch2 = new Chess("车")
console.log(Chess.width)
console.log(Chess.height)
Chess.method();
字段初始化器
class Test {
constructor(){
this.a=1;
this.b=2;
this.c=3;
}
}
等效于
class Test {
a = 1
b = 2
c = 3
// 有兼容性问题 ES7
}
后者就是ES7支持的字段初始化器
class Test {
static a = 1
b = 2
c = 3
// 有兼容性问题 ES7
constructor() {
this.d = this.b + this.c
}
}
const t = new Test();
console.log(t);
console.log(Test.a);
- 使用static的字段初始化器,添加的是静态成员;
- 没有使用static的字段初始化器,添加的成员位于对象上;
- 箭头函数在字段初始化器位置时,指向当前对象(可以解决this指向混乱的情况) 绑定了this,但是会占用较多内存空间
class Test {
constructor() {
this.a = 123;
}
print = () => {
console.log(this.a)
}
}
const t1 = new Test();
const t2 = new Test();
console.log(t1.print === t2.print) //false
//print方法是在各自的实例上,不在原型上,不是原型方法所以它们不相等
可以把类写成表达式的形式
const A = class {
// 匿名类 类表达式
a = 1;
b = 2;
}
const a = new A();
console.log(a)
类也是JS的一等公民,可以用类作为一个参数,也可以作为函数返回值 类在JS中本质上就是函数
装饰器Decorator
装饰器本质是一个函数
这是横切关注点的问题 像是否过时;具有通用性与具体某一个类无关 做标记就是装饰器
语法:@ 装饰器的名称
例如 : @Obsolete 过时
class Test1 {
@Obsolete
print() {
console.log("print方法")
// console.warn("print方法已过时")
//不用装饰器, 通过函数体内改动,太麻烦了,也容易出问题
}
}
function Obsolete(target, methodName, descriptor) {
// console.log(target,methodName,descriptor);
// 现在还打印不出来 浏览器还不支持,借typeScript里的情况做一下说明,它们隐含的内容,类似于将要打印出如下内容。
// function Test
// print
// {value:function print(){},...}
const oldFunc = descriptor.value
descriptor.value = function(...ags) {
console.warn(`${methodName}方法已过时`);
oldFunc.apply(this, args);
}
}