JS `new.target`:在构造函数中检测是否通过 `new` 调用

各位观众,晚上好!欢迎来到今天的JavaScript奇妙夜,我是你们的老朋友,BUG终结者!今天我们要聊点刺激的——new.target,一个能让你在构造函数里像柯南一样,一眼识破“凶手”是否用了new关键词的秘密武器。准备好了吗?咱们开始!

第一幕:构造函数的困境

在JavaScript的世界里,构造函数扮演着创造对象的关键角色。但是,它们有个小小的烦恼:它们可以被当成普通函数调用,而这往往不是我们希望的。

function Person(name) {
  this.name = name;
  console.log("Hi, I'm " + this.name);
}

Person("Alice"); // 哎呀!全局对象被污染了!(严格模式下会报错)

let bob = new Person("Bob"); // 这才是正道!

看出问题了吗?当我们直接调用Person("Alice")时,this指向了全局对象(在浏览器里是window),导致全局变量被意外修改。这简直就是一场灾难!

那么问题来了:我们怎么在构造函数内部判断,它到底是被new调用的,还是被当成普通函数调用的呢?

第二幕:new.target闪亮登场!

new.target就是解决这个问题的神器!它是一个ES6引入的元属性,只能在构造函数或class构造器中使用。它的作用是:

  • 如果构造函数是通过new调用的,new.target会指向构造函数本身。
  • 如果构造函数是作为普通函数调用的,new.target的值是undefined

有了它,我们就可以像福尔摩斯一样,轻松判断调用方式了!

function Animal(name) {
  if (new.target === undefined) {
    throw new Error("必须使用 'new' 关键字调用!");
  }
  this.name = name;
  console.log("A new animal named " + this.name + " is created!");
}

try {
  Animal("Charlie"); // 抛出错误:必须使用 'new' 关键字调用!
} catch (error) {
  console.error(error.message);
}

let dog = new Animal("Buddy"); // 正常运行

这个例子中,我们通过new.target检查了调用方式。如果不是new调用,就抛出一个错误,防止全局对象被污染。

第三幕:new.target的进阶用法

new.target的功能远不止于此。它还可以用来:

  • 实现抽象基类

抽象基类是一种不能直接实例化的类,只能被继承。我们可以利用new.target来阻止抽象基类的实例化。

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

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

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

  getArea() {
    return Math.PI * this.radius * this.radius;
  }
}

// let shape = new Shape(); // 抛出错误:Shape 是一个抽象类,不能直接实例化!
let circle = new Circle(5);
console.log("Circle area:", circle.getArea()); // Circle area: 78.53981633974483

这里,Shape类是一个抽象基类。我们在它的构造函数中检查new.target是否指向Shape本身。如果是,说明有人试图直接实例化Shape,我们就抛出一个错误。

  • 区分父类和子类的构造函数

在继承关系中,我们可能需要在父类和子类的构造函数中执行不同的逻辑。new.target可以帮助我们区分当前正在执行的是哪个类的构造函数。

class Base {
  constructor() {
    if (new.target === Base) {
      console.log("Base constructor called directly.");
    } else {
      console.log("Base constructor called from a derived class.");
    }
  }
}

class Derived extends Base {
  constructor() {
    super();
    console.log("Derived constructor called.");
  }
}

let base = new Base(); // Base constructor called directly.
let derived = new Derived();
// Base constructor called from a derived class.
// Derived constructor called.

在这个例子中,当直接实例化Base时,new.target指向Base,所以输出"Base constructor called directly."。当实例化Derived时,super()会调用Base的构造函数,此时new.target指向Derived,所以输出"Base constructor called from a derived class."。

第四幕:new.target与类(Class) 的关系

ES6 引入了 class 关键字,它本质上是 JavaScript 基于原型继承的语法糖。new.target 在类构造器中的行为与在普通构造函数中类似。

class MyClass {
  constructor() {
    if (new.target) {
      console.log("MyClass constructor called with new.");
      console.log("new.target is: ", new.target);
    } else {
      console.log("MyClass constructor called without new (probably an error).");
    }
  }
}

const instance = new MyClass();  // MyClass constructor called with new.
// new.target is:  class MyClass { constructor() { ... } }

// 尝试直接调用类构造器 (严格模式下会报错)
try {
  MyClass();  // TypeError: Class constructor MyClass cannot be invoked without 'new'
} catch (e) {
  console.log(e);
}

需要注意的是,在严格模式下,直接调用类构造器会抛出一个 TypeError,因为类构造器必须通过 new 关键字调用。即使没有使用 new.target 进行显式检查,JavaScript 引擎也会强制执行这一规则。

第五幕:new.target的实际应用场景

  • 防止构造函数被错误调用: 这是 new.target 最常见的用途,确保构造函数只能通过 new 关键字调用,避免污染全局作用域或产生意外行为。

  • 实现工厂模式: new.target 可以用于在构造函数内部创建不同类型的对象,具体取决于调用方式或传入的参数。

  • 实现单例模式: 通过 new.target 确保只有一个实例被创建。

  • 控制继承行为: 如前文所述,可以基于 new.target 实现抽象基类,或者在父类构造函数中针对子类进行特殊处理。

第六幕:兼容性与注意事项

new.target 是 ES6 引入的特性,因此在一些老旧的浏览器中可能不支持。在使用时,需要考虑兼容性问题,可以使用 Babel 等工具进行转译。

此外,new.target 只能在构造函数或类构造器中使用,如果在其他地方使用,会抛出一个 SyntaxError

第七幕:代码示例大放送!

为了让大家更深入地理解 new.target,这里再提供几个示例:

示例 1:单例模式

let Singleton = (function() {
  let instance;

  function createInstance() {
    return {
      data: "Singleton Data"
    };
  }

  return function() {
    if (new.target !== Singleton) {
      throw new Error("Singleton must be instantiated with 'new'");
    }
    if (!instance) {
      instance = createInstance();
    }
    return instance;
  };
})();

let instance1 = new Singleton();
let instance2 = new Singleton();

console.log(instance1 === instance2); // true  (指向同一个对象)

// 尝试直接调用 (将会抛出错误,因为加了new.target判断)
// Singleton(); // Error: Singleton must be instantiated with 'new'

示例 2:工厂模式

function ShapeFactory(type) {
  if (new.target !== ShapeFactory) {
    return new ShapeFactory(type); // 允许不使用new调用
  }

  switch (type) {
    case 'circle':
      return new Circle();
    case 'square':
      return new Square();
    default:
      return null;
  }
}

function Circle() {
  this.type = 'circle';
  this.draw = function() { console.log("Drawing a circle"); }
}
function Square() {
  this.type = 'square';
  this.draw = function() { console.log("Drawing a square"); }
}

let circle = ShapeFactory('circle');
circle.draw(); // Drawing a circle

let square = new ShapeFactory('square');
square.draw(); // Drawing a square

第八幕:总结与展望

new.target 是一个强大的工具,它可以帮助我们更好地控制构造函数的行为,编写更健壮、更可维护的代码。掌握 new.target,你就可以像一位经验丰富的侦探,轻松识破 JavaScript 代码中的各种“陷阱”,成为真正的 JavaScript 大师!

总而言之,new.target 就像一把瑞士军刀,在构造函数中,提供了判断调用方式、实现抽象类、区分父子类等多种功能。合理运用 new.target,能够写出更优雅、更安全、更易于维护的 JavaScript 代码。

今天的课程就到这里,感谢大家的观看!希望大家以后在写JavaScript代码的时候,能够想起今晚的new.target,用它来保护你的代码,让BUG无处遁形! 咱们下期再见! 祝大家编码愉快,永不秃头!

发表回复

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