ES6的class

本文详细介绍了JavaScript中的类(class)的使用,包括构造函数、实例属性、静态方法、取值函数和存值函数、属性表达式、Class表达式等。同时,讨论了类的继承机制,包括子类继承父类的方法、静态方法、原型链、new.target属性以及如何实现Mixin模式。文章还涉及了私有方法和私有属性的概念及使用,并讲解了类的静态属性和实例属性的新写法。
摘要由CSDN通过智能技术生成

一、简介

1、类的由来

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);

class只是一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法
class改写

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

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
  • constructor() 构造方法
  • this关键字代表实例对象
  • 方法前面不加function
  • 方法与方法之间不用逗号分隔
  • 创建实例时用new

类class是构造函数的另一种写法:

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

类的数据类型就是函数,类本身就指向构造函数

在类的实例上面调用方法,其实就是调用原型上的方法

class B {}
const b = new B();

b.constructor === B.prototype.constructor // true

prototype对象的constructor()属性,直接指向“类”的本身

Point.prototype.constructor === Point // true

类的内部所有定义的方法,都是不可枚举的(non-enumerable)

2、constructor方法

constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。

constructor()方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

class Foo {
  constructor() {
    return Object.create(null);
  }
}

new Foo() instanceof Foo
// false

constructor()函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。

类必须使用new调用,否则会报错。

3、类的实例

实例的属性除非显式定义在其本身(即定义在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

proto 并不是语言本身的特性,不建议在生产中使用该属性,避免对环境产生依赖。

可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例

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

get和set,拦截该属性的存取行为

存值函数和取值函数是设置在属性的 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

存值函数和取值函数是定义在html属性的描述对象上面

5、属性表达式

类的属性名,可以采用表达式。

let methodName = 'getArea';

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

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

6、Class 表达式

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

Me只在 Class 的内部可用,指代当前类。(类的内部没用到的话,可以省略Me)
在 Class 外部,用MyClass引用

7、注意点

(1)严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。

(2)不存在提升

类不存在变量提升(hoist)

(3)name 属性

函数的许多特性都被Class继承,包括name属性。

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

(4)Generator 方法

方法前加上星号(*),表示是一个 Generator 函数

(5)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

由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错

解决方法:

  1. 在构造方法中绑定this
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}
  1. 使用箭头函数
class Obj {
  constructor() {
    this.getThis = () => this;
  }
}

const myObj = new Obj();
myObj.getThis() === myObj // true

箭头函数内部的this总是指向定义时所在的对象。
箭头函数处于构造函数内部,this总是指向实例对象
3. 使用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());

二、静态方法

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

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

注意,静态方法的this指的是,而不是实例。

  • 类中的静态方法可以调用静态方法
  • 父类的静态方法,可以被子类继承
  • 静态方法也是可以从super对象上调用的
class Foo {
  static classMethod() {
    return 'hello';
  }
}

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

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

三、实例属性的新写法

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层

class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

四、静态属性

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

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

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

五、私有方法和私有属性

1、概念

私有方法私有属性,是只能在类的内部访问的方法和属性,外部不能访问。

2、私有属性的提案

#表示私有属性和私有方法

class Foo {
  #a;
  #b;
  constructor(a, b) {
    this.#a = a;
    this.#b = b;
  }
  #sum() {
    return this.#a + this.#b;
  }
  printSum() {
    console.log(this.#sum());
  }
}
  • 私有属性也可以设置 getter 和 setter 方法
  • 私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性
    - 私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法

3、in 运算符

in运算符可以用来判断私有属性

class A {
  use(obj) {
    if (#foo in obj) {
      // 私有属性 #foo 存在
    } else {
      // 私有属性 #foo 不存在
    }
  }
}
  1. in可以和this一起使用
  2. 判断私有属性时,in只能用在定义该私有属性的类的内部
  3. 子类从父类继承的私有属性,也可以使用in运算符来判断。
  4. in运算符对于Object.create()Object.setPrototypeOf形成的继承,是无效的,因为这种继承不会传递私有属性

六、静态块

静态块(static block),允许在类的内部设置一个代码块,在类生成时运行一次,主要作用是对静态属性进行初始化。还有一个作用,就是将私有属性与类的外部代码分享

  1. 每个类只能有一个静态块,在静态属性声明后运行。静态块的内部不能有return语句
  2. 静态块内部可以使用名或this,指代当前类
class C {
  static x = 1;
  static {
    this.x; // 1
    // 或者
    C.x; // 1
  }
}

七、new.target 属性

  • new从构造函数生成实例对象的命令。
  • new.target属性一般用在构造函数之中,返回new命令作用于的那个构造函数
  • 如果构造函数不是通过new命令或Reflect.construct()调用的,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 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会报错。

八、class的继承

1、简介

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。
super在这里表示父类的构造函数,用来新建一个父类的实例对象

子类必须在constructor()方法中调用super(),否则就会报错。
继承机制:

  • ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后
  • ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。
  1. 在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类
  2. 除了私有属性和私有方法,父类的所有属性和方法,都会被子类继承,其中包括静态方法。

2、Object.getPrototypeOf()

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

class Point { /*...*/ }

class ColorPoint extends Point { /*...*/ }

Object.getPrototypeOf(ColorPoint) === Point
// true

可以使用这个方法判断,一个类是否继承了另一个类

3、super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。

(1)第一种情况,super作为函数调用时,代表父类的构造函数

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

new.target指向当前正在执行的函数
super()只能用在子类的构造函数之中,用在其他地方就会报错。

(2)第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

  • 由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
  • 在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例
  • this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性
  • 子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例
  • 使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错
  • super.valueOf()表明super是一个对象
  • 由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字

4、类的 prototype 属性和__proto__属性

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

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

Object.setPrototypeOf方法的实现:

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

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

第一种,子类继承Object类。

class A extends Object {
}

A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

第二种情况,不存在任何继承。

class A {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

5、实例的 proto 属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

6、原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构

ECMAScript 的原生构造函数:
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数

class VersionedArray extends Array {
  constructor() {
    super();
    this.history = [[]];
  }
  commit() {
    this.history.push(this.slice());
  }
  revert() {
    this.splice(0, this.length, ...this.history[this.history.length - 1]);
  }
}

var x = new VersionedArray();

x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]

x.commit();
x.history // [[], [1, 2]]

x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]

x.revert();
x // [1, 2]

7、Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

实现: 将多个类的接口“混入”(mix in)另一个类

function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝静态属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== 'constructor'
      && key !== 'prototype'
      && key !== 'name'
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

原文链接:

  1. Class的基础语法
  2. Class的继承
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值