本文深入解析 ES6 新增的 new.target 属性,展示如何利用它检测构造函数调用方式。通过示例演示如何模拟抽象类,防止直接实例化,并进一步利用构造期间对派生类原型的访问,在实例化阶段强制子类重写特定方法,从而将潜在的运行期错误提前至构造期,提升代码健壮性。
起源与问题背景
起源
近期在开发中尝试使用 Symbol 作为唯一标识值,发现 Symbol 无法通过 new 运算符进行实例化,仅能作为普通函数调用。若强行使用 new 操作,程序会抛出类型错误。
new Symbol()
// error
Uncaught TypeError: Symbol is not a constructor
at new Symbol (<anonymous>)
at <anonymous>:1:1
暂且不考虑底层引擎的具体实现,仅从代码层面思考:能否实现一个函数,允许正常调用但禁止使用 new 操作?经过思考,得出如下实现方案:
function disConstructor() {
if (this !== window) {
throw new TypeError(' disConstructor is not a constructor')
}
console.log('gogo go')
}
// 测试结果如下
disConstructor() // gogo go
new disConstructor()
// error
Uncaught TypeError: disConstructor is not a constructor
at new disConstructor (<anonymous>:3:15)
at <anonymous>:1:1
若运行环境为 Node.js,可将 window 替换为 global,代码执行结果保持一致。由于个人场景中未见明显应用价值,该方案未作进一步深入。然而,近期重新研读 ES6 规范时,发现了 new.target 这一属性。
new.target 属性详解
new.target 属性
介绍(引用 MDN 文档)
new.target 属性允许开发者检测函数或构造方法是否是通过 new 运算符被调用的。
当函数或构造方法通过 new 运算符初始化时,new.target 返回一个指向该构造方法或函数的引用。而在普通的函数调用中,new.target 的值为 undefined。
基于上述特性,我们可以对之前的代码进行如下重构:
function disConstructor() {
// 普通的函数调用中,new.target 的值是undefined。
if (new.target) {
throw new TypeError(' disConstructor is not a constructor')
}
console.log('gogo go')
}
该实现得到的运行结果与前述手动检测方案完全一致。
深入:模拟抽象类与强制重写
深入探讨
ES6 引入此功能是否仅用于检查函数调用方式?
在查阅相关资料时发现,大多数方案利用 new.target 来编写只能被继承的类,类似于实现 Java 中的抽象类。
class Animal {
constructor(name, age) {
if (new.target === Animal) {
throw new Error('Animal class can`t instantiate');
}
this.name = name
this.age = age
}
// 其他代码
...
}
class Dog extends Animal{
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
}
new Animal()
// error
Uncaught Error: Animal class can`t instantiate
at new Animal (<anonymous>:4:13)
at <anonymous>:1:1
new Dog('mimi', 12, '公')
// Dog {name: "mimi", age: 12, sex: "公"}
然而,Java 抽象类要求必须重写抽象方法,而 JS 中此前并无直接方案。在测试与使用过程中,意外发现超类在构造期间可以访问派生类的原型,利用这一特性可以解决上述问题。
class Animal {
constructor(name, age) {
console.log(new.target.prototype)
}
// 其他代码
...
}
此前在运行时调用未重写方法时的报错逻辑如下:
class Animal {
constructor(name, age) {
this.name = name
this.age = age
}
getName () {
throw new Error('please overwrite getName method')
}
}
class Dog extends Animal{
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
}
const pite = new Dog('pite', 2, '公')
a.getName()
// error
Uncaught Error: please overwrite getName method
at Dog.getName (<anonymous>:8:11)
at <anonymous>:1:3
此时利用 new.target,我可以在构造期间对子类进行操作并触发报错。
class Animal {
constructor(name, age) {
// 如果 target 不是 基类 且 没有 getName 报错
if (new.target !== Animal && !new.target.prototype.hasOwnProperty('getName')) {
throw new Error('please overwrite getName method')
}
this.name = name
this.age = age
}
}
class Dog extends Animal{
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
}
const pite = new Dog('pite', 2, '公')
// error
Uncaught Error: please overwrite getName method
at new Animal (<anonymous>:5:13)
at new Dog (<anonymous>:14:5)
at <anonymous>:1:1
这将原本在运行方法时发生的错误提前到了构造时期。虽然错误最终仍在运行期暴露,但该触发机制更早,危害更大,反而对代码构成了一种保护机制。
当然,利用超类在构造期间访问派生类原型的特性远不止于此,其功能强大,可结合具体业务场景深入探讨其理解与实际作用。
其他替代方案
其他替代方案
- 增加编辑器插件
- Proxy 代理
- 修饰器(Decorators)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。