
前端面试十有八九会问到你new到底做了什么但今天聊的这个问题比背八股更刁钻作为一种可以既当普通函数又被new调用的语言JavaScript 里怎么判断某个函数“这一次”到底是普通调用还是构造调用这听起来像是一个只在面试题里出现的冷门考点但只要你在真实的项目里写过组件库、工具函数或者封装过 SDK早晚会撞上它。先说一个我自己的经历有一次我维护公司内部的一个轮播组件历史代码里有一行new Slider(.banner)我以为是多余的反手删成了Slider(.banner)结果整个轮播失效。排查了半天才发现这个组件内部通过this挂了一堆状态普通调用的时候this指向全局对象所有状态全部串到了 window 上。从那以后我就明白了识别“有没有被 new”不是炫技是保命。这篇文章我会从原理到实战把下面几条主线讲透new调用和普通调用到底差在哪里、为什么需要区分、以及怎么通过new.target、instanceof、严格模式等方案编程式地判断。最后还会附上我踩过的坑和面试高频题速查表适合所有写 JavaScript 的前端同学尤其是想搞懂底层机制的初中级工程师和准备面试的朋友。1. 先说结论怎么判断函数“被 new 了”1.1 一条代码就能验证的判断方法如果你只需要一个答案记住这个在现代 JavaScriptES6 之后里判断当前函数是否被new调用的标准方式是new.target。function IsItNew() { if (new.target) { console.log(我是构造调用我认识 new.target); } else { console.log(我是普通调用new.target 是 undefined); } } IsItNew(); // 普通调用 new IsItNew(); // 构造调用new.target这个语法看起来像属性访问实际上是 JS 引擎在函数内部维护的一个“元数据”引用。普通调用时它是undefined被new调用时它指向“跟在 new 后面的那个构造函数”。注意这里有个细节如果在继承链上new.target指向的是最外层的那个构造函数也就是你实际写new Child()时的Child而不是内部的super调用目标。class Parent { who() { console.log(new.target , new.target.name); } } class Child extends Parent {} new Child().who(); // new.target Child这段代码的输出可能颠覆一部分人的直觉who()虽然定义在Parent上但new.target仍然指向Child。原因很简单——new.target记录的是“最初的 new 动作”发生在谁身上它不会因为 this 的绑定而改变。这个特性在框架源码里非常常见比如写基类时用来判断到底是哪个子类被实例化了。1.2 普通调用和构造调用差在哪两个结果表面上都是“执行了函数体”但背后做了完全不同的事。普通调用只是一个很直接的函数执行创建执行上下文、绑定this默认指向全局对象、严格模式下是undefined、执行代码。构造调用则要复杂得多它相当于在调用函数体之前先由引擎替你完成了一系列“对象孵化”动作然后把这套东西以一个改头换面的this呈现在函数面前。普通调用函数体执行 - this 指向全局/undefined - 返回值随你 构造调用创建新对象 - 连接原型 - this 指向新对象 - 函数体执行 - 特殊返回规则换句话说构造调用自带“对象创建 原型继承”的副作用。同样是function Foo() { this.name foo; }普通调用会把name塞到全局对象上严格模式直接报错而new Foo()则把这套属性稳稳地挂在一个新的实例上。这就是区分两者的现实意义——很多混淆和 bug 都来源于“这个函数到底该被 new还是不该被 new”。2. new 背后的四步操作2.1 手写一个 new看懂引擎干了什么要识别构造调用光知道new.target还不够你得理解new这个操作符到底动了什么手脚。ECMAScript 规范里new的执行过程可以浓缩成四步我建议每个前端都亲手实现一遍function myNew(Constructor, ...args) { // 第一步创建一个新的空对象 // 第二步把这个对象的原型链指向 Constructor.prototype const obj Object.create(Constructor.prototype); // 第三步以 obj 为 this执行构造函数 const result Constructor.apply(obj, args); // 第四步看返回值对象/函数优先原始值忽略 const isObject result ! null typeof result object; const isFunction typeof result function; return isObject || isFunction ? result : obj; }这段代码的每一行都有讲究。Object.create(Constructor.prototype)一步完成了“创建空对象”和“设置原型”两件事相当于{}加上Object.setPrototypeOf(obj, Constructor.prototype)。Constructor.apply(obj, args)则把构造函数内部的this绑定到这个新对象上让this.xxx xxx的赋值能落在实例上。这里最容易被忽略的是最后一步的返回值判定。很多初学者以为构造函数 return 什么就返回什么实际上规范里有个“对象优先”的原则只有显式返回一个对象或函数时这个返回值才会覆盖引擎创建的对象如果返回的是原始值比如return 1或return abc这个返回值会被静默丢弃仍然返回引擎创建的那个新对象。2.2 返回值规则很多人栽在这里这个规则看起来简单实际写起来特别容易踩坑直接上代码演示function ReturnObject() { this.fromThis 挂在 this 上; return { fromReturn: 来自显式 return }; } const a new ReturnObject(); console.log(a.fromThis); // undefined console.log(a.fromReturn); // 来自显式 return function ReturnPrimitive() { this.name 实例上的属性; return 42; } const b new ReturnPrimitive(); console.log(b.name); // 实例上的属性 console.log(b); // { name: 实例上的属性 }第一个函数里显式返回的对象完全“劫持”了构造过程this上挂的属性全部作废第二个函数里return 42白写了原始值被忽略返回的还是实例对象。这也解释了为什么有些new出来的东西看起来奇奇怪怪——八成是构造函数里藏着一个对象类型的 return。注意new的结果只认两种东西。一种是 object 类型含数组、Map、Set 等所有引用类型一种是 function 类型函数本身也是对象除此之外的 primitive 全部被无视。这个认知在调试的时候能帮你省下大把时间。理解了这四步你就明白为什么说“普通调用和构造调用是两种不同的执行模式”了。接下来从程序员视角给你三种可落地的判别方案。3. 三种判别方案从推荐到备选3.1 首选方案new.target正如开头所说new.target是现代 JavaScript 判断构造调用最干净、最不会误伤的方式。它的优点有三个一是语义明确看到new.target就知道这段代码在处理“构造调用”问题二是不受调用方式影响无论你是用.call()、.apply()还是Function.prototype.bind()去调用只要不是真正的new它就保持undefined三是天然支持 class因为 class 内部永远存在new.target。实际里最常见的用法是做一个“免 new 也能用”的兼容函数也就是所谓的双模式工厂function Slider(selector) { if (!new.target) { return new Slider(selector); } this.selector selector; this.init(); } Slider.prototype.init function () { console.log(初始化轮播${this.selector}); }; // 两种写法效果完全等价 const s1 new Slider(.banner); const s2 Slider(.banner); console.log(s1 instanceof Slider, s2 instanceof Slider); // true truenew.target在这里起到了一个“补 new”的作用用户忘了写new函数内部帮你补上对外行为完全一致。这也是 jQuery 当年$()不带new也能用的底层原理之一。不过要注意这种写法必须在函数体最前面做判断因为一旦先执行了别的逻辑this已经被绑定成全局对象了再想补救就要先把误操作清掉很麻烦。3.2 经典方案this instanceof 检查在 ES6 之前前端们用的是另一套经典写法判断this是不是当前函数的一个实例。function LegacySlider(selector) { if (!(this instanceof LegacySlider)) { return new LegacySlider(selector); } this.selector selector; } LegacySlider(.banner); // 内部自动补 new new LegacySlider(.banner); // 正常构造调用原理很直接构造调用时引擎创建的新对象会以Constructor.prototype为原型所以this instanceof Constructor一定为真普通调用时this指向全局对象或者严格模式下的undefined无论如何都不可能instanceof这个函数于是判断为“没被 new”走return new分支。但它有一个著名的漏洞如果你用.call()或.apply()手动手动把this绑定到一个“长得像实例”的对象上这个检查就会误判。const fake Object.create(LegacySlider.prototype); LegacySlider.call(fake); // this instanceof LegacySlider 为 true这种代码看着很别扭但真实项目里有人为了复用逻辑确实会干出构造函数.call(某个对象)这种操作。结果就是fake被当成了一次“构造调用”来执行函数内部直接跳过了补new的逻辑。相比之下new.target就不会有这个问题——.call()永远改变不了new.target的值因为它根本不受 this 绑定影响。3.3 严格模式下的 this 判断还有一个更冷门的方案利用严格模式下普通调用的this undefined来识别function StrictSlider(selector) { use strict; if (this undefined) { return new StrictSlider(selector); } this.selector selector; }构造调用时this是一个实例对象非严格模式普通调用时this是全局对象严格模式普通调用时this是undefined。所以这个判断在严格模式场景下是准确的。但我不推荐你优先用它原因是它把判断逻辑和“严格模式”这个额外的状态绑定了一旦有人把use strict去掉或者用.call({})显式传了个对象判断就失真了。它更适合作为你理解this机制的一个思维练习题而不是生产代码里的主力方案。我的个人建议排序生产代码优先new.target兼容老语法ES5时用this instanceof Constructorthis undefined只用于学习理解别拿来当主力。4. 哪些函数天生就不能被 new4.1 箭头函数没有 prototype也没有自己的 this判断“能不能被 new”有一个更前置的问题不是所有函数都配被new。第一类典型就是箭头函数。箭头函数没有自己的prototype属性也没有自己的this和arguments而new机制的核心就是“创建以 prototype 为原型的对象 重新绑定 this”这两个前提箭头函数都不满足。const Foo () { console.log(箭头函数); }; // 直接报错TypeError: Foo is not a constructor const f new Foo();这里有个特别容易在团队代码 review 里被抓出来的场景有人把构造函数误写成了箭头函数比如const User (name) { this.name name; }然后new User()直接抛错。因为箭头函数里的this是词法绑定的继承外层作用域就算你能想办法执行this也不会指向新实例。顺带说一下箭头函数里的new.target会怎样它会“继承”外层函数的new.target。这是一个经常被忽略的行为function Outer() { const inner () { console.log(箭头函数里的 new.target , new.target); }; inner(); } new Outer(); // 箭头函数里的 new.target Outer Outer(); // 箭头函数里的 new.target undefined想要一眼识破箭头函数不能当构造器看两点就够了一是函数定义用的是二是它没有prototype属性普通的function声明天生带一个可用的prototype箭头函数上是undefined。4.2 方法简写也不支持构造调用第二类不能new的是“方法简写”包括对象字面量里的方法和 class 里的方法const obj { method() { return 我简写了; }, }; // TypeError: obj.method is not a constructor new obj.method(); class Counter { increment() { return this.count; } } // TypeError: (intermediate value).increment is not a constructor new (new Counter()).increment;方法简写和普通函数长得几乎一样但它没有prototype所以在new机制眼里它同样不是合法的构造函数。这里有一个实用的 review 技巧想快速判断一个函数能不能被new直接看它有没有prototype属性有就是候选构造函数没有就基本宣判“不可 new”。这个规则覆盖了箭头函数、方法简写和 bound 函数。不过 bound 函数是个例外它本身没有prototype但new一个 bound 出来的函数是合法的会创建原函数的实例function Animal(name) { this.name name; } const BoundAnimal Animal.bind(null); const pig new BoundAnimal(猪); console.log(pig.name); // 猪 console.log(pig instanceof Animal); // true这个行为是规范里的特殊情况new遇到 bound 函数时会直接使用“被绑定的原函数”来完成构造绑定参数照样生效但this绑定会被忽略。也就是说bind对this的锁定在new面前是失效的——新对象照样被创建原型照样指向原函数。4.3 class 的特殊规则class 的情况和上面的又不太一样class 语法糖本质上还是 function但它强制了“必须new才能调用”。直接调用一个 class 会抛TypeErrorclass StaticCounter { constructor() { this.count 0; } } // TypeError: Class constructor StaticCounter cannot be invoked without new StaticCounter();这是一个比普通函数严格得多的约束。普通函数你还能通过new.target做双模式兼容class 天生不允许你这么做——它不允许“不带 new 调用”这种形态存在。另外注意 class 声明不会提升存在暂时性死区所以你不能在定义之前实例化它这点和普通function声明的提升行为完全不同。顺便记一个冷知识class 内部的new.target永远是“有值”的。即使在constructor里判断new.target也没有意义因为它不可能为undefined。这也是为什么 class 里不需要也不允许用new.target来做普通调用兜底——语法层面已经把这条路封死了。5. 真实业务场景为什么非要区分构造调用5.1 双模式函数像 jQuery 一样免 new前面已经提了好几次双模式函数这里展开讲讲它的设计动机。假设你在写一个组件库对外 API 是Slider(selector)你希望用户写起来轻一点不用记住new但同时你又希望内部能拿到一个实例对象来挂载状态和方法。这时候双模式就是最佳解法入门用户不写new也能用进阶用户写new也没问题两条路径殊途同归。function Toast(options) { if (!(this instanceof Toast)) { return new Toast(options); } this.options options || {}; this.render(); } Toast.prototype.render function () { console.log(渲染提示配置项, this.options); };这个模式在 jQuery、Lodash 的历史代码、以及很多老牌工具库里都能看到影子。它的好处是 API 友好缺点是“机器可读性”差一点——调用方不写new就很难一眼看出它是构造调用。这时候如果你做代码审查识别要点就要转移到函数命名和内部写法上函数名首字母大写PascalCase是行业约定函数体内密集出现this.xxx xxx通常就是构造函数的特征。5.2 工厂函数与单例兼顾“只有一份”的需求另一个死活需要判断构造调用的场景是单例模式。有时候我们希望一个类无论被new多少次都返回同一个实例。这个需求如果不区分“是否被 new”写起来会非常别扭。let cache null; function ApiClient(config) { if (!new.target) { return new ApiClient(config); } if (cache) { return cache; } cache this; this.config config; this.init(); } const c1 new ApiClient({ base: /api }); const c2 ApiClient({ base: /api }); console.log(c1 c2); // true同一个实例注意这里cache是一个模块级变量函数被调用多次也不会被重置。new.target在这里扮演了两个角色一是普通调用时补new二是配合if (cache) return cache实现“无论怎样我只有一个实例”。如果你用 class 写同样的功能反而麻烦——class 的构造函数不能直接返回自定义对象来控制实例化结果。这算是一个“函数式构造函数”相对 class 的独特优势。5.3 防御式写法不该 new 的new 了就给你点颜色看看还有一种方向相反的需求有些函数设计上就是纯粹的工具函数使用者却因为误解写上了new结果this上的属性污染了全局。为了避免这种事故可以在函数内部做一个防御性检查发现被new了就直接抛错或降级。function formatDate(date) { // 纯工具函数不该被 new if (new.target) { throw new TypeError(formatDate 是工具函数请直接调用); } return ${date.getFullYear()}-${date.getMonth() 1}-${date.getDate()}; } formatDate(new Date()); // 2025-01-15举例 new formatDate(new Date()); // 抛错你可能会问formatDate被new调用会发生什么由于函数体不往this上挂任何东西返回值又是一个字符串原始值所以new formatDate()实际会返回一个空对象结果完全不符合预期。防御式写法的意义就在于把这种“静默错误”升级成“明显报错”让调用方第一时间发现自己的问题。5.4 框架和类库源码里的常见套路如果你读过 Vue、React 或者一些核心库的源码会发现new.target的另一个高频用途在基类里校验“抽象约束”。比如某个抽象基类被声明后子类继承时会用new.target来确认实例化的是哪个具体类从而走对应分支。这种写法在写跨端 SDK 时尤其常见。class BaseAdapter { constructor(input) { if (new.target BaseAdapter) { throw new TypeError(BaseAdapter 是抽象类不能直接实例化); } this.input input; } } class WeChatAdapter extends BaseAdapter {} // 抛错 new BaseAdapter({ type: wechat }); // 正常 new WeChatAdapter({ type: wechat });这个模式叫“抽象类模拟”。在纯 JavaScript 里没有abstract关键字但通过new.target BaseAdapter就能阻止别人直接实例化基类。这个写法比instanceof更精确因为new.target会指向实际的调用目标而不是被继承链上的中间类误导。6. 面试高频题速查与避坑实录6.1 一眼识破代码 review 时的快速判断清单把前面的内容总结成一个可直接套用的判断清单你在 review 代码或者排查问题时照单抓药就行你要判断的事情看哪里结论某次调用是否走了 new调用点有没有new关键字一目了然函数内部是否做了构造兼容函数体开头有没有new.target或instanceof检查有就是双模式函数这个函数能不能被 new有没有prototype属性没有就基本不能普通 function 还是箭头函数看定义语法function还是箭头函数不能当构造器class 能否不加 new 调用看是不是 class 声明class 必须加 new新对象到底是不是预期实例看返回值类型对象优先、原始值忽略这张表基本涵盖了“一眼识破”的所有入口。实际操作里我最依赖的是第二和第三行——绝大多数 bug 都出在“写了 new 但函数根本没有 prototype”或者“函数内部偷偷兼容了双模式调用方没意识到”。6.2 我踩过的几个坑第一个坑是instanceof误判。我早期写过一个公共函数内部用this instanceof Foo做双模式兼容结果某天线上出现一个诡异问题一个第三方库用Foo.call(existingObj)的方式复用了我的函数逻辑existingObj恰好是通过Object.create(Foo.prototype)伪造的于是判断失效走错了分支。打那之后我所有新代码一律用new.target兼容老环境的项目才用 instanceof。第二个坑是箭头函数里的new.target继承问题。有一次我在一个 class 的方法里写了内部工具函数习惯性用了箭头函数然后在内部判断new.target想确认某个分支结果它继承的是 class 的new.target值永远存在导致我的判断逻辑完全失效。排查了很久才发现是“箭头函数没有自己的 new.target”这个特性。第三个坑和bind有关。你以为new (fn.bind(obj))()会绑定obj作为this实际不是——new会忽略 bound 的this照常创建新实例。很多人在写“返回一个绑定了 this 的构造函数”时踩过这个坑结果this根本没指向预期对象。第四个坑是 class 的调用方式。有人从普通函数切到 class 之后习惯性地少写了new浏览器直接抛TypeError。看报错信息别慌那是在提醒你“class 必须 new 调用”。这其实是个好设计它把一部分“构造调用判断”从运行时错误提升到了语法约束层面。6.3 面试高频题速查表这里把围绕这个主题的高频面试题整理成一个速查表方便临阵磨枪题目核心考察点一句话回答new 操作符做了什么构造调用原理创建新对象、连原型、绑定 this、处理返回值手写一个 new 实现综合能力用 Object.create apply 按四步实现为什么箭头函数不能被 newthis 与 prototype没有自己的 this 和 prototype构造函数 return 一个对象会怎样返回值规则对象覆盖实例原始值被忽略如何判断函数是否被 new 调用new.target 的掌握看 new.target 是否为 undefinedclass 能不能不加 new 调用class 的约束不能会抛 TypeErrorbind 出来的函数能被 new 吗bound 函数特例可以new 会忽略 this 绑定构造函数里 this instanceof 写法有啥缺点经典方案缺陷.call() 传原型链对象会误判6.4 一个练习5 分钟写出一个万能“免 new”工具函数最后来个能立刻上手的练习。想验证自己是否真的理解了可以尝试实现一个factory(Constructor)高阶函数它接收任意构造函数返回一个新的函数这个新函数无论怎么调用带不带 new都返回正确实例。function factory(Constructor) { return function (...args) { if (!new.target) { // 不带 new 调用补一个 new return new Constructor(...args); } // 带 new 调用直接交给原构造函数 return new Constructor(...args); }; } function Point(x, y) { this.x x; this.y y; } const createPoint factory(Point); const p1 createPoint(1, 2); // 普通调用自动补 new const p2 new createPoint(3, 4); // 外来 new也不出岔子 console.log(p1 instanceof Point); // true console.log(p2 instanceof Point); // true这个小函数本身没什么商业价值但它把new.target的行为边界全部串起来了——普通调用、构造调用、参数透传、原型保持。你能把这个函数解释清楚说明new相关的知识点已经过关。我在实际工作里还有个习惯凡是对外暴露的构造函数一律在 JSDoc 或者 TypeScript 声明里写清楚“必须用 new 调用”或者“支持双模式调用”。这不是文档洁癖而是因为 JS 的函数形态太自由了同一个函数可以有两种截然不同的调用方式如果不对调用方做明确约定早晚会有人写出和我当年一样“帮忙删 new”的代码。工具函数和构造函数的边界很多时候就差一个new.target的判断而代码的可维护性往往就看这些不起眼的细节能不能被一眼识破。