1. 开篇一个让我加班到深夜的this错误大概两年前我被一个线上的 bug 折腾得不轻。场景很朴素页面初始化时用构造函数创建了一个弹窗实例结果弹窗在点击按钮后死活不出现控制台倒是报了一行 TypeError——this.show is not a function。我盯着那行报错看了半天又翻了一遍调用链最终定位到了new构造函数里一个不起眼的回调函数。那一刻我才真正意识到new 构造函数里的 this 绑定远不是this 指向新对象这么一句话能糊弄过去的。后来我在带新人和做代码审查的时候发现这个知识点几乎是踩坑重灾区。很多人能背出new的四个步骤能说出this 指向实例的结论但一旦遇到构造函数内部的事件回调、定时器、或者把方法赋值给变量再调用就开始翻车。这也不怪大家因为 this 的绑定规则本来就不是单一维度的new只是其中最特殊、也最容易让人产生错觉的一种。这篇文章我想把new构造函数的 this 绑定这件事彻底聊透。不光是讲原理更重要的是把我在实际项目里遇到的坑、排查的思路、以及怎么从原理层面去预判 this 的走向全部摊开来说。如果你是刚接触 JavaScript 不久的新手这篇文章能帮你把 this 的底层逻辑理顺如果你已经写了不少业务代码那么排查思路和面试题的拆解部分可能会让你有原来如此的感觉。2. 为什么只有 new 能让 this 安心new 操作符到底做了哪四件事要理解 this 绑定得先搞清楚new在那一刻到底做了什么。很多人知道new会创建一个新对象但并不知道这个新对象和this之间是怎样被绑定起来的。我把这个过程拆得非常慢目的是为了让你在脑海中建立一条完整的因果链。2.1 new 的四个动作缺一不可当你在代码里写下const obj new Foo()时JavaScript 引擎在幕后帮你干了四件事创建一个全新的空对象{}将这个空对象的原型__proto__指向Foo.prototype将Foo函数体内的this绑定到这个空对象上并以这个对象为上下文执行Foo的代码判断Foo的返回值如果Foo返回了一个对象包括函数、数组等引用类型那么new表达式的结果就是那个对象否则返回第一步创建的这个新对象。我见过很多资料把第 4 步写成返回新对象漏掉了如果构造函数自己返回了对象这个分支。而这个分支恰恰是后面我们要讲的构造函数返回值大坑的根源。第 3 步就是 this 绑定的核心。new是 JavaScript 里少数几个能强制指定 this 指向的操作之一它跟call、apply、bind一样都有能力覆盖普通的 this 推导规则。换句话说只要用了new构造函数体内部的this就必然指向新对象——除非函数内部又因为某些机制把这个 this 弄丢了。2.2 一个被 this 丢失 坑到的定时器案例光知道步骤还不够因为构造函数体内的代码往往不是顺序执行到底的。最常见的问题出现在回调里。举个例子function Counter() { this.count 0; setInterval(function () { this.count; console.log(this.count); }, 1000); } const counter new Counter();这段代码你会以为每隔一秒 count 加一然后依次打印 1、2、3…… 但实际上打印的全是NaN。原因很简单setInterval的回调函数执行时它是被当作普通函数调用的这个函数里的this指向的是全局对象浏览器里是window而window.count根本不存在this.count等价于给一个 undefined 做自增结果当然是NaN。这里的核心逻辑是new只保证构造函数体内部的 this 指向新对象并不保证构造函数体内部嵌套定义的所有函数里的 this 也都指向这个新对象。那些内部函数里的 this要遵循它们各自被调用时的规则。定时器、DOM 事件回调、Promise 回调这一类延迟调用的场景是 this 丢失的重灾区。建议你在构造函数内部优先使用箭头函数来定义回调因为箭头函数没有自己的 this它会继承外层作用域的 this——也就是构造函数刚被new绑定的那个新对象。这是解决此类问题最直接的手段。3. 构造函数返回值一个能悄悄坑掉老手的细节如果你只在业务代码里见过constructor里写属性、写方法可能从来没注意过构造函数的返回值。但只要你写过封装组件、写过插件、或者做过对象池之类的应用就一定会撞上这个细节。3.1 返回值规则对象和基本类型的区别构造函数也是函数函数自然可以return东西。但new对返回值的处理是有明确规则的如果构造函数返回的是对象普通对象、数组、函数、正则等一切引用类型new表达式的结果就是这个返回的对象第一步创建的那个新对象被直接丢弃如果构造函数返回的是基本类型字符串、数字、布尔、null、undefined、symbol、bigintnew表达式的结果还是新创建的那个对象返回值被忽略。看起来很简单但实际开发中有一个很常见的坑构造函数返回了一个null。typeof null object这个耳熟能详的语言 bug在这里就会发挥作用。function Foo() { this.name foo; return null; } const instance new Foo(); console.log(instance); // 输出 Foo { name: foo }你看返回null会被当作基本类型处理新对象照样返回。但如果你返回的是{}那么instance就不是Foo的实例了function Foo() { this.name foo; return {}; } const instance new Foo(); console.log(instance instanceof Foo); // false console.log(instance.name); // undefined这种写法在业务里几乎没有人会主动用但如果你写的构造函数是给别人调用的而调用方有包一层再返回的习惯就会踩到这个雷。3.2 一个让我印象深刻的 return this 写法很多人可能不知道老一代的 jQuery 风格插件很喜欢在构造函数里写return this。为什么因为this是一个对象所以满足返回对象的分支。但这样写其实是多此一举——new默认就会返回这个 this你写不写都一样。真正有意义的情况是构造函数里根据某种条件返回另一个预先创建好的对象。比如实现一种最简单的单例let sharedInstance; function User(name) { if (sharedInstance) { return sharedInstance; } this.name name; sharedInstance this; } const a new User(张三); const b new User(李四); console.log(a b); // true这里new的结果因为构造函数返回了已有的对象a和b就指向了同一个实例。这种写法在实现全局状态管理和共享资源时挺实用但注意它只对对象返回值生效。如果你写的是return sharedInstance而此刻sharedInstance还没初始化是undefined那你得到的还是新对象而不是undefined——这个细节容易让人误解建议你在代码里加注释说明。3.3 从返回值看 实例 的真正含义这个细节还牵出一个问题instanceof到底在判断什么它判断的是对象的原型链上有没有Foo.prototype。如果new返回的是另一个对象而那个对象的原型链上没有Foo.prototype那么就是false。这让我想起一个经典面试场景构造函数返回了一个数组然后面试官问instance instanceof 构造函数是什么结果。很多人第一反应是返回什么就是什么所以是 false但真正成熟的理解是instanceof只看原型链而new的返回值决定了最终对象是谁两者结合才能推出结论。在写构造函数时我个人的习惯是永远不主动 return 对象。除非你有非常明确的需求比如对象池、单例否则 return 对象会让代码变得不可预测别人阅读你代码时也容易困惑。new本身就是一种自动返回实例的机制没必要再画蛇添足。4. this 绑定优先级的 权力游戏new、call/apply、bind 谁说了算this 绑定之所以让很多人头疼是因为它不只是一种规则而是一套规则。专家们都喜欢把这套规则整理成优先级表我这里结合new来梳理一遍。4.1 四条规则的完整排序JavaScript 的 this 绑定有四大规则按照优先级从高到低依次是new 绑定用new调用函数this 指向新创建的对象显式绑定用call、apply、bind调用函数this 指向你指定的对象隐式绑定作为对象的方法调用比如obj.method()this 指向obj默认绑定普通函数调用非严格模式下 this 指向全局对象严格模式下 this 是undefined。很多人以为call是最强的毕竟你都能显式指定了。但实际测试会发现new的优先级高于callfunction Foo() { this.name foo; } const obj { name: obj }; const instance new (Foo.bind(obj))(); console.log(instance.name); // 输出 foo而不是 obj这里我用了Foo.bind(obj)再new。按照直觉bind已经把 this 固定为obj了为什么new之后 this 又回到了新对象上因为new在绑定 this 时根本不管函数原来的 this 指向是什么——它直接创建新对象、把 this 绑上去、执行函数。bind产生的硬绑定在new面前会被无视。这是个很违反直觉的结论很多资深开发者都会在这个点上犹豫一下。4.2 为什么 new 能 无视 bind从底层实现角度理解我们可以尝试模拟一个简单的new实现来理解引擎的行为function myNew(Constructor, ...args) { const obj Object.create(Constructor.prototype); const result Constructor.apply(obj, args); return (result ! null typeof result object) ? result : obj; }注意第 3 行用的还是apply(obj, args)。因为obj是Object.create(Constructor.prototype)创建的它的原型链上必然有Constructor.prototype所以new出的对象一定是Constructor的实例。而Constructor.apply(obj, args)这个动作本质上跟调用Constructor.call(obj, ...args)是一样的——它执行的是函数体内的代码把 this 指到obj。但为什么bind过的函数再用newthis 还是新对象因为标准规定如果函数是通过bind产生的绑定函数当它被new调用时会直接忽略绑定的 this 值转而使用构造调用规则。换句话说new操作符对绑定函数有特殊的豁免条款。这个设计其实是刻意为之。因为bind最常用的场景是固定参数如果你需要固定构造函数的一部分参数又想继续用new那么这个豁免就能让你同时满足两个需求function Point(x, y) { this.x x; this.y y; } const PointAtOrigin Point.bind(null, 0, 0); const p new PointAtOrigin(10); // x 被固定为 0y 为 10 console.log(p); // Point { x: 0, y: 10 }this的初始值被忽略因为传了null但 x 参数照常被预设这是bind在 new 场景下最常见的用途。4.3 先 new 再 bind 会怎样 这种问题根本不用怕反过来的情况呢new Foo()之后如果拿实例的方法去bind这个 bind 会怎样这就跟new没关系了——因为new Foo()产生的是实例对象实例上的方法本质上就是一个普通函数。你把它 bind 给谁它的 this 就跟着谁走function Foo() { this.name foo; this.show function () { console.log(this.name); }; } const instance new Foo(); const fn instance.show.bind({ name: other }); fn(); // 输出 other这里的 this 绑定已经被 bind 接管跟new创建的那个对象没有关系了。很多人一看到对象的方法就以为是隐式绑定但实际上 bind 的优先级高于隐式绑定。帮你建立一个直觉只要看到new第一反应就应该是this 指向新对象看到call/apply/bind再考虑显式指到哪儿。二者同时出现时new胜出。5. 特殊选手的 this 行为箭头函数、class 语法糖与严格模式前面讲的是常规函数的前提下的规则。实际开发中我们还会遇到箭头函数、class 语法、strict 模式等特殊情况它们对new构造函数的 this 绑定有不同的影响。5.1 箭头函数不能 new但箭头函数能保 this箭头函数最核心的特征是它没有自己的 this它的 this 继承自外层作用域的词法环境。这意味着箭头函数不能用作构造函数new 箭头函数()会直接抛出 TypeError但构造函数内部的箭头函数能稳定地继承构造函数实例的 this。前面提到的Counter那个坑就可以用箭头函数修复function Counter() { this.count 0; setInterval(() { this.count; console.log(this.count); }, 1000); } const counter new Counter();这里的箭头函数定义在构造函数体内它的外层作用域是构造函数的上下文。构造函数执行时this 已经被new绑定到实例所以箭头函数捕获的 this 就是实例本身。这样即使回调延迟到 1 秒后执行这个 this 也不会丢。这个特性在实际开发里非常实用。我在写类组件、写事件绑定时几乎是无脑优先用箭头函数。因为它把 this 会不会丢 这件事从源头解决了不再需要小心翼翼地在闭包里存const self this了。但注意箭头函数的 this 继承是定义时决定的不是调用时决定。所以如果你在对象的方法里定义一个箭头函数然后把这个方法赋值给别人调用箭头函数的 this 依然指向定义它时所在的对象——因为对象方法本身作为作用域上下文并没有独立 this 的概念。这个点有点绕我举个例子const obj { name: obj, method: function () { const arrow () { console.log(this.name); }; arrow(); }, }; const fn obj.method; fn(); // 输出的是 undefined 还是 obj先留个悬念答案是undefined。因为method是普通函数它被赋值给fn后以普通函数方式调用this 指向全局对象非严格模式所以箭头函数捕获的 this 就是这个全局对象而不是 obj。这里你就能看出来箭头函数继承的是外层作用域的 this而外层作用域的 this 只能是它定义那一刻外层函数的真实 this。理解了这句话你就不会在箭头函数的 this 永远指向外层对象这种错误认知上打转了。5.2 class 构造函数的 this语法糖背后仍然是 newES6 的class语法本质上还是构造函数加原型链的语法糖但有几个细微差别。class里面的constructor方法在new调用时this 绑定逻辑与普通构造函数完全一致。但如果你在constructor里返回了对象同样会覆盖 new 出来的实例class Foo { constructor() { this.name foo; return { name: object }; } } const instance new Foo(); console.log(instance.name); // object console.log(instance instanceof Foo); // false这个行为跟普通构造函数一致所以上面 3.1 里的坑在 class 里同样存在。不过 class 有额外的限制constructor不能通过return返回一个undefined之外的基本类型否则同样会被忽略——原理一样规则相同。一个更微妙的点是 class 内部constructor中如果省略了return那么 this 就是新创建的实例这跟普通构造函数完全一致。所以class 是不是真的 class 语义这个问题从这个角度看答案是一样的底层还是那些规则。5.3 strict 模式与 new一种安全感的来源严格模式对 this 的默认绑定有影响普通函数调用时this 不再指向全局对象而是undefined。但new调用时不管是否严格模式this 都指向新对象。换句话说new的运行结果在严格模式下与非严格模式下是一致的。不过这里有一个容易被忽略的细节如果你在构造函数里定义了内部普通函数并作为回调使用这个回调在非严格模式下指向全局对象在严格模式下指向 undefined。这样从this 丢失的报错形态上会有差异——非严格模式下是静默改错对象比如我前文 Counter 的例子严格模式下是直接 TypeError。所以我一直建议你在项目中开启严格模式或者直接用 ES Module模块本身就是严格模式。这样一旦 this 意外丢失错误会直接暴露出来而不是悄悄污染全局排查的难度会低很多。6. 从面试题到线上事故这些知识的真实使用场景讲完原理我整理了一些在面试题和真实项目里反复出现的情景帮你把这些知识点从理解变成能动手排查。6.1 纸老虎题new 与普通函数调用的 this 差异来看一道经典的纸老虎题function User(name) { this.name name; this.showName function () { console.log(this.name); }; } const instance new User(小明); const showName instance.showName; showName();问最后输出什么答案是非严格模式下输出undefined因为 this 指向全局对象全局对象没有name属性严格模式直接报错。但很多人会直觉地以为输出小明。根源就在于实例方法被单独拿出来调用时隐式绑定就失效了。面试官喜欢拿这个题考你对 this 规则的理解因为只要你把showName instance.showName看成把函数本身取出来再把函数调用看成没有绑定对象答案就清楚了。我在代码审查里见过类似的问题某个同事写了个回调把实例方法当作参数传给了第三方库结果第三方库内部直接调用这个方法this 就飘了。修复方式要么是this.showName this.showName.bind(this)要么用箭头函数定义方法要么在传参时包一层() this.showName()。这三种方案各有适用场景我简单对比一下方案原理优缺点构造函数里bind(this)利用 bind 的硬绑定把方法绑定到实例每次创建实例多一个绑定函数内存开销略有增加但逻辑清晰急用方便箭头函数类字段定义时捕获 this不受调用方式约束代码简洁但会改变原型链上的方法定义方式不利于继承场景包一层回调延迟调用保证隐式绑定的完整性传参时最直观但每次调用都生成一个临时函数容易出现误用和额外的分配我个人的倾向是如果类字段箭头函数的写法在你的项目里能接受比如不需要类继承或者不需要在原型上暴露方法那它是最优解如果涉及类继承建议用普通方法 在构造函数中绑定或者在基类中统一处理。6.2 拦路虎题new、bind 联手的组合拳再上一个难度看看这道题function Foo() { this.name foo; return { name: bar }; } const Bound Foo.bind({ name: baz }); const instance new Bound(); console.log(instance.name);这道题把三个考点揉到一起了bind 的豁免、构造函数的返回值、new 的优先级。我们来推理一遍Foo.bind({ name: baz })产生了一个绑定函数 Bound但当我们new Bound()时绑定被豁免this 指向新对象Foo 执行给新对象赋name foo随后 Foo 又返回了{ name: bar }这是对象返回值new的结果就变成了这个对象最终instance.name是bar。整个过程里baz和foo都只是过程中被抛弃的中间状态。这个题可以算是 JavaScript 中new绑定优先级和返回值规则的终极试炼了。如果你能在纸上把这三个步骤推出来你就对这些规则有了扎实的把握。6.3 线上排查从 this 丢了 到 修好 的完整思路当你真的在代码里遇到 this 问题时不要急着改代码按下面这个排查链路走效率会高很多先看函数的调用方式是直接调用、作为对象方法调用、通过call/apply/bind调用还是用了new这一眼就能确定 this 大概率指向哪儿再看调用栈如果函数被延迟执行定时器、事件、Promise、第三方回调就要考虑 this 丢失打印验证在函数开头console.log(this)一次输出就足够判断确认修改方案优先考虑箭头函数如果你想保留外层 this否则在定义时用 bind 硬绑定实在不行就包一层箭头函数传参。我记得有一次排查一个脱发的线上问题某个 tab 切换组件在切换后需要自动聚焦输入框。业务代码里有一个setTimeout(() { this.focus() }, 0)但组件实例却被频繁销毁重建导致 setTimeout 回调执行时组件都换了新实例this 指向新实例焦点作用在了错误的输入框上。你说这是 this 绑定问题吗表面上看是为什么会指向错误实例本质上是定时器延迟执行中的实例生命周期问题。这时光靠 bind 已经不够了还得引入销毁时的标记判断比如isUnmounted来规避。这也是我在实战中深刻体会到的一件事this 绑定问题往往不是一个孤立的技术问题它背后还藏着生命周期、闭包、作用域等更大的上下文。所以排查时别只看 bind 不 bind要把整个上下文一起理清。7. 我踩过几次坑之后给你留下的几条实战建议最后这部分不讲新概念了就聊几个我在实际项目里反复验证过的操作习惯也算是对全文的收束。这些习惯不一定写进教科书但对于日常工作非常有用。第一个建议是构造函数里所有需要被异步调用的函数一律用箭头函数定义。不管是定时器、事件监听、fetch 回调还是别人传给你的回调用箭头函数就能保住 this。在 React 类组件时代这个习惯帮我少改了好几个 bug。第二个建议是如果你在写一个库或者插件千万别在构造函数里偷偷 return 对象。库的调用方通常会用instanceof来判断返回值的类型你的 return 对象会直接破坏他们的判断逻辑。单例模式请用静态方法或者模块变量去实现别用返回对象的构造函数除非你测试过所有的调用方式。第三个建议是面试和实际代码里经常出现的 this 题目都不是考记忆而是考推导。遇到这类题不要背结论直接从两条规则出发一看调用形式二看是否涉及new/bind/call/apply。想清楚了大部分题都能推出来。这也是我在给团队面试候选人时最看重的思维方式——你不需要背住每一个边界情况但你需要有一套可靠的推导链条。如果你正在学习 JavaScript 的 this 机制我推荐的做法是打开控制台手写一个构造函数分别用new、call、bind、普通调用四种方式去调用它再尝试在构造函数内部写定时器回调和箭头函数对比它们的 this。这比读十篇博客都有用。JavaScript 的 this 绑定确实容易绕晕但它背后的规则并不复杂复杂的是规则之间的组合。把new这条规则彻底吃透其他规则就都是顺水推舟的事了。