es6 class学习笔记

前言

虽然class在实际开发中已经被大量使用,仍然打算做一篇整理好好梳理一下。


基本语法

js语言中,生成实例对象的传统方法就是通过构造函数:

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);
p.toString(); //"(1,2)"

ES6中的class可以看做只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰,更像面向对象编程语法而已
上面的代码用class改写:

//定义类
class Point{
  constructor(x,y){
    this.x = x;
    this.y = y;
  }
  toString () {
    return `( ${this.x} , ${this.y} )`
  }
}
var p = new Point(1,2);
p.toString(); // "(1,2)"

定义了一个Point类,constructor即为构造方法;而this关键字则代表实例对象,也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。范例中Point类除了构造方法,还定义了一个toString方法,定义类的方法的时候,前面不需要加function这个关键字,方法之间不需要逗号分隔
构造函数的prototype属性,在ES6的类上继续存在,实际上,类的所有方法都定义在类的prototype属性上面。

一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
constructor方法默认返回实例对象this。
类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__ // true
// 实例的原型原型都是Point.prototype,所以__proto__相等
// 生产环境我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,
// 然后再来为原型添加方法/属性,尽量避免使用__proto__。

细化

  1. 在类和模块的内部默认就是严格模式,所以不需要use strict指定运行模式,只要代码写在类或者模块之中,就只有严格模式可用
  2. 与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
  3. 属性表达式
let methodName = 'getArea';
class Square {
  constructor(length) {}
  [methodName]() {}
}
// Square类中添加了getArea方法
  1. class表达式
//采用Class表达式,可以写出立即执行Class
let person = new class {
  constructor (name) {
    this.name = name ;
  }
  sayName() {
    console.log(this.name);
  }
}("张三");
person.sayName() //"张三"
//person是一个立即执行类的实例
  1. 不存在变量提升
    这个和ES5完全不一样,会提示ReferenceError,更贴近let、const的风格。这种规定的原因和继承有关,必须保证子类在父类之后定义。
  2. name属性总是返回紧跟class关键字后的类名
  3. 如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数(这一部分重心应该在函数生成器上,衍生开来又是一大篇知识)
  4. this的指向
    类的方法内部如果含有this,他默认指向类的实例,但是,必须非常小心,一旦单独使用该方法,可能会报错
class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }
  print(text) {
    console.log(text);
  }
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
// printName方法中的this,默认指向Logger类的实例。但如果将这个方法提取出来单独使用,
// this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。
// 正确的方法是使用bind、箭头函数或Proxy
  1. 私有方法、私有属性ES6不提供,只能通过变通方法模拟实现。
    命名上私有方法前缀_,私有属性前缀#。(有提案,通过前缀#完成类似private词缀)

静态方法、静态属性

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}
Foo.classMethod() // 'hello'
var foo = new Foo()
foo.classMethod() // TypeError: foo.classMethod is not a function

Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
*注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

class Foo {
  static bar () {
    this.baz();
  }
  static baz () {
    console.log('hello');
  }
  baz () {
    console.log('world');
  }
}
Foo.bar() // hello

静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名
父类的静态方法,可以被子类继承。

class Foo {
  static classMethod() {
    return 'hello';
  }
}
class Bar extends Foo {}
Bar.classMethod() // 'hello

静态方法也是可以从super对象上调用的。
至于静态属性。指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

// 老写法
class Foo {}
Foo.prop = 1;
// 新写法
class Foo {
  static prop = 1;
}

继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point {}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

class ColorPoint extends Point {
// 如果子类没有定义constructor方法,这个方法会被默认添加
}
// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

Object.getPrototypeOf方法可以用来从子类上获取父类。

继承中的super关键词
  1. super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。子类构造函数外的地方不能调用否则报错。
    其代表父类的构造函数,但返回的是子类的实例。相当于Parent.prototype.constructor.call(this)
  2. super作为对象时,在普通方法中指向父类的原型对象;在静态方法中指向父类。

prototype 属性和__proto__属性

关于这两者我在__proto__和prototype中已经做过整理。阮一峰前辈所表达的主要内容:

关于类的prototype和__proto__

Class 作为构造函数的语法糖,同时有prototype属性和proto属性,因此同时存在两条继承链。

  1. 子类的__proto__属性,表示构造函数的继承,总是指向父类。
  2. 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {}
class B extends A {}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
// 上面的结果是因为类的继承按照下面的模式去实现的
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
// 关于setPrototypeOf方法的实现
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
关于实例的__proto__ 属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。


心得总结

class归根结底还是es5原型链的语法糖,学过基础的面向对象课程应该会比较容易上手起来,反倒是先前的prototype使用起来可能会比较反人类。
核心内容其实只要掌握了构造函数、静态方法静态变量、继承。还有就是搞清prototype与__proto__之间的关系,这块内容基本就拿下了。


参考

阮一峰的ES6--Class的基本语法
阮一峰的ES6--Class 的继承

你可能感兴趣的:(es6 class学习笔记)