零碎知识点摘要-0819-class与继承

130 阅读7分钟

class

用来代替老版本生成实例对象的方法,为迎合大众的传统的面向对象语言生成对象的方法新加的一个语法糖。


// ES5
function Point(x, y) {
    this.x = x;
    this.y = y;
}
Point.prototype.toString = function () {
    return `(${this.x}, ${this.y})`
}
// ES6
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    
    toString () {
        return `(${this.x}, ${this.y})`
    }
}

这是一个类的模板。 constructor 是构造方法,而 this 关键字则代表实力对象。对应 ES5 中的构造函数 Point,就是 ES6 中的 Point 的构造方法。 完全可以把 ES6 的类,看作是构造函数的另一种写法。

class Point {
    // ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true

可以看到,类的数据类型就是函数,类本身也指向构造函数,直接使用 new 命令。 类所有的方法都定义在类的 prototype 属性上面。 在类的实例上调用方法,其实就是调用原型上的方法。 那么在类里面定义方法,其实也可以直接在类的原型函数上添加方法。

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

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

类内部定义的所有方法,都是不可枚举的(non-enumerable),与ES5的写法有点不一致。

constructor方法

默认方法,没有显示的写,都会默认添加。

类的实例

  1. 类必须使用 new 调用,否则会报错。而普通构造函数不会,不用 new 也可以执行。
  2. 与 ES5 一样,实例的属性除非显示的定义在期本身,也就是定义在this对象上,否则都是定义在原型上,即定义在class上。
  3. 类和构造函数类似,那么同个类生成的实例共享一个原型对象。

取值函数(getter)和存值函数(setter)

在“类”内部可以使用 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'

属性表达式

let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}

Class 表达式

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

这个 Me 是内部命名,MyClass 是外部命名,Me是内部用的,要是内部没用,那么就可以省略

const MyClass = class {
  getClassName() {
    // ...
  }
};

通过这种方法,可以写出立即执行的 Class

** 注意点 **

  1. 严格模式
  2. 不存在提示(变量提升)
  3. name 属性。通过类名.name 拿到类名
  4. Generator 方法。如果在某个方法钱加星号 *,就表示该方法是一个 Generator函数
  5. this的指向。默认指向类的实例,会导致一些问题出现,导致 this 的指向异常。 解决方法:
// 1.可以在构造方法中绑定 this。
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}
// 2. 箭头函数
class Obj {
  constructor() {
    this.getThis = () => this;
  }
}
const myObj = new Obj();
myObj.getThis() === myObj // true
// 3. 使用 Proxy
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());

静态方法

静态方法只能通过类来调用,不能通过实例调用。静态方法中要是有this,那么这个 this 是指向类的。 静态方法可以继承,并且可以在子类中通过 super 对象来调用。 通过在方法前加 static

实例属性的新写法

实例属性出了放在 constructor 里面之外,也可以放在类的最顶层。

静态属性

同静态方法一样,静态属性是指的 Class 本身的属性,而不是实例对象上的属性。 但是 ES6 明确规定,Class 内部只有静态方法,没有静态属性。

// 原来的
class Foo {
}
Foo.prop = 1;
Foo.prop // 1

但是现在有一个提案提供了类的静态属性,写法是在实例属性前面,加上 static。

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

私用方法和私有属性

是只能在类内部访问的属性和方法,外部不能访问。 ES6 不提供,但是可以通过变相的方法实现。

  1. 私有方法
// 私有方法
class Widget {
    // 公有方法
    foo (baz) {
        this._bar(baz)
    }
    // 私有方法
    _bar (baz) {
        return this.snaf = baz
    }
}

这种方法,不保险,因为外部还是可以访问;另外一个方法,就是将私有方法一处模块,因为模块内部的所有方法都是对外可见的。

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}

上面 foo 是公开的方法,内部调用了 bar.call(this.baz),相当于,我内部调用了一个方法,使得 bar 变成当前模块的私有方法。 除此之外,还有一种,利用 Symbol,这个这里不做描述,具体自行看文档。 这上面三种方法呢,都不是很保险,只能看项目自行选择。 2. 私有属性 目前又一个提案,就是在属性名前加 #,只能在类内部使用,外部使用就会报错。

class IncreasingCounter {
    #count = 0;
}

当然,这个也可以用来写私有方法。

class Foo {
    #a; 
    #b;
    #sum () {
        return #a + #b
    }
}

私有属性可以设置 getter 和 setter 方法。 私有属性不限于 this 引用,只要是在类的内部,实例也可以引用私有属性

class Foo {
    #privateValue = 42;
    static getPrivateValue(foo) {
        return foo.#privateValue
    }
}
Foo.getPrivateValue(new Foo())
  1. 私有方法和私有属性可以和 static 结合
class FakeMath {
  static PI = 22 / 7;
  static #totallyRandomNumber = 4;

  static #computeRandomNumber() {
    return FakeMath.#totallyRandomNumber;
  }

  static random() {
    console.log('I heard you like random numbers…')
    return FakeMath.#computeRandomNumber();
  }
}

FakeMath.PI // 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
// 4
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错

一个很好玩的 new.target 属性

能帮你识别判断,实例是不是通过 new 命令 或 Reflect.construct() 调用,若不是 new.target 会返回 undefined。确保构造函数只能通过 new 命令调用

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

类里面调用

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 会返回子类。 利用上面那个特点,可以写出不能独立使用,必须继承后才能使用的类。

继承

那么,上述说到继承,我们接着聊聊继承。 继承,两点。

  1. 使用 extends
  2. 在 constructor 中使用 super
  3. 出了属性和方法,静态方法也会被继承
class Father { /* ... */}
class Son extends Father {
    constructor(x) {
        super()
        this.x = x // this的调用要在 super() 方法后
    }
}

注意:子类的 constructor 方法中不写 super(),那么子类中不能用 this 对象;要是子类中不屑 constructor 方法,则默认添加,也默认添加 super()

关于继承实现的不同

在 ES5 中,实质是先创建子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,是先将父类实例对象的属性和方法,添加到 this 上面(所以必须先调 super 方法),然后再用子类的构造函数修改 this。

其他

  1. Object.getPrototypeOf(),可以从子类上获取父类
Object.getPrototypeOf(Son) === Father // true
  1. super 关键字 当作函数使用;代表父类的构造函数。
    class A {}
    class B extends A {
        constructor() {
            super()
        }
    }
    // super()在这里相当于A.prototype.constructor.call(this),只能在 constructor中使用
    
    当作对象使用;在普通方法中,指向父类的原型对象。在静态方法中,指向父类。 普通方法汇总 super 指向的是父类的原型对象,所以定义在实例上的方法或属性,无法通过 super 调用。
    super === A.prototype
    

    在子类通过super调用父类方法时,父类方法中的this指向,是当前子类的实例。 在子类通过super调用父类静态方法时,父类静态方法中的this指向,是当前子类。

  2. 类的 prototype 属性和 proto 属性
class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

类的继承按照这种模式实现的

class A {
}

class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

const b = new B();
//////////////
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;

以上内容均自己借鉴写的摘要,不做任何商业用途,若侵权则删除。