JavaScript 类中的 super 关键字

1,849 阅读6分钟

super 关键字

在 JavaSCript 中,能通过 extends 关键字去继承基类。super 关键字在派生类中有以下用法:

  • 当成函数调用 super()
  • 作为 "属性查询" super.propsuper[expr]

super() 用于调用基类构造函数

super.propsuper[expr] 用于获取基类的静态属性和方法、获取实例对象的原型对象(基类的 [[Prototype]])上的属性和方法。

super()

super 作为函数调用时,只能使用在派生类的构造函数中。

super() 调用的是基类的构造函数,而且必须在使用 this. 之前调用,和在构造函数返回之前调用。例子:

class Polygon {
  constructor(height, width) {
    this.name = 'Polygon';
    this.height = height;
    this.width = width;
  }
}
class Square extends Polygon {
  constructor(length) {
    super(length, length);
    // 注意:必须比 'this.' 先调用 super()
    this.name = 'Square';
  }
}
const square = new Square(20);
console.log(square)
// 打印:Square: { "name": "Square", "height": 20, "width": 20 }

如果派生类有自定义构造函数,必须调用 super()。没调用会抛出异常。例子:

class Polygon {
    constructor() {
        this.name = 'Polygon';
    }
}
class Square extends Polygon {
    constructor() {
        // Error:Constructors for derived classes must contain a 'super' call
    }
}

如果没有自定义构造函数,JavaScript 会默认调用。例子:

class Polygon {
    constructor(name,height,width) {
        this.name = name;
        this.height = height;
        this.width = width;
    }
}
class Square extends Polygon {}
// 等同于
// class Square extends Polygon {
//     constructor(...arg) {
//         super(...arg)
//     }
// }
const square = new Square('square', 20, 20);
console.log(square)
// 打印:Square: { "name": "square", "height": 20, "width": 20 }

super.prop & super[expr]

super.propsuper[expr] 可用于派生类的所有地方,在不同地方有不同的作用。

  • 在派生类的静态方法或静态字段声明上使用,可获取基类的静态属性和方法。
  • 在构造函数、原型方法、实例方法、实例字段声明上使用,可获取实例对象的原型对象(基类的 [[Prototype]])上的属性和方法。

获取基类的静态属性和方法

在派生类的静态方法或静态字段声明上使用 super.prop,可获取基类的静态属性和方法。

例子:

class Rectangle {
  static baseStaticField = 90;
  static logNbSides() {
    return 'I have 4 sides';
  }
}
class Square extends Rectangle {
  // 静态字段初始化
  static extendedStaticField = super.baseStaticField; // 90
  static logDescription() {
    return `${super.logNbSides()} which are all equal`;
  }
}
Square.logDescription(); 
// 打印:'I have 4 sides which are all equal'

上面个例子中,在派生类的静态方法中调用 super.logNbSides(),实际上调用的是基类的 logNbSides 静态方法。而且在派生类静态字段初始化期间访问了基类的 baseStaticField 静态属性。

获取实例对象的原型对象属性和方法

在构造函数、原型方法、实例方法、实例字段声明上使用 super.prop,可获取实例对象的原型对象(基类的 [[Prototype]])上的属性和方法。

例子:

class Polygon {
    name(){
       return 'Polygon'
    }
}

class Square extends Polygon {
    constructor() {
        super()
        this.instanceProp = super.name()
    }
    // 这也是创建 实例属性/方法 的写法
    instanceName =  super.name()
    instanceFn = ()=>{ console.log(super.name()) }
    // 原型方法
    prototypeName () { console.log(super.name()) }
}
const square = new Square();
square.instanceFn()              // 'Polygon'
square.prototypeName()           // 'Polygon'
console.log(square.instanceProp) // 'Polygon'
console.log(square.instanceName) // 'Polygon'

例子中,所有的属性和方法都获取到了基类的 [[prototype]] 属性上的 'name' 方法

类声明中,是无法声明原型对象字段的,只能声明原型方法

在对象字面量使用 super.prop

super 也可以用在对象字面量中:

const obj1 = {
  method1() {
    console.log("method 1");
  },
};

const obj2 = {
  method2() {
    super.method1();
  },
};

Object.setPrototypeOf(obj2, obj1);
obj2.method2(); // "method 1"

在这个例子中,两个对象各自定义了一个方法。在第二个对象中,使用 super 调用第一个对象的方法。必须使用 Object.setPrototypeOf()obj2 的原型对象设置为 obj1,这样 super 才能找到 obj1 上的 method1 方法。

super 的引用

访问 super.x 的行为类似于 Reflect.get(Object.getPrototypeOf(objectLiteral), "x", this),这意味着总是在实例对象/类声明的原型上寻找属性,并且解除绑定和重新绑定方法不会更改 super 的引用(尽管它们确实会更改 this 的引用)。

class Base {
  baseGetX() {
    return 1;
  }
}
class Extended extends Base {
  getX() {
    return super.baseGetX();
  }
}

const e = new Extended();
console.log(e.getX()); // 1
const { getX } = e;
console.log(getX()); // 1

只有重置原型对象才会改变 super 的引用:

class Base {
  baseGetX() {
    return 1;
  }
  static staticBaseGetX() {
    return 3;
  }
}

class AnotherBase {
  baseGetX() {
    return 2;
  }
  static staticBaseGetX() {
    return 4;
  }
}

class Extended extends Base {
  getX() {
    return super.baseGetX();
  }
  static staticGetX() {
    return super.staticBaseGetX();
  }
}

const e = new Extended();
// 重置实例继承
Object.setPrototypeOf(Extended.prototype, AnotherBase.prototype);
console.log(e.getX()); // 2
console.log(Extended.staticGetX()); // 仍然为 3,因为我们没有修改静态部分
// 重置静态继承
Object.setPrototypeOf(Extended, AnotherBase);
console.log(Extended.staticGetX()); // 4

注意,super 的引用是由声明 super 的类或对象字面量决定的,而不是由调用该方法的对象决定的。因此,取消绑定或重新绑定方法不会更改其中的 super 引用(尽管它们确实会更改 this 的引用)。

使用 super 调用方法,方法中的 this 指向

使用 super.prop 调用方法时,方法中的 this 指向调用 super.prop 的上下文对象。

class Base {
  getName() {
    console.log(this.name);
  }
}

class Extended extends Base {
    name = "extendedInstance"
    getName() {
        super.getName();
    }
}
const obj = new Extended()
obj.getName.call({name: 'Julie'}); // "Julie"

注意,下面例子中,类默认有静态属性 name,值为类名:

class Base {
  static getName() {
    console.log(this.name);
  }
}

class Extended extends Base {
  static getName() {
    super.getName();
  }
}

Extended.getName(); // "Extended"

这在与 私有属性 交互时尤为重要。会因为 this 的改变,而获取不到私有属性和静态私有属性。

为 super 设置属性

当为 super 设置属性时,例如 super.x = 1,其行为类似于 Reflect.set(Object.getPrototypeOf(objectLiteral), "x", 1, this)。所以将 super 简单理解为 "原型对象的引用" 是不准确的,因为它实际上设置的是 this 的属性。

class A {}
class B extends A {
  setX() {
    super.x = 1;
  }
}

const b = new B();
b.setX();
console.log(b); // B { x: 1 }
console.log(Object.hasOwn(b, "x")); // true

super.x = 1 将在 A.prototype 上查找属性 x 的描述符(并调用那里定义的 setter),但是 setter 中的 this 将被设置为 b。可参考 Reflect.set,了解 Reflect.set 目标和接收者不同的情况下的更多详细信息。

设置 super 的属性会受到上下文 this 改变的影响:

const b2 = new B();
b2.setX.call(null);
// TypeError: Cannot assign to read only property 'x' of object 'null'

如果在原型对象中,该属性是不可改写的,那么意味着不能对该属性重新赋值。

class X {
  constructor() {
    // 创建一个不可改写的属性
    Object.defineProperty(X.prototype, "prop", {
      configurable: true,
      writable: false,
      value: 1,
    });
  }
}

class Y extends X {
  constructor() {
    super();
  }
  foo() {
    super.prop = 2; // 不能重写该值
  }
}

const y = new Y();
y.foo(); // TypeError: "prop" is read-only
console.log(y.prop); // 1

小结:这意味着,虽然 super 的引用不受到上下文 this 改变的影响,但是 super.prop 为方法时,方法中的 this 会受影响。

而设置 super.prop 时,会找到基类的 [[Prototype]] 对象的属性,调用该属性的描述符 setter 方法,setter 方法中的 this 也会受到影响,从而变成了给该上下文 this 设置属性。

删除 super 上的属性,会抛出错误

不能使用 delete 操作符和 super.propsuper[expr] 去删除基类的属性,会抛出一个 引用错误

class Base {
  foo() {}
}
class Derived extends Base {
  delete() {
    delete super.foo; // this is bad
  }
}

new Derived().delete(); // ReferenceError: invalid delete involving 'super'.

其它

super 是一个关键字,并有一些特殊语法结构。并不是指向原型对象的变量。试图读取 super 会抛出 语法错误.

const child = {
  myParent() {
    console.log(super); // SyntaxError: 'super' keyword unexpected here
  },
};

感谢观看,如有错误,望指正

MDN 参考连接:developer.mozilla.org/en-US/docs/…