ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

new与点运算符优先级:从零拆解JavaScript经典面试题

new与点运算符优先级:从零拆解JavaScript经典面试题 这两年面试前端我手里常备一道题就是标题里说的这个new 运算符和点运算符优先级的八股题。别听到“八股”就觉得没营养这道题能把 JavaScript 里变量提升、this 指向、new 的底层行为、函数调用优先级这几个最基础也最容易含糊的点一次性全考了。我拿它面过三年经验的候选人也面过刚毕业的校招生能完整答对七个小问的人说实话不多。这题适合谁看如果你正在准备前端面试或者工作中写过new Date().getTime()、new RegExp().test()这类代码但没细想过为什么能这么写那这篇文章就是给你准备的。我会把优先级规则掰开揉碎把每一问从语法解析到执行结果完整推一遍再讲几个面试官常追问的变体。文章偏长但读完你再去面这类题基本可以平推。1. 先把题亮出来一道看起来简单、翻车率却极高的综合题1.1 完整题目代码先看题目代码不长七个小问建议你先自己拿纸笔写一遍答案再往下对function Foo() { this.getName function () { console.log(1); }; return this; } Foo.getName function () { console.log(2); }; Foo.prototype.getName function () { console.log(3); }; var getName function () { console.log(4); }; function getName() { console.log(5); } // 请写出以下表达式的输出结果 Foo.getName(); getName(); Foo().getName(); getName(); new Foo.getName(); new Foo().getName(); new new Foo().getName();我先不公布答案你写完了吗好继续往下看。1.2 这道题到底在考什么这七个小问看似是在问“输出几”其实背后是四个独立的 JavaScript 知识点每一个都能单独拎出来当一道面试题第一是函数对象的静态属性。JavaScript 里函数也是对象Foo.getName ...这种写法是给函数对象本身挂属性它和实例属性、原型属性是三条完全不同的链路。第二是变量提升与声明覆盖。var getName和function getName同时存在时谁覆盖谁哪个先提升这是老生常谈但又总有人记反。第三是普通函数调用中 this 的指向。直接调用Foo()函数体内的 this 在非严格模式下指向全局对象这决定了第三问的答案也埋了第四问的坑。第四是new 和点运算符的优先级。这是全题的核心也是最容易出错的地方。注意new Foo.getName()和new Foo().getName()看起来只差一对括号实际解析结果完全不同。搞懂这一块这题就拿下了一大半。1.3 建议先自己动手验证我强烈建议你在写答案之前先把这段代码原封不动贴到浏览器控制台跑一遍。注意要在浏览器环境跑因为代码里依赖了非严格模式下全局对象window的行为Node.js 里全局对象不叫 window表现会有差异。跑完之后再对照你自己写的答案看看错在哪几题。我这些年看下来错得最集中的就是第五题和第七题几乎全是优先级的问题。2. 优先级是根因new 在 JavaScript 里有两副面孔2.1 先看 MDN 运算符优先级表的关键行要彻底搞懂这道题得先看 JavaScript 运算符优先级表。这里我不贴整张表只截取和题目相关的几行这些行才是这道题的命门优先级运算符类型结合性示例19成员访问、带参数 new、函数调用从左到右foo.bar、foo[bar]、new Foo()、foo()18无参数 new从右到左new Foo注意看带参数列表的new Foo()和无参数列表的new Foo在优先级表里是两个不同的条目前者优先级是 19和点运算符同级后者优先级是 18比点运算符低一级。这一点是理解全题的核心也是最多人忽略的细节。2.2 “带参数 new”和“无参数 new”的差别在哪很多人不知道new Foo()和new Foo在语言规范里是两个不同的语法产生式。new Foo()是new MemberExpression Arguments它后面跟了参数列表new Foo是new NewExpression后面没有参数列表。当你写new Foo不带括号时Javascript 引擎会把它视为一个独立的“无参数 new 表达式”它的优先级只有 18。这个细微差别直接影响表达式的解析方式。举个例子new Foo.bar这个表达式因为无参数 new 的优先级是 18低于点运算符的 19所以引擎会先做成员访问解析成new (Foo.bar)而不是(new Foo).bar。而new Foo().bar因为new Foo()是带参数 new优先级是 19和点运算符同级且都是从左到右结合所以解析成(new Foo()).bar。这就是整道题最关键的一把钥匙成员访问的优先级高于无参数 new但等同于带参数 new。2.3 三个基础表达式先建立直觉我们从简单到复杂先把三个表达式的解析方式搞清楚。第一个new Foo.getName()。这里的new后面跟的是Foo.getName然后有参数列表()。因为无参数 new优先级 18低于点运算符优先级 19引擎先解析Foo.getName这个成员访问再对这个成员访问结果执行 new 操作。所以它等价于new (Foo.getName)()也就是说构造函数是 Foo 的静态方法 getName而不是 Foo 本身。第二个new Foo().getName()。new Foo()是带参数 new优先级 19点运算符也是 19两者从左到右结合所以先执行new Foo()得到实例再对实例做.getName()调用。等价于(new Foo()).getName()这里 new 的是 Foo 构造函数然后访问实例上的 getName 方法。第三个new new Foo().getName()。这个看起来最唬人其实还是优先级规则在起作用。new Foo()先结合得到实例然后.getName成员访问最外层无参数 new 的优先级最低作用在(new Foo()).getName这个成员访问的结果上。所以它等价于new ((new Foo()).getName)()最外层 new 的构造函数是实例的 getName 方法。这三个基础表达式搞明白第五、六、七问的答案基本就出来了。2.4 记忆口诀有朋友问我怎么记我自己总结了一个口诀点运算和带参 new 同级别从左到右无参 new 低一级靠后站。写代码时如果不确定有个笨办法是加括号把你想先执行的部分显式括起来这样既不依赖记忆也方便别人 review 你的代码。3. 逐题拆解七个小问一步一步推到答案3.1Foo.getName()函数对象的静态属性第一问是送分题。Foo.getName function () { console.log(2); }这行代码是给 Foo 函数对象本身挂了一个属性叫 getName。所以Foo.getName()就是调用这个静态方法输出 2。这里有个点可以顺带复习一下函数是对象函数可以有属性。Foo自身有getName属性第 6 行赋值的Foo.prototype上也有一个getName属性第 9 行赋值的这两个是互不干扰的。如果你想访问实例上的getName需要通过实例去访问原型链会帮你找到Foo.prototype.getName。而Foo.getName是直接访问函数对象自己的属性不会走原型链。3.2getName()变量提升的正确姿势第二问开始上难度了。这里有两个同名声明var getName function () { console.log(4); }和function getName() { console.log(5); }。JavaScript 引擎在执行代码之前会先进行变量提升。var getName的声明被提升到顶部此时 getName 的值是 undefined。接着函数声明function getName() { console.log(5); }也被整体提升而且函数声明会覆盖掉之前那个值为 undefined 的变量声明。所以在代码真正执行之前全局作用域里的 getName 其实已经是function () { console.log(5); }这个函数了。但是注意执行顺序。代码从上往下执行到var getName function () { console.log(4); }这一行时会执行赋值操作把 getName 重新指向输出 4 的函数。function getName() { console.log(5); }这个函数声明在执行阶段不会再执行所以最终全局的 getName 是输出 4 的那个函数。答案就是 4。这里有个容易混淆的知识点函数声明提升是“整体提升”包含函数体而var声明提升只是“声明提升”不包含赋值。所以当两种声明同时存在时执行阶段变量赋值会覆盖函数声明。3.3Foo().getName()直接调用下的 this 指向第三问Foo()是普通函数调用。关键在 Foo 函数内部function Foo() { this.getName function () { console.log(1); }; return this; }非严格模式下普通函数调用前面没有对象点出来也没有 new里的 this 指向全局对象。在浏览器里就是 window。所以执行Foo()时相当于给 window 挂了一个getName方法然后return this返回的也是 window。接下来Foo().getName()是在Foo()的返回值上调用 getName。因为返回值是 window而 window 上刚被挂上了 getName所以输出 1。这里我提醒一个细节Foo()执行后会产生一个副作用——全局对象的 getName 被覆盖了。原来全局的 getName 是输出 4 的那个函数现在被换成了输出 1 的函数。这个副作用直接影响到第四问的答案很多人在第四问栽跟头就是因为忽略了这一点。3.4getName()上一步副作用带来的结果翻转第四问单独拿出来很简单但放在这里就是个陷阱。经过第三问的调用全局对象window上的 getName 已经被替换成了输出 1 的函数。而getName在全局作用域里就是 window.getName所以再调用getName()输出 1。如果你忽略了第三问的副作用会以为这里和第一问一样输出 4那就错了。这类题目设计的巧妙之处就在这它考的不仅仅是某个孤立的知识点而是你能否追踪代码执行过程中状态的连续变化。3.5new Foo.getName()优先级决定一切第五问是整道题的第一个分水岭。new Foo.getName()这个表达式new后面没有直接跟()所以这里是无参数 new优先级 18而点运算符的优先级是 19更高。引擎会先做成员访问把Foo.getName作为整体然后对这个整体执行 new 操作。所以new Foo.getName()等价于new (Foo.getName)()。Foo.getName是那个输出 2 的函数把它当构造函数调用执行结果是输出 2。第五问的答案就是 2。这里我要特别强调一个常见的错误理解有人把new Foo.getName()错解析成new (Foo.getName())也就是先执行Foo.getName()得到返回值再对返回值执行 new。如果真这么解析Foo.getName()返回的是 undefinednew undefined会直接抛出 TypeError。但实际代码不会报错说明解析方式不是这个。原因就在于new 表达式的语法结构里()是 new 的参数列表不是函数调用它是和前面的成员访问Foo.getName组合在一起构成new (Foo.getName)()的。3.6new Foo().getName()带参数 new 和点运算符同优先级第六问new Foo().getName()。这里new Foo()是带参数 new优先级 19和点运算符同为 19按从左到右结合先执行new Foo()。new Foo()会创建一个新对象然后执行 Foo 函数体此时 Foo 函数内的 this 指向这个新对象所以新对象会被挂上getName方法也就是输出 1 的那个函数。Foo 里return this返回的也是这个新对象其实就算它不 returnnew 默认返回的就是这个新对象。接下来(new Foo()).getName()在实例上调用 getName输出 1。第六问答案是 1。这里可以对比一下如果写的是new Foo().getName不带最后的括号结果会变成对实例上 getName 函数执行无参数 new也就是new (new Foo().getName)这时候会创建一个新对象并执行 getName 函数输出还是 1但表达式的最终值是一个对象而不是数字。这种写法容易让人困惑实际项目里建议加括号明确。3.7new new Foo().getName()双 new 解析第七问是终极关卡也是优先级知识的综合应用。new new Foo().getName()里有两个 new要分清楚哪个先执行。按照优先级规则new Foo()带参数优先级最高先结合成(new Foo())然后.getName成员访问得到实例上的 getName 方法最后外层new是无参数 new因为new后面没有直接跟()而是跟了一个表达式它的优先级最低作用在整个成员访问表达式的结果上。所以new new Foo().getName()等价于new ((new Foo()).getName)()。也就是说最外层 new 的构造函数是实例的 getName 方法。执行这个构造函数会输出 1并且返回一个新对象。第七问答案是 1。为了验证理解你可以做个小实验把这几个表达式分别加上括号再跑一遍new ((new Foo()).getName)(); // 输出 1 new (new Foo().getName)(); // 输出 1 new (new Foo()).getName(); // 输出 1几种不同加括号方式最后都是 1但它们解析的构造过程截然不同。这就是为什么我强调要看懂优先级规则而不是死记答案。4. 变体与延伸这个考点还能怎么玩4.1 加括号改变行为面试官考完原题大概率会加一个追问如果我在某个位置加上括号结果会变吗我整理了几个常见变体你可以收藏当笔记用。new (Foo.getName)(); // 显式指定 new 的构造函数是 Foo.getName输出 2 new (Foo().getName)(); // 先执行 Foo()返回 window再取 window.getName对输出 1 的函数执行 new输出 1 (new Foo()).getName(); // 先 new Foo()再取实例上的 getName输出 1 new (new Foo().getName)(); // 先 new Foo() 再取 instanceof getName 再 new输出 1 (new new Foo().getName)(); // 语法上会报错吗实际上可以跑输出 1加括号的好处是消除歧义。面试时主动写出带括号的等价写法是加分项说明你不仅知道答案还知道这是优先级规则推出来的结果而不是背的。4.2 严格模式下的翻车这道题默认是在非严格模式下跑的。如果把 Foo 函数体末尾加上use strict;第三问的结果会完全不同function Foo() { use strict; this.getName function () { console.log(1); }; return this; } Foo().getName(); // TypeError: Cannot read properties of undefined严格模式下普通函数调用里的 this 是 undefined不是全局对象。所以Foo()执行时this.getName ...会直接抛错因为不能给 undefined 添加属性。很多人知道严格模式会影响 this但遇到具体题目时容易忘记面试时被问到“如果开头加一行 use strict 会怎样”要能答出这一步。4.3 箭头函数和 class 场景下的限制再延伸一下如果把 Foo.getName 改成箭头函数Foo.getName () { console.log(2); }; new Foo.getName(); // TypeError: Foo.getName is not a constructor箭头函数没有 [[Construct]] 内部方法不能作为构造函数使用new 它会直接抛 TypeError。这是一个很常见的“延伸题”考的是 new 对函数类型的限制。ES6 的 class 也一样class 的静态方法虽然可以正常调用但不能被 newclass Foo { static getName() { console.log(2); } } new Foo.getName(); // TypeError: Foo.getName is not a constructor不过 class 的实例方法也有类似约束class 本身不能被当成函数调用。这些延伸知识都是面试官喜欢追问的点。4.4 构造函数返回值对 new 的影响Foo 函数体里有一句return this很多人好奇这一句到底是不是多余的。在 new 调用中return this确实不影响结果因为 new 默认构造过程已经创建了一个新对象并且 this 指向它。但构造函数如果返回一个对象结果就变了function Bar() { this.name instance; return { name: override }; } const b new Bar(); console.log(b.name); // override如果构造函数返回的是原始值数字、字符串、布尔值、null、undefinednew 的结果仍然是新建的实例但如果返回的是对象new 的结果会被这个对象替代。这道题里 Foo 的return this在 new 调用时返回的是新对象不改变行为但在普通调用时返回全局对象这正好服务了第三问。4.5 实际项目中的“踩坑”场景有人觉得这类题纯属面试刁难实际工作中真的会遇到。我举一个很常见的例子new Date().getTime()。这个表达式为什么能正常工作因为new Date()是带参数 new优先级和点运算符相同从左到右等价于(new Date()).getTime()。但如果你手滑写成new Date.getTime()解析就变成new (Date.getTime)()而Date.getTime本身不是构造函数会直接报错。这类问题在写工具函数、封装类库时特别容易遇到尤其是项目里使用new返回对象的第三方库时理解优先级能帮你少踩很多坑。5. 面试现场怎么答才有含金量5.1 推荐回答框架如果面试问道这道题我建议你按三层来组织回答第一层直接给出每个表达式的输出结果简洁有力不犹豫。第二层挑选两到三个关键表达式比如第三问、第五问、第七问讲清楚背后的机制变量提升的顺序、普通函数调用 this 指向全局对象、new 与点运算符的优先级差异。讲这一层时能用 MDN 优先级表说明“无参数 new 优先级低于成员访问”是最好的这是区分死记硬背和真正理解的分水岭。第三层主动补充变体和边界情况如果加括号会变成什么、严格模式下会怎样、改成箭头函数会怎样。这一层是加分项能展示你知识面的广度。5.2 最容易说错的两个细节我在面试中听过很多答案有两个细节几乎人人都会栽。第一个是new Foo.getName()的解析过程。很多人直接说“new 的优先级比点高”然后背出答案是 2但问他为什么没有报错就解释不清了。正确的说法是这里的 new 是无参数 new它的优先级低于点运算符所以引擎先解析Foo.getName再对结果执行 new。这个细节要说清楚否则面试官会认为你只是背了答案。第二个是new new Foo().getName()很多人不知道内层先结合会猜成报错或者输出 2。这里的关键是new Foo()带参数优先级最高先得到实例然后.getName最后外层无参数 new 作用于这个成员访问结果。有了这个推导链条答案 1 就是自然而然的结果。5.3 常见追问和应对面试官问完原题大概率会追问一个变体常见的有如果把var getName ...和function getName() {...}的顺序换一下结果变吗答案不变因为函数声明整体提升发生在编译阶段而变量赋值发生在执行阶段执行顺序决定最终值。如果在文件开头加上use strict哪些题的答案会变答案第三问会报错可能连带第四问也无法正常执行。把function Foo() {...}改成class Foo {...}还能这么玩吗答案class 有诸多限制静态方法不能 newclass 不能普通调用优先级问题反而更少。把new Foo().getName()中的点换成方括号new Foo()[getName]()结果一样吗答案一样方括号成员访问和点运算符优先级相同。这类追问万变不离其宗只要优先级表和 this 指向这两块根基牢固基本都能答上来。5.4 建议面试前自己出一道变式题如果你正在准备面试我建议你以这道题为蓝本自己改编一个变式出来换一个构造函数、换一组变量声明、加一个 return 对象的分支。自己出题的过程其实就是逼迫自己把每个知识点串起来的过程。等你能够轻松改编一道题并且准确预判所有输出时这块内容基本就焊死在脑子里了。最后分享一点个人体会。这道题我之所以一直留在面试题清单里不是因为它多刁钻而是因为它特别能反映一个人写代码时对语言机制的理解程度。能全对的人通常写代码时边界感很强知道每一行表达式会被引擎怎么解析错一两个的人往往工作中也容易写出“看起来对但只在特定情况下对”的代码。把这套优先级规则吃透短时间是应付面试长期看是在给自己的 JavaScript 功底打底子。在实际项目中我建议但凡遇到new和.混用的表达式不犹豫直接加括号写明意图别和自己记性过不去。
返回列表