你的浏览器不支持canvas

Enjoy life!

ES6 - Class的基本语法

Date: Author: JM

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。

一、class简介

1.1 简单例子

  • 通过class关键字,可以定义类。

  • es5 语法

function Point (x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function() {
  return `this.x = ` + this.x + ', this.y = '+ this.y; 
};

var p = new Point(10, 20);
  • es6语法:class改写上面的代码
// 定义类
class Point {
    constructor (x, y) {
        this.x = x;
        this.y = y;
    }

    toString () {  // 等价于 Point.prototype.toString = function(){}
        return `(${this.x},${this.y})`;
    }
}

// new 命令
var p = new Point(10, 20);
console.log(p.toString()); // (10,20)

typeof Point; // function

1.2 class注意事项

  • 定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。

  • 方法之间不需要逗号分隔,加了会报错。

  • 类必须使用new调用,否则会报错;普通构造函不用new也可以执行。。

  • 类的所有方法都定义在类的prototype属性上面

  • 类不存在变量提升(hoist
    • 保证子类在父类之后定义。
  • prototype对象的constructor属性,直接指向“类”的本身。
Point.prototype.constructor === Point // true
  • 类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype);
// []
// 如果是es5,是可枚举的: ["toString"]

Object.getOwnPropertyNames(Point.prototype);
// ["constructor","toString"]
  • 类的属性名,可以采用表达式。
let methodName = 'getData';

class Square {
    constructor () {
        //...
    }
    [methodName] () {
        // ...
    }
}

1.3 一次向类添加多个方法 Object.assign

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

1.4 constructor 方法

  • constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。
  • 一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
  • constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
class Foo {
    constructor () {
         // console.log(this); // Foo {}
        return Object.create(null);
    }
}
new Foo() instanceof Foo; //false

// new Foo() === > this ====> Object {}

1.5 类的实例对象

  • 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
  • 类的所有实例共享一个原型对象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.__proto__ === p2.__proto__
//true
  • 生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

1.6 class表达式

const MyClass = class Me {
    getName () {
        // console.log(Me);  // class Me {getName () {console.log(Me, this);return Me.name;}}
        return Me.name;
    }
}

let inst = new MyClass();
inst.getName(); // Me
console.log(Me.name); // ReferenceError: Me is not defined

const MyClass = class {};
  • 这个类的名字是MyClass而不是MeMe只在 Class 的内部代码可用,指代当前类。
  • Me只在 Class 内部有定义。
  • 如果类的内部没用到的话,可以省略Me

  • 采用 Class 表达式,可以写出立即执行的 Class
let person = class {
    constructor (name) {
        this.name = name;
    }
    
    sayName () {
        return this.name;
    }
}('jm');

person.sayName(); // jm

1.7 私有方法

  • ES6 不提供私有方法,只能通过变通方法模拟实现。
    • 1.在命名上加以区别
      • 这种命名是不保险的,在类的外部,还是可以调用到这个方法。
      class Widget {
        
        // 公有方法
        foo (baz) {
          this._bar(baz);
        }
        
        // 私有方法
        _bar(baz) {
          return this.snaf = baz;
        }
        
        // ...
      }
    
    • 2.将私有方法移出模块,因为模块内部的所有方法都是对外可见的。
      class Widget {
        // foo是公有方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。
        
        foo (baz) {
          bar.call(this, baz);
        }
        
        // ...
      }
        
      function bar(baz) {
        return this.snaf = baz;
      }
    
    • 3.利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。
          // bar和snaf都是Symbol值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果。
            
          const bar = Symbol('bar');
          const snaf = Symbol('snaf');
            
          export default class myClass{
            
            // 公有方法
            
            foo(baz) {
              this[bar](baz);
            }
            
            // 私有方法
            
            [bar](baz) {
              return this[snaf] = baz;
            }
            
            // ...
          };
    

### 1.8 私有属性

  • ES6 不支持私有属性
  • class加私有属性:方法是在属性名之前,使用#表示。
class Point {
    // #x就表示私有属性x,在Point类之外是读取不到这个属性的。
    #x;
    
    // 初始化:#x = 0;
    
    constructor (x = 0) {
        #x += x;
        
        // 初始化后: #x; // 此时 #x = 0;
    }
    
    get x () {
        return #x;
    }
    
    set x (value) {
        #x += value;
    }
}
  • 私有属性与实例的属性是可以同名的(比如,#x与get x())。
  • 私有属性可以指定初始值,在构造函数执行时进行初始化。
  • 也可以用来写私有方法
class Foo {
  #a;
  #b;
  #sum() { return #a + #b; }
  printSum() { console.log(#sum()); }
  constructor(a, b) { #a = a; #b = b; }
}

1.9 私有方法

  • 类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class Logger {
    printName (name = 'there') {
        this.print(`Hello,${name}`);
    }
    
    print (text) {
        console.log(text);
    }
}

const logger = new Logger();

const { printName } = logger; // 等价于 const printName = logger.printName; 其实就是解构

printName(); // TypeError: Cannot read property 'print' of undefined
  • const { printName } = logger;这一行代码,导致this指向的不是Logger类的实例,而是指向该方法运行时所在的环境,所以因找不到print方法而导致报错。

解决方法

  • 1.在构造方法中绑定this
class Logger {
    constructor () {
        this.printName = this.printName.bind(this);
    }
    
    // ...
}
  • 2.使用箭头函数
class Logger {
    construtor () {
        this.printName = (name = 'there') => {
             this.print(`Hello ${name}`);
        }
    }
    // ...
}
  • 3.使用Proxy,获取方法的时候,自动绑定this。 ```js 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());


### 1.10 name属性

* `name`属性总是返回紧跟在`class`关键字后面的类名。

```js
class Point {}
Point.name // "Point"

1.11 Class 的取值函数(getter)和存值函数(setter)

  • 与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

// prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。
  • 存值函数和取值函数是设置在属性的 Descriptor 对象上的。
class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
);

"get" in descriptor  // true
"set" in descriptor  // true

  • 报错:Uncaught RangeError: Maximum call stack size exceededdemo
  • 原因:在构造函数中执行 this.name = name 的时候,就会去调用 set name,在set name 方法中,我们又执行 this.name = name,进行无限递归,最后导致栈溢出( RangeError )。
 class Person {
    constructor(name) {
      this.name = name
    }
    // getter
    get name() {
      return this.name
    }
    // setter
    set name(name) {
      this.name = name
    }
  }

  let p1 = new Person();
  p1.name = 'jm'
  // 报错: Uncaught RangeError: Maximum call stack size exceeded
  • 解决方法:demo
  • 补充:以上 namegettersetter 只是给 name 自定义存取值行为,开发者还是可以通过 _name 绕过 gettersetter 获取 name 的值。
class Person {
    constructor(name) {
      this.name = name
    }
    // getter
    get name() {
      return this._name
    }
    // setter
    set name(name) {
      this._name = name
    }
  }

  let p1 = new Person();
  p1.name = 'jm'
  console.log(p1.name) // 'jm

1.12 Class 的 Generator 方法

  • 如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。
class Foo {
  constructor(...args) {
    this.args = args;
  }
  
  // Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。
  // Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。
  * [Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world

1.13 Class 的静态方法

  • “静态方法”:在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用
    • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。
  • 如果静态方法包含this关键字,这个this指的是类,而不是实例。
class Foo {
  static bar () {
   // this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz
    this.baz();
  }
  // 静态方法可以与非静态方法重名。
  static baz () {
    console.log('hello');
  }
  baz () {
    console.log('world');
  }
}

// bar 直接在Foo类上调用
Foo.bar() // hello

// bar 不能在Foo类的实例上调用,如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法
var foo = new Foo();
foo.bar(); // TypeError: foo.classMethod is not a function
  • 父类的静态方法,可以被子类继承。
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'
  • 静态方法也是可以从super对象上调用的。
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"

1.14 Class 的静态属性和实例属性

  • 静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

  • 例子: 为Foo类定义了一个静态属性prop

    • 目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。
class Foo {
}

Foo.prop = 1;
Foo.prop // 1

// 以下两种写法都无效
class Foo {
  // 写法一
  prop: 2

  // 写法二
  static prop: 2
}

Foo.prop // undefined

有一个静态属性的提案,对实例属性和静态属性都规定了新的写法。

  • 1.类的实例属性
    • 类的实例属性可以用等式,写入类的定义之中。
class MyClass {
  myProp = 42; // myProp就是MyClass的实例属性。

  constructor() {
    console.log(this.myProp); // 42
  }
}
  • 2.类的静态属性
    • 类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。
class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}

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

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

1.15 new.target属性

  • new是从构造函数生成实例的命令。
  • ES6new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。
    • 如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用new生成实例');
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错
  • Class 内部调用new.target,返回当前 Class
class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}

var obj = new Rectangle(3, 4); // 输出 true
  • 子类继承父类时,new.target会返回子类。
class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}

var obj = new Square(3); // 输出 false
  • 利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正
  • 上面代码中,Shape类不能被实例化,只能用于继承。

注意,在函数外部,使用new.target会报错。


对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。