Class基本语法
JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
上面这种写法跟传统的面向对象语言(比如C++和Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。例如:
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。
class Point {
constructor(){
// ...
}
toString(){
// ...
}
toValue(){
// ...
}
}
// 等同于
Point.prototype = {
toString(){},
toValue(){}
};
由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
ES5:
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
class类:
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
constructor方法
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor() {}方法会被默认添加。
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。例如:
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
类的构造函数,不使用new是没法调用的,会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
class Foo {
constructor() {
return Object.create(null);
}
}
Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
类的实例对象
生成类的实例对象的写法,与ES5完全一样,也是使用new命令。如果忘记加上new,像函数那样调用Class,将会报错。
// 报错
var point = Point(2, 3);
// 正确
var point = new Point(2, 3);
与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上),类的所有实例共享一个原型对象,且可以通过实例的__proto__属性为Class添加方法。
不存在变量提升
Class不存在变量提升(hoist),这一点与ES5完全不同。
this的指向
类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class Log {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const log = new Log();
const { printName } = log;
printName(); // TypeError: Cannot read property 'print' of undefined
解决方案
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
另一种解决方法是使用箭头函数。
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
// ...
}
还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger = selfish(new Logger());
小结
class类是基于原型的继承的语法糖。拥有原型对象的特点,可以将自己的属性共享给新的对象。
但两者还是有一些区别的
1. 写法上
在ES5中,我们是通过prototype和Object.create()来实现类和继承
function Person(){}
function Student(){}
Student.prototype = new Person()
而在ES6中,我们使用class语法,通过extends来实现类和继承,但实际原理还是通过原型链实现的
class Person{}
class Student extends Person{}
2. 提升上的区别
在ES5中的function是可以提升的,而class和let,const一样具有块级作用域的概念,不存在变量提升,可能会导致报错
3.class内部会采用严格模式
在ES5中的function中,默认是使用非严格模式的,而在ES6的class中,因为使用了严格模式,所以下面的写法是错误的:
class Foo{
constructor(){
fo = 42
}
}
var foo = new Foo()
// Uncaught ReferenceError: fo is not defined
4. class的方法无法被枚举
在ES5的function中的方法,可以被枚举出来,而在ES6的class中的方法,无法被枚举出来。
class Foo{
constructor(){
this.foo = 42
}
fn(){}
static sfn(){}
}
Object.keys(Foo)
// []
Object.keys(Foo.prototype)
// []
5. class的方法没有原型对象,无法使用new来调用
在ES5中,我们在function上和其原型对象上建立的函数,都是有原型对象的,因此可以通过new来构造一个对象,但在class中的方法,是不能通过new来构造一个对象的
class Foo{
constructor(){
this.foo = 42
}
static sFn(){}
fn(){}
}
var foo = new Foo()
var fn = new foo.fn()
// Uncaught TypeError: foo.fn is not a constructor
var sFn = new foo.sFn()
// Uncaught TypeError: foo.sFn is not a constructor
6. class必须使用new来调用
在ES5中,我们使用function来模拟类的,但实际上,function仍然是一个函数,所以我们还是可以直接调用函数,而对于class,我们不能直接调用,因为它不是一个方法
7. class内部无法重写类名
对于function,只要当前函数作用域里面没有和当前函数名相同的变量,我们就可以直接在function内部对当前function的变量名做一个新的指向,而对于class来说,这是行不通的
class Foo{
constructor(){
this.foo = 42
Foo = 'new Foo'
}
}
var f = new Foo()
// Uncaught TypeError: Assignment to constant variable.
8. 实现继承时的__proto__的指向不同
我们在ES5使用function实现继承时,是通过改变prototype指向做到的,而使用ES6中的class实现类的继承的时候,__proto__是指向父类的
class Fa{}
class Ch extends Fa{}
Ch.__proto__===Fa
// true