ES6学习笔记之class类(一)

321 阅读5分钟

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