导语
class只是语法糖,并没有为js引入一种新的对象继承模式,之前通过原型链一样可以实现class的功能;
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
定义class
class 就像是特殊的函数,和创建函数一样,有类声明(class declarations),和类表达式( class expressions )两种创建类的方式:
类声明
使用class 关键字,后面跟上类名(class 关键字后面的是类名)
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
类声明和普通函数声明不同的地方在于,类声明并没有函数提升(下面要介绍的类表达式也没有函数提升):
var obj = new Myclass(); //报错
class Myclass (){ }
类声明不能函数提升是为了保证,子类继承父类的那个语句,不会提升至头部,否则将会出现父类还没有定义,子类就要继承, 看下面的例子:
{
let B = class {}; // let 声明 不存在函数提升
class A extends B { //如果存在类哈函数提升的话,这行会提升到第一行,父亲还没声明,儿子怎么继承?
}
}
类声明不能和已经存在的类重名,(不管这个类之前是通过类声明的方式声明还是通过类表达式的方式声明的), 否则将会报错;
class f1 {};
class f1 {};
var f2 = class {};
class f2 {}; // 报错了
class f3 {};
var f3 = class {}; // 报错了
var f4 = class {};
var f4 = class {}; // 如果两个函数表达式重名了,那么不会报错
类表达式
类表达式是定义类的另外一种方式
var myClass = class [className] [extends] {
// class body
}
就像函数表达式一样,在类表达式中,类名是可有可无的。若定义的类名,则该类名只有的类的内部才可以访问到。
// 方式一
const MyClass = class {};
// 方式二:给出类名
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
如果class 后面没有名字,那么该类.name 就是 函数表达式的名字:
var Foo = class {
constructor() {}
bar() {
return 'Hello World!';
}
};
var instance = new Foo();
instance.bar(); // "Hello World!"
Foo.name; // "Foo"
如果 class 后面有名字,那么该名字只能在函数内被访问到,同时该类 . name 就是class 后面的名字:
var Foo = class NamedFoo {
constructor() {}
whoIsThere() {
return NamedFoo.name;
}
}
var bar = new Foo();
bar.whoIsThere(); // "NamedFoo"
NamedFoo.name; // ReferenceError: NamedFoo is not defined
Foo.name; // "NamedFoo"
采用类表达式,可以写出立即执行的Class。如下:
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('Zhang San');
person.sayName(); // Zhang San
类体和方法定义
类的成员需要定义在一对大括号内{},大括号内的代码的大括号本身组成了类体。类成员包括类构造器和类方法 (包括静态方法和实例方法)。
类体中的代码都强制在严格模式中执行,即默认”use strict”。考虑到未来所有的代码,其实都是运行在模块之中,所以ES6实际上把整个语言升级到了严格模式。
构造器(constructor方法)
一个类只能拥有一个名为constructor
的方法(否则会报错),一个类的 constructor 方法只有在实例化的时候被调用。
如果没有显式定义constructor
方法,这个方法会被默认添加,即,不管有没有显示定义,任何一个类都有constructor
方法。
子类必须在constructor方法中调用super
方法,否则新建实例时会报错。因为子类没有自己的this
对象,而是继承父类的this
对象,然后对其进行加工,如果不调用super
方法,子类就得不到this
对象。
class Point {}
class ColorPoint extends Point {
constructor() {}
}
let cp = new ColorPoint(); // ReferenceError
上面代码中,
ColorPoint
继承了父类Point
,但是它的构造函数没有调用super
方法,导致新建实例时报错。原型方法
定义类的方法时,方法名前面不需要加上function
关键字。另外,方法之间不需要用逗号分隔,加了会报错。
class Bar {
constructor() {}
doStuff() {}
toString() {}
toValue() {}
}
上面的写法就等同于下面:
Bar.prototype = {
doStuff() {},
toString() {},
toValue() {}
};
所以,在类的实例上调用方法,实际上就是调用原型上的方法。既然类的方法都是定义在
prototype
上面,所以类的新方法可以添加在prototype
对象上面。Object.assign
方法可以很方便地一次向类添加多个方法。
class Point {
constructor() {
// ...
}
}
Object.assign(Point.prototype, {
toString() {},
toValue() {}
});
另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
class Point {
constructor(x, y) {
// ...
}
toString() {
return '(' + x + ', ' + y + ')';
}
}
Object.keys(Point.prototype); // []
Object.getOwnPropertyNames(Point.prototype); // ["constructor", "toString"]
Object.getOwnPropertyDescriptor(Point, 'toString');
// Object {writable: true, enumerable: false, configurable: true}
静态方法
static
关键字用来定义类的静态方法。静态方法是指那些不需要对类进行实例化,使用类名就可以直接访问的方法。静态方法经常用来作为工具函数。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx*dx + dy*dy);
}
}
const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
console.log(Point.distance(p1, p2));
静态方法不可以被实例继承,是通过类名直接调用的。但是,父类的静态方法可以被子类继承。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod(); // "hello"
extends关键字
extends
关键字用于实现类之间的继承。子类继承父类,就继承了父类的所有属性和方法。 extends
后面只可以跟一个父类。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
extends
关键字不能用于继承一个对象,如果你想继承自一个普通的对象,你必须使用 Object.setPrototypeof ( )es5 的继承和 es6 的继承
es5中的原型链继承,就是通过将子类构造函数的原型作为父类构造函数的实例(sub.prototype=new super),这样就连通了子类-子类原型-父类;
//先来个父类,带些属性
function Super(){
this.flag = true;
}
//为了提高复用性,方法绑定在父类原型属性上
Super.prototype.getFlag = function(){
return this.flag;
}
//来个子类
function Sub(){
this.subFlag = false;
}
//实现继承
Sub.prototype = new Super;
//给子类添加子类特有的方法,注意顺序要在继承之后
Sub.prototype.getSubFlag = function(){
return this.subFlag;
}
//构造实例
var es5 = new Sub;
但是这样的原型链继承有问题:我们的目标是构造函数的属性私有化,方法复用化,所以我们把属性放在函数内,把方法放到原型上;但是原型链继承显然,父类的属性和方法都放到了子类的原型上;
为了解决上面的做法,我们在es5中混合使用 构造函数call 继承;
function Super(){
this.flag = true;
}
Super.prototype.getFlag = function(){
return this.flag; //继承方法
}
function Sub(){
this.subFlag = flase
Super.call(this) //继承属性
}
Sub.prototype = new Super;
var obj = new Sub();
Sub.prototype.constructor = Sub;
Super.prototype.getSubFlag = function(){
return this.flag;
}
但是还有个小问题是,子类.prototype = new 父类,子类.prototype的constructor 就指向了父类,所以我们要重写一下:
Sub.prototype.constructor = Sub;
ES6的继承实现方法,其内部其实也是ES5组合继承的方式,通过call构造函数,在子类中继承父类的属性,通过原型链来继承父类的方法。
我们将 extend 用babel 进行转码:
function _inherits(subClass, superClass) {
// 确保superClass为function
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
// 把子类.prototype 继承了父类.prototype(new 父类), 同时把子类prototype的constructor进行了重写;
// 给subClass添加constructor这个属性
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
// 将父类设为子类的prototype
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
里面 子类. prototype = Object.create ( 父类.prototype )这句话,实际上和下面的代码类似:
子类.prototype = new 父类
不同的是, 子类. prototype = Object.create ( 父类.prototype )不会继承父类的constructor里面的属性,只会继承父类prototype上的方法;
一个关于 Object.create(car.prototype) 用法的代码:
function Car (desc) {
this.desc = desc;
this.color = "red";
}
Car.prototype = {
getInfo: function() {
return 'A ' + this.color + ' ' + this.desc + '.';
}
};
//instantiate object using the constructor function
var car = Object.create(Car.prototype);
car.color = "blue";
alert(car.getInfo()); //displays 'A blue undefined.' ??! // 看见了吧,只会继承方法,不能继承属性
当你只想继承类的原型,而不想继承类的constructor的时候,使用Object.create 是很棒的选择;
如果我们想子类继承父类的prototype ,同时子类也要有自己的属性,请看下面的代码:
var Car2 = Object.create(null); //this is an empty object, like {}
Car2.prototype = {
getInfo: function() {
return 'A ' + this.color + ' ' + this.desc + '.';
}
};
var car2 = Object.create(Car2.prototype, {
//value properties
color: { writable: true, configurable:true, value: 'red' },
//concrete desc value
rawDesc: { writable: false, configurable:true, value: 'Porsche boxter' },
// data properties (assigned using getters and setters)
desc: {
configurable:true,
get: function () { return this.rawDesc.toUpperCase(); },
set: function (value) { this.rawDesc = value.toLowerCase(); }
}
});
car2.color = 'blue';
alert(car2.getInfo()); //displays 'A RED PORSCHE BOXTER.'
每一个属性又是一堆属性的集合,又称descriptor, 分为 data descriptor 和 accessor(访问 ) descriptor
总之,extends做了两件事情,一个是通过Object.create()把子类的原型赋值为父类的实例, 实现了继承方法,子类.prototype.__proto__也自动指向父类的原型,一个是手动修改了子类的__proto__, 修改为指向父类,(本来在es5 中应该是指向Function.prototype);
在子类中必须执行的super()方法,实际上是用call 方法:
var _this = _possibleConstructorReturn(this, (b.__proto__ || Object.getPrototypeOf(b)).call(this));
父类被当成普通函数来执行,从而将this绑定到子类上;
同时,extend后面可以跟多种类型的值:
第一种特殊情况,子类继承Object类。
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
第二种特殊情况,不存在任何继承。
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
第三种特殊情况,子类继承null。
class C extends null {
constructor() { return Object.create(null); }
}
两条继承链
一个继承语句同时存在两条继承链:一条实现属性继承,一条实现方法继承.
class A extends B {}
A.__proto__ === B; //继承属性
A.prototype.__proto__ === B.prototype; //继承方法
ES6的子类的__proto__是父类,子类的原型的__proto__是父类的原型
第二条继承链理解起来没有什么问题,es6 本身就是对es5 混合模式继承的封装,在原型继承上,es6使用的是
子类.prototype = Object.create (父类.prototype) // 相当于 new 父类
子类的原型是父类的实例(暂时这样理解,其实子类并不能继承父类的属性,只能继承方法),所以子类.prototype(父类的实例)指向父类的prototype。
但是第一个继承链就不好理解了,在ES5中 子类.__proto__是指向Function.prototype的,因为每一个构造函数其实都是Function这个对象构造的。在ES6的继承中,有这样一句话:
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
es6 的 extends 把子类的__proto__指向父类可以实现属性的继承,在ES5中在没有用借用继承的时候由于父类属性被子类原型继承,所有的子类实例实际上都是同一个属性引用。
可以这么说,在ES5继承和构造实例,ES6构造实例的时候可以理解__proto__指向构造函数的原型的,但是在ES6继承中,__proto__指继承自哪个类或原型。也就是说,两条继承链只存在于两个类之间的关系,实例与构造函数之间的关系,还是es5的模式;
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
这也意味着,可以通过实例的__proto__属性为Class添加方法。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
但是我们不推荐这样做
super 关键字
super
关键字可以用来调用其父类的构造器或方法。super 作为方法的时候,必须在 constructor 中调用,并且只能在 constructor 里面被调用
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
class Lion extends Cat {
speak() {
super.speak();
console.log(this.name + ' roars.');
}
}
super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)。
第二种情况,super作为对象时,在普通方法中,指向父类的原型对象,可以调用原型上的方法(但是父类的私有属性和方法就调用不到了);在静态方法中,指向父类。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
ES6 规定,通过super调用父类的方法时,super会绑定子类的this。
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
通过super对某个属性赋值,super 的this 指向子类,如果要访问,super 的 this 就变成了父类的prototype:
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作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
类的Getter和Setter方法
与ES5一样,在类内部可以使用get
和set
关键字,对某个属性设置取值和赋值方法。
class Foo {
constructor() {}
get prop() {
return 'getter';
}
set prop(val) {
console.log('setter: ' + val);
}
}
let foo = new Foo();
foo.prop = 1;
// setter: 1
foo.prop;
// "getter"
上面代码中,
存值和取值方法是设置在属性的descriptor对象上的。
prop
属性有对应 的赋值和取值方法,因此赋值和读取行为都被自定义了。 存值和取值方法是设置在属性的descriptor对象上的。
var descriptor = Object.getOwnPropertyDescriptor(Foo.prototype, 'prop');
"get" in descriptor // true
"set" in descriptor // true
上面代码中,存值和取值方法是定义在
prop
属性的描述对象上的,这与ES5一致。类的Generator方法
如果类的某个方法名前加上星号(*
),就表示这个方法是一个Generator函数。
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world
上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个Generator函数。Symbol.iterator方法返回一个Foo类的默认遍历器,
for...of
循环会自动调用这个遍历器。new.target属性
ES6为new命令引入了一个new.target属性,(在构造函数中)返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的(比如说calll),new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用new生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用new生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
Class内部调用new.target,在new 一个实例的时候 ,返回当前Class。然而当子类继承父类时,new.target会返回子类。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length); // 相当于执行父类中的constructor,
}
}
var obj = new Square(3); // 输出 false
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化'); // 抛出一个错误
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确