分析 JavaScript 中的 new.target 在 Class 和函数构造器中的作用,以及它如何影响继承和构造过程。

各位观众,掌声欢迎!今天咱们要聊聊 JavaScript 里一个有点神秘,但关键时刻能派上大用场的家伙:new.target

开场白:new.target 是啥?

你可能见过 this,知道它指向谁。但 new.target 是什么鬼?简单来说,new.target 就像一个侦探,专门负责追踪你是怎么被“new”出来的。它只在函数或者类的构造函数里有意义。

如果一个函数或者类是用 new 关键字调用的,new.target 就会指向这个函数或类本身。如果不是用 new 调用的,new.target 就是 undefined

第一幕:函数构造器里的 new.target

在 ES5 及之前的年代,我们用函数来模拟类。那时候,防止函数被直接调用,确保只能通过 new 来创建实例,是个常见需求。new.target 出现之前,大家可能会用 this instanceof MyConstructor 这样的方式来判断。现在,有了 new.target,就优雅多了。

function Person(name) {
  if (!new.target) {
    throw new Error("Person 必须用 'new' 调用");
  }
  this.name = name;
}

// 正确用法
const john = new Person("John");
console.log(john.name); // 输出 "John"

// 错误用法
try {
  const jane = Person("Jane"); // 直接调用,会报错
} catch (error) {
  console.error(error.message); // 输出 "Person 必须用 'new' 调用"
}

在这个例子里,如果 Person 函数不是通过 new 调用的,new.target 就是 undefinedif (!new.target) 就会触发错误,阻止了函数的直接调用。

第二幕:Class 里的 new.target

ES6 引入了 class 语法,new.target 在 class 构造函数里也一样有效。但它还有更高级的用法,尤其是在抽象类和继承方面。

class Animal {
  constructor() {
    if (new.target === Animal) {
      throw new Error("Animal 是一个抽象类,不能直接实例化");
    }
  }

  speak() {
    throw new Error("speak 方法必须在子类中实现");
  }
}

class Dog extends Animal {
  constructor(name) {
    super(); // 必须先调用 super()
    this.name = name;
  }

  speak() {
    return "Woof!";
  }
}

// 错误用法
try {
  const animal = new Animal(); // 报错:Animal 是一个抽象类,不能直接实例化
} catch (error) {
  console.error(error.message);
}

// 正确用法
const dog = new Dog("Buddy");
console.log(dog.speak()); // 输出 "Woof!"

这里,Animal 类被设计成一个抽象类,不能直接实例化。new.target === Animal 确保了这一点。只有子类(比如 Dog)才能被 new 出来。

第三幕:new.target 与继承

new.target 在继承中扮演着重要的角色,尤其是在多层继承的情况下。它能准确地告诉你,最终是谁被 new 出来的。

class Base {
  constructor() {
    console.log("Base constructor, new.target:", new.target.name);
  }
}

class Derived extends Base {
  constructor() {
    super();
    console.log("Derived constructor, new.target:", new.target.name);
  }
}

class FurtherDerived extends Derived {
  constructor() {
    super();
    console.log("FurtherDerived constructor, new.target:", new.target.name);
  }
}

const fd = new FurtherDerived();
// 输出:
// Base constructor, new.target: FurtherDerived
// Derived constructor, new.target: FurtherDerived
// FurtherDerived constructor, new.target: FurtherDerived

可以看到,即使在 Base 类的构造函数里,new.target 仍然指向最终被 new 出来的 FurtherDerived 类。这使得我们可以在基类中,根据最终的派生类执行不同的逻辑。

第四幕:new.target 的高级用法:工厂模式

new.target 还可以用于实现更灵活的工厂模式。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error("Shape 是一个基类,不能直接实例化");
    }
  }

  static create(type, ...args) {
    switch (type) {
      case "circle":
        return new Circle(...args);
      case "square":
        return new Square(...args);
      default:
        throw new Error("不支持的形状类型");
    }
  }

  draw() {
    throw new Error("draw 方法必须在子类中实现");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  draw() {
    return `绘制一个半径为 ${this.radius} 的圆形`;
  }
}

class Square extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }

  draw() {
    return `绘制一个边长为 ${this.side} 的正方形`;
  }
}

const circle = Shape.create("circle", 5);
console.log(circle.draw()); // 输出 "绘制一个半径为 5 的圆形"

const square = Shape.create("square", 10);
console.log(square.draw()); // 输出 "绘制一个边长为 10 的正方形"

在这个例子里,Shape 类提供了一个静态方法 create,根据传入的类型创建不同的形状实例。虽然 Shape 本身不能直接实例化,但可以通过 create 方法间接创建子类的实例。

第五幕:new.target 与元编程

new.target 还可以结合 Proxy 等元编程特性,实现更高级的功能,比如控制类的实例化过程。

function classFactory(className, baseClass, properties) {
  const handler = {
    construct(target, args, newTarget) {
      // 在构造函数调用前进行拦截
      console.log(`正在创建 ${className} 的实例...`);
      const instance = Reflect.construct(target, args, newTarget);
      // 在构造函数调用后进行拦截
      console.log(`${className} 的实例创建完成`);
      return instance;
    }
  };

  const proxyClass = new Proxy(baseClass, handler);

  return proxyClass;
}

class MyClass {
  constructor(name) {
    this.name = name;
  }
}

const ProxiedMyClass = classFactory("MyClass", MyClass);

const instance = new ProxiedMyClass("Alice"); // 输出拦截信息
console.log(instance.name); // 输出 Alice

这个例子中,classFactory 函数使用 Proxy 拦截了 MyClass 的构造过程,可以在实例创建前后执行自定义逻辑。

new.target 的总结:

咱们来总结一下 new.target 的作用:

  • 防止函数或抽象类被直接调用: 确保函数只能通过 new 来创建实例,或者阻止抽象类被直接实例化。
  • 在继承中确定最终的派生类: 即使在基类的构造函数里,也能知道最终是谁被 new 出来的。
  • 实现更灵活的工厂模式: 可以根据不同的条件创建不同的实例,而无需直接使用 new 关键字。
  • 结合元编程进行更高级的控制: 可以拦截类的实例化过程,实现更复杂的逻辑。

表格总结:

用途 说明 示例
防止函数/抽象类直接调用 确保函数只能通过 new 调用,或者阻止抽象类被直接实例化。 javascript function Person(name) { if (!new.target) { throw new Error("Person 必须用 'new' 调用"); } this.name = name; } class Animal { constructor() { if (new.target === Animal) { throw new Error("Animal 是一个抽象类"); } } }
在继承中确定最终派生类 即使在基类的构造函数中,也能知道最终哪个类被实例化。 javascript class Base { constructor() { console.log("Base constructor, new.target:", new.target.name); } } class Derived extends Base {} const d = new Derived(); // Base constructor, new.target: Derived
实现工厂模式 根据类型创建不同类的实例,无需直接使用 new javascript class Shape { static create(type) { if (type === 'circle') return new Circle(); if (type === 'square') return new Square(); } }
结合元编程进行高级控制 拦截类的实例化过程,执行自定义逻辑。 javascript const handler = { construct(target, args) { console.log('Creating instance'); return new target(...args); } }; const MyClass = new Proxy(class {}, handler); new MyClass(); // 输出: Creating instance

结尾:

new.target 虽然平时不显山不露水,但在某些场景下,却是解决问题的关键。掌握了它,你的 JavaScript 功力就能更上一层楼。希望今天的讲解对你有所帮助!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注