1 Class in ES6
ES6提出了类(Class)的概念,让对象的原型的写法更像面向对象语言写法。 ES6中通过class定义对象,默认具有constructor方法和自定义方法,但是包含在class中的方法不可枚举。
class Point{
constructor(){
this.x=x;
this.y=y;
}
toString(){
return this.x+this.y;
}
}
注意:constructor方法对应ES5的构造函数;创建类的方法不需要使用function关键字,方法之间不需要逗号分隔。
ES5中定义对象时组合使用构造函数模式和原型模式,构造函数模式用于定义实例属性,原型模式用于定义共享方法和共享属性,节省内存,而且支持向构造函数传递参数。
function Point (x,y){
this.x=x;
this.y=y;
}
Point.prototype.toString= function(){
return this.x+this.y;
}
2 ES5和ES6创建实例和对象对比
1)ES5中的Person构造函数和Person原型对象以及实例Person1、Person2的关系:构造函数的prototype属性以及实例的__proto__属性指向原型对象,原型对象的constructor属性指向构造函数。
构造函数的原型(prototype)属性在ES6中依旧存在,这一点和ES5一样:类的方法都定义在类的原型(prototype)上,在类的实例上面调用方法,其实就是调用原型上的方法。
//实例的constructor方法就是类的原型(prototype)的constructor方法
class B{
constructor(){}
}
let b = new B();
console.log(b.constructor === B.prototype.constructor);//true
2)ES6的类可以看做是构造函数的另一种写法(和ES5中构造函数等同的并不是constructor方法):类是function类型,且类本身指向类的prototype对象的constructor属性,这与ES5一样。
class Point{
// ...
}
console.log(typeof Point) // "function"
console.log(Point === Point.prototype.constructor) // true
3)使用类创建实例对象也是直接对类使用new命令,跟ES5中构造函数的用法一致。
4)类内部定义的方法都是不可枚举的,这和ES5不同。
5)constructor方法
一个类必须有constructor方法,如果没有就默认添加constructor(){}。虽然类是函数,但是和ES5不同,不通过new而直接调用类会导致类型错误,也就是说这个函数仅当在和new一起使用时才有意义。
6)类的实例
与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
以上代码中,x和y都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。
与ES5一样,类的所有实例共享一个原型对象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
可以通过实例的__proto__属性为Class原型添加方法,增加后所有的原型都具有这个方法,这和ES5一样。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
7)Class不存在变量提升(hoist),这一点与ES5完全不同。
new Foo(); // ReferenceError
class Foo {}
上面代码中,Foo类使用在前,定义在后,这样会报错,因为ES6不会把类的声明提升到代码头部。这种规定的原因与继承有关,必须保证子类在父类之后定义。
8)严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以ES6实际上把整个语言升级到了严格模式。
3 ES6 Class的继承
Class通过extends关键字实现继承,这和ES5通过修改原型链实现继承不同(巧合的是,SASS也通过@extend实现样式继承):
class ColorPoint extends Point{}
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // super代表父类原型,调用父类的toString()
}
}
上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
super这个关键字,既可以当作函数使用,也可以当作对象使用。第一种情况,super作为函数调用时,代表父类的构造函数,只能用在子类的构造函数中。ES6 要求,子类的构造函数必须执行一次super函数。第二种情况,super作为对象时,指代父类的原型对象。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。(ES5通过原型模式的继承:创建子类,然后将父类的实例赋值给子类原型,也就是重写子类原型,代之以一个新类型的实例。)ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
如果子类没有定义constructor方法,这个方法会被默认添加。在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
和ES5一样,通过子类创建的实例是父类以及子类的实例:
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
4 补充
1)类的prototype属性和__proto__属性(这段还没看明白,太绕了。)
ES5中每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。ES6中没有构造函数,Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
这样的结果是因为,类的继承是按照下面的模式实现的。
class A {
}
class B {
}
// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);
const b = new B();
// B的实例继承A的静态属性
Object.setPrototypeOf(B, A);
const b = new B();
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。
2)Object.getPrototypeOf
Object.getPrototypeOf方法可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
3)实例的__proto__属性
子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类实例的原型的原型,是父类实例的原型。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
总的来说,ES6是对ES5的形式上的改变,真实内容依旧不变,本质上依旧是通过原型链实现继承。