概述
ES6引入了class以接近传统的面向对象(java、c++)语法。我觉得这不是很有必要。因为在继承方面它和java/c++完全不一样,这样会对新手可能会造成困扰(虽然java我已经忘掉了)。
实际上,class可以看做一个语法糖,它的绝大部分功能都可以由ES5做到,在此基础之上,增加了一些功能而已。使用class只是让js更像面向对象编程的语法而已。。
在es6中,定义一个class可能会如下所示。
class Point {
constructor(x, y) { // 定义一个class时constructor是可选的
this.x = x;
this.y = y;
}
toString() {
return '('+ this.x + ', '+ this.y +')';
}
}
var p = new Point(1, 1);
这里定义了一个“类”(我很不愿意说在js中是有类的)。可以看到有一个constructor方法,这就是构造方法。除此之外,还有一个toString方法,用以改写原型链上层的toString方法。需要注意的是:
- 使用class而不是function
- Point后面没有括号
- 方法定义之间没有逗号
- 方法定义的时候直接是方法名后面跟着括号
- 除了constructor方法外别的方法都定义在原型上
所以如果用ES5的写法,上面的定义为:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return '('+ this.x + ', '+ this.y +')';
}
var p = new Point(1, 1);
有所区别的是:类(class)中定义的所有方法都是不可枚举的。
Object.keys(Point.prototype)
[]
Object.getOwnPropertyNames(Point.prototype)
["constructor", "toString"]
这不同与ES5。ES5原型上constructor本来就是不可枚举的,而自定义的toString是可枚举的。
Object.keys(Point.prototype)
["toString"]
Object.getOwnPropertyNames(Point.prototype)
["constructor", "toString"]
类和对象
new用来生成一个一个对象,它会有以下四个步骤:(摘自你不知道的javascript)
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数会自动返回这个新对象。
也就是说,在constructor或者function Point(){}中,如果返回了一个新对象,那么new操作得到的就是这个return的对象(上述三步无效)。否则,就是new出来的对象。
class Point {
constructor() {
return Object.create(null);
}
}
new Point() instanceof Point // false
在ES5中,使用
var p = Point(1, 1)
时构造函数中的this会绑定到window(非严格模式)。但是在ES6中省略new会报错。
与ES5一样。类的所有实例共享同一个原型对象。
var p1 = new Point(1, 1);
var p2 = new Point(1, 2);
p1.__proto__ === Point.prototype; //true
p1.__proto__ === p2.__proto__;
每个对象都有__proto__
属性,这是一种非正式的写法。如果要取得一个对象的原型,可以使用Object.getPrototypeOf方法。
Object.getPrototypeOf(Object.prototype); //null
同理,如果要手动设置一个对象的原型,可以使用Object.setPrototypeOf(后续的继承中会写到)。
name属性
与ES5一样,name属性总是返回跟在class关键字后面的类名。
class Point {}
Point.name; // "point"
length属性
与ES5一样,length属性返回构造器参数的个数。
class Foo {
constructor(x, y, z) {}
}
Foo.length; // 3
class表示式
const MyClass = class Me {
getClassName() {
return Me.name;
}
}
上述的代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me。Me只是对类的内部可见的,在外部是未定义的。
不存在变量提升
在ES5中,存在变量提升(函数会提升到变量前面)。
new Foo();
function Foo() {};
上述的代码是没有问题的。然而,在ES6中,
new Foo()
class Foo {}
这样是会报错的。
严格模式
类和模块的内部默认就是严格模式。所以不需要再次使用”use strict”声明了。
继承
class之间可以通过extends继承,更加清晰明了。
class ColorPoint extends Point {}
上面的代码表示类ColorPoint继承了Point,如果在ColorPoint中显式使用了constructor,那么必须要使用到super方法调用父类的构造器(借用构造函数继承)。
如果子类没有显式定义constructor方法,那么这个方法会被默认添加,代码如下。
constructor(...args) {
super(...args)
}
class ColorPoint extends Point {
constructor(x, y, color) { //如果有constructor,那么就必须使用super
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
这是因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用this方法,子类得不到this对象报错。只有通过super方法继承了父类的this后,才可以使用this。
class Point { constructor(x) {this.x = 1;} };
class ColorPoint extends Point {
constructor() {
}
}
var cp = new ColorPoint(); // ReferenceError: this is not defined
而在ES5中,类似于这种借用构造函数的继承是先创造子类的实例对象this,然后通过Point.call(this,x,y)这种方法实现。
实际上,这个继承操作做了两步动作:
ColorPoint.__proto__ === Point; // true
ColorPoint.prototype.__proto__ === Point.prototype; // true
使用ES6的写法就是:
Object.setPrototypeOf(ColorPoint, Point);
Object.setPrototypeOf(ColorPoint.prototype, Point.prototype);
有三种情况比较特殊:
- 继承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 A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
Object.getPropertyOf()
这个方法可以从子类上获取父类。
Object.getPropertyOf(ColorPoint) === Point; // true
super
super代表父类实例或者父类(静态方法中)。
class B extends A {
get m() {
return this._p * super._p;
}
set m() {
throw new Error('该属性只读');
}
}
上面的代码中,子类通过super关键字调用了父类实例的属性。
由于对象总是继承其他对象的,所以可以在任意一个对象上调用super关键字。
var obj = {
toString() {
return super.toString()
}
}
obj.toString(); // "[object Object]"
__proto__
关于__proto__
。任意一个对象都有__proto__
属性。你可以称呼为笨蛋proto。但是__proto__
并不是标准而且无法兼容所有浏览器。如果要设置原型,还是推荐Object.setPrototypeOf()
。
function foo() {}
foo.__proto__ === Function.prototype; // true
class foo {}
foo.__proto__ === Function.prototype; // true
// 接着上面的ColorPoint继承Point
Point.__proto__ === Function.prototype; // 诸君看吧!
var p1 = new Point(2,3);
var p2 = new ColorPoint(2, 3, 'red');
// 下面的返回值都是true
p1.__proto__ === Point.prototype;
p2.__proto__ === ColorPoint.prototype;
ColorPoint.prototype.__proto__ === Point.prototype;
Point.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null; // 到此结束!
原生构造函数的继承
原生构造函数指语言内置的构造函数。比如object下面有九种内置子类型,都可以作为构造函数使用。分别是:
- Number
- String
- Boolean
- Array
- Object
- Date
- Function
- RegExp
- Error
以前,这些原生的构造函数是无法继承的。因为ES5的继承方式是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承远程的构造函数。比如Array有个内部属性[[DefineOwnProperty]],用于定义新属性时更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
// 属性描述符
value: MyArray,
writable: true,
configurable: true,
enumerable: true
})
var colors = new MyArray();
colors[0] = 'red';
colors.length; // 0
colors.length = 0;
colors[0]; // 'red';
而在ES6中,是可以继承的。
class MyArray extends Array {
constructor() {
super();
}
}
var colors = new MyArray();
colors[0] = 'red';
colors.length; // 1
colors.length = 0;
colors; // []
class的getter和setter
同ES5。可能以后会写属性描述符,那里再介绍。
class+Generator
同ES5定义类似。如果在某个方法前加上(*),表示这是一个generator函数。
class Foo {
* a() { yield 1; }
}
var f = new Foo();
var i = f.a();
i.next(); // Object {value: 1, done: false}
i.next(); // Object {value: undefined, done: true}
class的静态方法
class Foo {
static classMethod() { return 'hello'; }
}
var f = new Foo();
f.classMethod(); // TypeError: f.classMethod is not a function
Foo.classMethod(); // "hello"
另外,父类的静态方法可以被子类继承。
class Foo {
static classMethod() { return 'hello'; }
}
class Bar extends Foo {
static classMethod() { return super.classMethod() + ', too'; }
}
Bar.classMethod(); // "hello, too"
这里super指的是父类(foo),并不是父类的一个实例(所以上述的说法有失偏颇)。
只有static标注的方法是给类直接调用的,而且在子类的非静态方法中调用super是会报错的。比如去掉Bar类的static方法。使用实例调用classMethod会报错。
class的静态属性
静态属性指的是Class本身的属性,即Class.propname。而不是实例对象的属性(这个定义在this上)。
class Foo {}
Foo.prop = 1;
Foo.prop; // 1
上面的这种写法可以读写Foo类的静态属性。
ES7有一个静态属性的提案,目前babel转码器已经支持。
这个属性对静态属性和实例属性都规定了新写法。
// 实例属性的新写法
class MyClass {
myProp = 1;
constructor() {
console.log(this.myProp);
}
}
// 静态属性的新写法
class MyClass {
static myStaticProp = 12;
constructor() {
console.log(MyClass.myStaticProp)
}
}
new.target
new是构造函数生成实例的命令。ES6添加了new.target属性,返回构造函数的名字。如果构造函数不是通过new命令调用的,那么new.target会返回undefined。之前通过this instanceof Foo
这种写法。
function Person(name) {
if ( new.target == Person ) { // this instanceof Person
this.name = name;
} else {
throw new Error('不是使用new调用的');
}
}
子类继承父类时new.target会返回子类的名称。