详讲ES6中的class
一、ES5中的近类结构
//ES5中的近类结构 function Person(name) { this.name = name; } Person.prototype.sayname = function(){ console.log(this.name); } var person = new Person("Tom"); person.sayname(); console.log(person instanceof Person);//true console.log(person instanceof Object);//true
二、ES6中的类
ECMAScript2015中引入的JavaScript类实质上是JavaScript现有的基于原型的继承的语法糖。类实际上是个“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。
(一)类声明
函数声明和类声明之间的一个重要区别是函数声明会提升,类声明不会。你首先需要声明你的类,然后访问它。
class Person{
//等价于Person构造函数
constructor(name){
this.name = name;
}
//等价于Person.prototype.sayname
sayName(){
console.log(this.name);
}
}
类与自定义类型之间的差异:
1.函数声明可以被提升,而类声明与let声明类似,不能被提升,真正执行声明语句之前,它们会一直存在于临时死区中。
2.类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式执行。
3.在自定义类型中,需要通过Object.defineProperty()方法手工指定某个方法为不可枚举;而在类中,所有方法都是不可枚举的。
4.使用关键字new以外的方式调用类的构造函数会导致程序抛出错误。
5.在类中修改类名会导致程序报错。
模拟类声明代码:
let Person = (function(){ "use strict" const Person = function(name){ //确保通过关键字new调用该函数 if(typeof new.target === "undefined"){ throw new Error("必须通过关键字new调用构造函数"); } this.name = name; } Object.defineProperty(Peron.prototype,"sayName",{ value:function(){ //确保不会通过关键字new调用该方法 if(typeof new.target !== "undefined"){ throw new Error("不可使用关键字new调用该方法") } console.log(this.name); }, enumerable:false, writable:true, configurable:true }) return Person })();
(二)类表达式
类表达式可以是具名的或匿名的,一个具名类表达式的名称是类内的一个局部属性,它可以通过类本身的name属性来获取。类表达式也同样受到类声明中提到的类型提升的限制。
//匿名类
let Person = class {
constructor(height,width){
this.height = height;
this.width = width;
}
};
console.log(Person.name);//Person
//具名类
let Person = class Person {
constructor(height,width){
this.height = height;
this.width = width;
}
}
console.log(Person.name);//Person
(三)类体和方法定义
一个类的类体是一对花括号{}中的部分,这是你定义类成员的位置,如方法或构造函数。类声明和类表达式的主体都执行在严格模式下。
constructor
方法是类的构造函数,是一个默认方法,这种方法用于创建和初始化一个由class创建的对象,通过 new
命令创建对象实例时,自动调用该方法。一个类必须有 constructor
方法,如果没有显式定义,一个默认的 consructor
方法会被默认添加。所以即使你没有添加构造函数,也是会有一个默认的构造函数的。一般 constructor
方法返回实例对象 this
,但是也可以指定 constructor
方法返回一个全新的对象,让返回的实例对象不是该类的实例。
一个构造函数可以使用super关键字来调用一个父类的构造函数。
自由属性是实例中的属性,不会出现在原型上,且只能在类的构造函数或方法中创建,此例中的name就是一个自由属性,这里建议你在构造函数中创建所有自有属性,从而只通过一处就可以控制类中的所有自有属性。
类声明仅仅是基于已有自定义类型声明的语法糖,typeof Person最终返回的结果是“function”,所以Person声明实际上创建了一个具有构造函数方法行为的函数。此示例中的sayName()方法实际上是Person.prototype上的一个方法
(四)super关键字的作用
super
这个关键字,既可以当做函数使用,也可以当做对象使用。这两种情况下,它的用法完全不同。
1、当作函数使用
class A {} class B extends A { constructor() { super(); // ES6 要求,子类的构造函数必须执行一次 super 函数,否则会报错。 } }
注:在 constructor
中必须调用 super
方法,因为子类没有自己的 this
对象,而是继承父类的 this
对象,然后对其进行加工,而 super
就代表了父类的构造函数。super
虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super
内部的 this
指的是 B,因此 super()
在这里相当于 ```A.prototype.constructor.call(this, props)``。
class A { constructor() { console.log(new.target.name); // new.target 指向当前正在执行的函数 } } class B extends A { constructor() { super(); } } new A(); // A new B(); // B
可以看到,在 super()
执行时,它指向的是 子类 B 的构造函数,而不是父类 A 的构造函数。也就是说,super()
内部的 this
指向的是 B
2、当做对象使用
在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A { c() { return 2; } } class B extends A { constructor() { super(); console.log(super.c()); // 2 } } let b = new B();
上面代码中,子类 B 当中的 super.c()
,就是将 super
当作一个对象使用。这时,super
在普通方法之中,指向 A.prototype
,所以 super.c()
就相当于 A.prototype.c()
。
通过 super
调用父类的方法时,super
会绑定子类的 this
。
class A { constructor() { this.x = 1; } s() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } m() { super.s(); } } let b = new B(); b.m(); // 2
super.s()
虽然调用的是 A.prototytpe.s()
,但是 A.prototytpe.s()
会绑定子类 B 的 this
,导致输出的是 2,而不是 1。也就是说,实际上执行的是 super.s.call(this)
。this
,所以如果通过 super
对某个属性赋值,这时 super
就是 this
,赋值的属性会变成子类实例的属性。class A { constructor() { this.x = 1; } } class B extends A { constructor() { super(); this.x = 2; super.x = 3; console.log(super.x); // undefined console.log(this.x); // 3 } } let b = new B();
super.x
赋值为 3,这时等同于对 this.x
赋值为 3。而当读取 super.x
的时候,调用的是 A.prototype.x
,但并没有 x
方法,所以返回 undefined。super
的时候,必须显式指定是作为函数,还是作为对象使用,否则会报错。class A {} class B extends A { constructor() { super(); console.log(super); // 报错 } }
上面代码中,console.log(super);
的当中的 super
,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这是,如果能清晰的表明 super
的数据类型,就不会报错。
(五)静态方法
在ECMAScript5及早期版本中,直接将方法添加到构造函数中来模拟静态成员是一种常见的模式,例如:
function Person(name){ this.name = name; } //静态方法 Person.create = function(name){ return new Person(name); } //实例方法 Person.prototype.sayName = function(){ console.log(this.name); } var person = Person.create("top") console.log(person);
由于工厂方法,Person.create()使用的数据不依赖Person的实例,因此其会被认为是一个静态方法。ECMAScript6的类语法简化了创建静态成员的过程,在方法或访问器属性名前使用正式的静态注释即可。static关键字用来定义一个类的一个静态方法。调用静态方法不需要实例化该类,但不能通过一个类实例调用静态方法。静态方法通常用于为一个应用程序创建工具函数。不需要实例化类,即可直接通过该类来调用的方法,即称之为“静态方法”。这样该方法不会被实例继承!
class Person{
//等价于Person构造函数 constructor(name){ this.name = name; } //等价于Person.prototype.sayName sayName(){ console.log(this.name)
} //等价于Person.create static create(name){ return new Person(name)
}
static fund(){
return "我是person中的静态方法,无需实例化,可直接调用!"
}
static b(){
//通过静态方法b来调用静态方法fund
console.log(this.a());//
}
//通过实例方法调用会报错
c(){
console.log(this.a());//TypeError: this.a is not a function
}
} var person = new Person('tom');
var person = Person.create('qin'); console.log(person)
//类Box的a方法前有static关键字, 表明该方法是一个静态方法, 可以直接在Box类上调用。静态方法只能在静态方法中调用,不能在实例方法中调用。
console.log(Person.fund());//我是person中的静态方法,无需实例化,可直接调用!
Person.b();//我是person中的静态方法,无需实例化,可直接调用!
类中的所有方法和访问器属性都可以用static关键字来定义,唯一的限制是不能将static用于定义构造函数方法。不可在实例中访问静态成员,必须要直接在类中访问静态成员。
父类的静态方法, 可以被子类继承:
class Box { static a() {//父类Box的静态方法 return '我是父类的静态方法a'; } } class Desk extends Box {} //子类Desk可以直接调用父类的静态方法a console.log(Desk.a());
倘若想通过子类的静态方法调用父类的静态方法,需要从super对象上调用:
class Box { static a() { return '我是通过super来调取出来的'; } } class Desk extends Box { static a(){ return super.a(); } } console.log(Desk.a());
(六)静态属性
静态属性指的是 Class 本身的属性, 即Class.propname, 而不是定义在实例对象( this) 上的属性。
class Box{ constructor(){ this.name="实例属性" } } Box.prop1="静态属性1"; Box.prop2="静态属性2"; console.log(Box.prop1,Box.prop2);//静态属性1 静态属性2
(七)继承
在ECMAScript6之前,需要多个步骤实现继承。Son继承自Father,为了这样做,必须用一个创建来Father.prototype的新对象重写Son.prototype并调用Father.call()方法。
function Father(length,width){ this.length = length; this.width = width;
} Father.prototype.getArea = function(){ return this.length * this.width } function Son(length,width){ Father.call(this,length,width) } Son.prototype = Object.create(Father.prototype,{ constructor:{ value:Son, enumerable:true, writable:true, configurable:true } }) var son = new Son(3,4); console.log(son.getArea());//12 console.log(son instanceof Son);//true console.log(son instanceof Father);//true
类的出现让我们可以更轻松的实现继承功能,使用熟悉的extends关键字可以指定类继承的函数。原型会自动调整,通过调用super()方法即可访问基类的构造函数。
class Father{ constructor(length,width){ this.length = length; this.width = width; } getArea(){ return this.length * this.width }
static create(length,width){
return new Father(length,width)
} } class Son extends Father { constructor(length,width){ super(length,width) }
//派生类中的方法总会覆盖基类中的同名方法
getArea(){
//如果你想调用基类中的同名方法,则可以调用super.getArea()方法
super.getArea()
return this.length * this.width
}
} var son = new Son(3,4); console.log(son.getArea());//12 console.log(son instanceof Son)//true console.log(son instanceof Father);//true
var obj = Son.create(3,4);
继承自其他类的类被称为派生类,如果派生类中指定了构造函数则必须要调用super(),如果不调用程序就会报错。如果不使用构造函数,则当创建新的类实例时会自动调用super()并传入所有参数。
class Son extends Father{
//没有构造函数
}
//等价于
class Son extends Father{
constructor(...args){
super(...args)
}
}