js 变量及作用域经典面试题

article/2025/11/7 9:23:46
function Foo() {getName = function () { alert (1); };return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

 

这几天面试上几次碰上这道经典的题目,特地从头到尾来分析一次答案,这道题的经典之处在于它综合考察了面试者的JavaScript的综合能力,包含了变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等知识,此题在网上也有部分相关的解释,当然我觉得有部分解释还欠妥,不够清晰,特地重头到尾来分析一次,当然我们会把最终答案放在后面,并把此题再改高一点点难度,改进版也放在最后,方便面试官在出题的时候有个参考,更多详情可关注本文作者@Wscats

第一问

先看此题的上半部分做了什么,首先定义了一个叫Foo的函数,之后为Foo创建了一个叫getName的静态属性存储了一个匿名函数,之后为Foo的原型对象新创建了一个叫getName的匿名函数。之后又通过函数变量表达式创建了一个getName的函数,最后再声明一个叫getName函数。

第一问的Foo.getName自然是访问Foo函数上存储的静态属性,答案自然是2,这里就不需要解释太多的,一般来说第一问对于稍微懂JS基础的同学来说应该是没问题的,当然我们可以用下面的代码来回顾一下基础,先加深一下了解

function User(name) {var name = name; //私有属性this.name = name; //公有属性function getName() { //私有方法return name;}
}
User.prototype.getName = function() { //公有方法return this.name;
}
User.name = 'Wscats'; //静态属性
User.getName = function() { //静态方法return this.name;
}
var Wscat = new User('Wscats'); //实例化

注意下面这几点:

  • 调用公有方法,公有属性,我们必需先实例化对象,也就是用new操作符实化对象,就可构造函数实例化对象的方法和属性,并且公有方法是不能调用私有方法和静态方法的

  • 静态方法和静态属性就是我们无需实例化就可以调用

  • 而对象的私有方法和属性,外部是不可以访问的

第二问

第二问,直接调用getName函数。既然是直接调用那么就是访问当前上文作用域内的叫getName的函数,所以这里应该直接把关注点放在4和5上,跟1 2 3都没什么关系。当然后来我问了我的几个同事他们大多数回答了5。此处其实有两个坑,一是变量声明提升,二是函数表达式和函数声明的区别。

我们来看看为什么,可参考(1)关于Javascript的函数声明和函数表达式 (2)关于JavaScript的变量提升

在Javascript中,定义函数有两种类型

函数声明

// 函数声明
function wscat(type) {return type === "wscat";
}

函数表达式

// 函数表达式
var oaoafly = function(type) {return type === "oaoafly";
}

先看下面这个经典问题,在一个程序里面同时用函数声明和函数表达式定义一个名为getName的函数

getName() //oaoafly
var getName = function() {console.log('wscat')
}
getName() //wscat
function getName() {console.log('oaoafly')
}
getName() //wscat

上面的代码看起来很类似,感觉也没什么太大差别。但实际上,Javascript函数上的一个“陷阱”就体现在Javascript两种类型的函数定义上。

  • JavaScript 解释器中存在一种变量声明被提升的机制,也就是说函数声明会被提升到作用域的最前面,即使写代码的时候是写在最后面,也还是会被提升至最前面。

  • 而用函数表达式创建的函数是在运行时进行赋值,且要等到表达式赋值完成后才能调用

var getName //变量被提升,此时为undefinedgetName() //oaoafly 函数被提升 这里受函数声明的影响,虽然函数声明在最后可以被提升到最前面了
var getName = function() {console.log('wscat')
} //函数表达式此时才开始覆盖函数声明的定义
getName() //wscat
function getName() {console.log('oaoafly')
}
getName() //wscat 这里就执行了函数表达式的值

所以可以分解为这两个简单的问题来看清楚区别的本质

var getName;
console.log(getName) //undefined
getName() //Uncaught TypeError: getName is not a function
var getName = function() {console.log('wscat')
}
var getName;
console.log(getName) //function getName() {console.log('oaoafly')}
getName() //oaoafly
function getName() {console.log('oaoafly')
}

这个区别看似微不足道,但在某些情况下确实是一个难以察觉并且“致命“的陷阱。出现这个陷阱的本质原因体现在这两种类型在函数提升和运行时机(解析时/运行时)上的差异。

当然我们给一个总结:Javascript中函数声明和函数表达式是存在区别的,函数声明在JS解析时进行函数提升,因此在同一个作用域内,不管函数声明在哪里定义,该函数都可以进行调用。而函数表达式的值是在JS运行时确定,并且在表达式赋值完成后,该函数才能调用。

所以第二问的答案就是4,5的函数声明被4的函数表达式覆盖了

第三问

Foo().getName(); 先执行了Foo函数,然后调用Foo函数的返回值对象的getName属性函数。

Foo函数的第一句getName = function () { alert (1); };是一句函数赋值语句,注意它没有var声明,所以先向当前Foo函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的alert(4)函数,将此变量的值赋值为function(){alert(1)}

此处实际上是将外层作用域内的getName函数修改了。

注意:此处若依然没有找到会一直向上查找到window对象,若window对象中也没有getName属性,就在window对象中创建一个getName变量。

之后Foo函数的返回值是this,而JS的this问题已经有非常多的文章介绍,这里不再多说。

简单的讲,this的指向是由所在函数的调用方式决定的。而此处的直接调用方式,this指向window对象。

遂Foo函数返回的是window对象,相当于执行window.getName(),而window中的getName已经被修改为alert(1),所以最终会输出1
此处考察了两个知识点,一个是变量作用域问题,一个是this指向问题
我们可以利用下面代码来回顾下这两个知识点

var name = "Wscats"; //全局变量
window.name = "Wscats"; //全局变量
function getName() {name = "Oaoafly"; //去掉var变成了全局变量var privateName = "Stacsw";return function() {console.log(this); //windowreturn privateName}
}
var getPrivate = getName("Hello"); //当然传参是局部变量,但函数里面我没有接受这个参数
console.log(name) //Oaoafly
console.log(getPrivate()) //Stacsw

因为JS没有块级作用域,但是函数是能产生一个作用域的,函数内部不同定义值的方法会直接或者间接影响到全局或者局部变量,函数内部的私有变量可以用闭包获取,函数还真的是第一公民呀~

而关于this,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象

所以第三问中实际上就是window在调用**Foo()**函数,所以this的指向是window

window.Foo().getName();
//->window.getName();

第四问

直接调用getName函数,相当于window.getName(),因为这个变量已经被Foo函数执行时修改了,遂结果与第三问相同,为1,也就是说Foo执行后把全局的getName函数给重写了一次,所以结果就是Foo()执行重写的那个getName函数

第五问

第五问new Foo.getName();此处考察的是JS的运算符优先级问题,我觉得这是这题灵魂的所在,也是难度比较大的一题

下面是JS运算符的优先级表格,从高到低排列。可参考MDN运算符优先级

优先级运算类型关联性运算符
19圆括号n/a( … )
18成员访问从左到右… . …
 需计算的成员访问从左到右… [ … ]
 new (带参数列表)n/a new… ( … )
17函数调用从左到右… ( … )
 new (无参数列表)从右到左new …
16后置递增(运算符在后)n/a… ++
 后置递减(运算符在后)n/a… --
15逻辑非从右到左! …
 按位非从右到左~ …
 一元加法从右到左+ …
 一元减法从右到左- …
 前置递增从右到左++ …
 前置递减从右到左-- …
 typeof从右到左typeof …
 void从右到左void …
 delete从右到左delete …
14乘法从左到右… * …
 除法从左到右… / …
 取模从左到右… % …
13加法从左到右… + …
 减法从左到右… - …
12按位左移从左到右… << …
 按位右移从左到右… >> …
 无符号右移从左到右… >>> …
11小于从左到右… < …
 小于等于从左到右… <= …
 大于从左到右… > …
 大于等于从左到右… >= …
 in从左到右… in …
 instanceof从左到右… instanceof …
10等号从左到右… == …
 非等号从左到右… != …
 全等号从左到右… === …
 非全等号从左到右… !== …
9按位与从左到右… & …
8按位异或从左到右… ^ …
7按位或从左到右… 按位或 …
6逻辑与从左到右… && …
5逻辑或从左到右… 逻辑或 …
4条件运算符从右到左… ? … : …
3赋值从右到左… = …
   … += …
   … -= …
   … *= …
   … /= …
   … %= …
   … <<= …
   … >>= …
   … >>>= …
   … &= …
   … ^= …
   … 或= …
2yield从右到左yield …
 yield*从右到左yield* …
1展开运算符n/a... …
0逗号从左到右… , …

这题首先看优先级的第18和第17都出现关于new的优先级,new (带参数列表)比new (无参数列表)高比函数调用高,跟成员访问同级

new Foo.getName();的优先级是这样的

相当于是:

new (Foo.getName)();
  • 点的优先级(18)比new无参数列表(17)优先级高

  • 当点运算完后又因为有个括号(),此时就是变成new有参数列表(18),所以直接执行new,当然也可能有朋友会有疑问为什么遇到()不函数调用再new呢,那是因为函数调用(17)比new有参数列表(18)优先级低

.成员访问(18)->new有参数列表(18)

所以这里实际上将getName函数作为了构造函数来执行,遂弹出2。

第六问

这一题比上一题的唯一区别就是在Foo那里多出了一个括号,这个有括号跟没括号我们在第五问的时候也看出来优先级是有区别的

(new Foo()).getName()

那这里又是怎么判断的呢?首先new有参数列表(18)跟点的优先级(18)是同级,同级的话按照从左向右的执行顺序,所以先执行new有参数列表(18)再执行点的优先级(18),最后再函数调用(17)

new有参数列表(18)->.成员访问(18)->()函数调用(17)

这里还有一个小知识点,Foo作为构造函数有返回值,所以这里需要说明下JS中的构造函数返回值问题。

构造函数的返回值

在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。
而在JS中构造函数可以有返回值也可以没有。

  1. 没有返回值则按照其他语言一样返回实例化对象。

function Foo(name) {this.name = name
}
console.log(new Foo('wscats'))

 

  1. 若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本类型(String,Number,Boolean,Null,Undefined)则与无返回值相同,实际返回其实例化对象。

function Foo(name) {this.name = namereturn 520
}
console.log(new Foo('wscats'))

 

  1. 若返回值是引用类型,则实际返回值为这个引用类型。

function Foo(name) {this.name = namereturn {age: 16}
}
console.log(new Foo('wscats'))


原题中,由于返回的是this,而this在构造函数中本来就代表当前实例化对象,最终Foo函数返回实例化对象。

之后调用实例化对象的getName函数,因为在Foo构造函数中没有为实例化对象添加任何属性,当前对象的原型对象(prototype)中寻找getName函数。

当然这里再拓展个题外话,如果构造函数和原型链都有相同的方法,如下面的代码,那么默认会拿构造函数的公有方法而不是原型链,这个知识点在原题中没有表现出来,后面改进版我已经加上。

function Foo(name) {this.name = namethis.getName = function() {return this.name}
}
Foo.prototype.name = 'Oaoafly';
Foo.prototype.getName = function() {return 'Oaoafly'
}
console.log((new Foo('Wscats')).name) //Wscats
console.log((new Foo('Wscats')).getName()) //Wscats

第七问

new new Foo().getName();同样是运算符优先级问题。做到这一题其实我已经觉得答案没那么重要了,关键只是考察面试者是否真的知道面试官在考察我们什么。
最终实际执行为:

new ((new Foo()).getName)();

new有参数列表(18)->new有参数列表(18)

先初始化Foo的实例化对象,然后将其原型上的getName函数作为构造函数再次new,所以最终结果为3

答案

function Foo() {getName = function () { alert (1); };return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}//答案:
Foo.getName();//2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3

后续

后续我把这题的难度再稍微加大一点点(附上答案),在Foo函数里面加多一个公有方法getName,对于下面这题如果用在面试题上那通过率可能就更低了,因为难度又大了一点,又多了两个坑,但是明白了这题的原理就等同于明白了上面所有的知识点了

function Foo() {this.getName = function() {console.log(3);return {getName: getName //这个就是第六问中涉及的构造函数的返回值问题}}; //这个就是第六问中涉及到的,JS构造函数公有方法和原型链方法的优先级getName = function() {console.log(1);};return this
}
Foo.getName = function() {console.log(2);
};
Foo.prototype.getName = function() {console.log(6);
};
var getName = function() {console.log(4);
};function getName() {console.log(5);
} //答案:
Foo.getName(); //2
getName(); //4
console.log(Foo())
Foo().getName(); //1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //3
//多了一问
new Foo().getName().getName(); //3 1
new new Foo().getName(); //3

 

最后,其实我是不建议把这些题作为考察面试者的唯一评判,但是作为一名合格的前端工程师我们不应该因为浮躁忽略了我们的一些最基本的基础知识,当然我也祝愿所有面试者找到一份理想的工作,祝愿所有面试官找到心中那匹千里马~


http://chatgpt.dhexx.cn/article/x0u0fJjo.shtml

相关文章

前端面试题集锦——JavaScript

JavaScript 栈和队列的区别? 栈的插入和删除操作都是在一端进行的&#xff0c;而队列的操作却是在两端进行的。 队列先进先出&#xff0c;栈先进后出。 栈只允许在表尾一端进行插入和删除&#xff0c;而队列只允许在表尾一端进行插入&#xff0c;在表头一端进行删除 栈和…

收集的面试题 js面试题

题库推荐网址&#xff1a;https://github.com/FEGuideTeam/FEGuide/tree/master/javascript%E9%97%AE%E9%A2%98 第1题: ## JavaScript 由以下三部分组成&#xff1a;1. ECMAScript&#xff08;核心&#xff09;&#xff1a;JavaScript 语言基础2. DOM&#xff08;文档对象模…

JS闭包+常见面试题

scope作用域 、Closure闭包对象 可以借助chrome调式工具查看闭包对象 注意&#xff1a;function声明存在变量提升&#xff0c;所以22行已经存在闭包对象了&#xff1b; 闭包产生的条件&#xff1a; 函数嵌套&#xff1b;嵌套的内部函数引用了外部函数的变量才会产生闭包对象…

前端面试题汇总(含答案)(JS篇)

主要自用&#xff0c;持续更新&#xff0c;相同类型的题目尽量放在了一起&#xff0c;参考的实在太多了就没有列出&#xff0c;侵权烦请联系删除。提示&#xff1a;自动生成的目录在页面右边---------->>>>>>>>>>>>>>>> Js的…

前端面试题(js篇)

1.解释一下什么是闭包 什么是闭包&#xff1a;函数使用了不属于自己的局部变量(函数套函数&#xff0c;里面函数使用了外面函数定义的变量) 闭包的作用&#xff1a;避免全局污染 闭包的缺点&#xff1a;使用过多会造成内存泄漏&#xff08;占用的内存释放不掉&#xff09; 2.…

js 实现页面隐藏、关闭、刷新给出对应的提示

我们在做项目的时候经常会遇到一些需求&#xff0c;比如在某些页面当点击浏览器刷新 或者关闭的时候会有对应的提示&#xff0c;是否离开或者重新加载此网站。比如csdn写文章的时候就有这个弹窗&#xff0c;这功能就是用onbeforeunload实现的。 注意&#xff1a;如果你加载下面…

js面试题大坑——隐式类型转换

1.1 隐式转换介绍 1.2 隐式转换规则 1.3 坑一&#xff1a;字符串连接符与算术运算符隐式转换规则混淆 1.4 坑二&#xff1a;关系运算符&#xff1a;会把其他数据类型转换成number之后再比较关系 1.5 坑三&#xff1a;复杂数据类型在隐式转换时会先转成String&#xff0…

u一点·料:阿里巴巴1688ued体验设计践行之路

U一点料 阿里巴巴1688UED体验设计践行之路 阿里巴巴1688用户体验部著 图书在版编目&#xff08;CIP&#xff09;数据 U一点料&#xff1a;阿里巴巴1688UED体验设计践行之路/阿里巴巴1688用户体验部著. —北京&#xff1a;机械工业出版社&#xff0c;2015.8 ISBN 978-7-111-5122…

一段百年征程的有限单群

1832年的某个清晨&#xff0c;革命中的法国见证了重新决斗。 在某个瞬间&#xff0c;某位青年被对手的枪射中腹部。随后去世。在当时狂热的政治斗争中&#xff0c;仅仅有寥寥数人意识到。法国&#xff0c;甚至世界。又失去了还有一个伟大的头脑。 这位青年姓伽罗华&#xff0c;…

权威发布:新一代人工智能发展白皮书(2017)

来源&#xff1a;机器人大讲堂 指导单位、专家顾问及编写人员 顾 问 潘云鹤 中国工程院院士 指导单位 工业和信息化部信息化和软件服务业司 指导委员会 谢少锋 工信部信软司司长 李冠宇 工信部信软司副司长 徐晓兰 中国电子学会副理事长兼秘书长 张宏图 中国电…

《构建之法,邹欣》阅读笔记

前言&#xff1a; 从2018年10月30日开始&#xff0c;阅读由微软工程师邹欣老师撰写的《构建之法》一书&#xff0c;全书共435页&#xff0c;每天阅读15页&#xff0c;在一个月&#xff08;30天&#xff09;完成。每天阅读完成后&#xff0c;需要思考当日的阅读要点和一些思考。…

C语言练习——提高篇

新开通了本人的公众号&#xff0c;欢迎关注&#xff1a;燕南路GISer &#xff0c;专注GIS干货分享&#xff0c;不定期更新。 主要兴趣&#xff1a;GIS、时空数据挖掘、python、机器学习深度学习 CSDN的部分内容会重写再搬迁到公众号&#xff0c;欢迎关注&#xff01; 目录 汉诺…

一文说透低代码平台/无代码平台

一、低代码/无代码平台是什么 二、低代码/无代码平台是怎么产生的 三、低代码/无代码平台应具备哪些能力 四、主流的低代码/无代码平台有哪些 五、低代码/无代码平台典型应用场景 六、低代码/无代码平台有什么价值 七、低代码/无代码平台有什么优势 八、低代码/无代码平…

代码知识点

JS&基础知识篇&#xff1a; 1、事件流 分为捕获型、冒泡型&#xff0c;addEventListener的第三个参数&#xff0c;为true是捕获型&#xff0c;为false是冒泡型&#xff08;即默认不写是冒泡型&#xff09; 常用的事件&#xff1a;click、mouseover&#xff08;支持冒泡…

「可视化搭建系统」——从设计到架构,探索前端领域技术和业务价值

阿里巴巴集团前端委员会主席 圆心&#xff1a;未来前端的机会在哪里 对前端未来期许有四点&#xff1a;搭建服务&#xff0c; Serverless&#xff0c;智能化&#xff0c;IDE。仔细想想&#xff0c;一个「可视化搭建系统」的想象空间&#xff0c;正能完美命中这些方面。前端的边…

集合类详解

1.List 1.1 ArrayList ArrayList概述 实现List接口的动态数组&#xff08;大小可变&#xff09;。默认初始容量10&#xff0c;随着元素增加容量也在不断变化每次添加之前检查是否需要扩容带来数据向新数组的拷贝&#xff0c;若知道数据量可以指定一个初始容量。ArrayList实现…

魔方还原算法(二) 科先巴的二阶段算法

科先巴的二阶段算法 本文来具体介绍一种具体的魔方还原算法——科先巴的二阶段算法&#xff0c;有一部分相关内容在前篇讲述&#xff0c;主要是方向定义那一块儿&#xff0c;没有看的建议先看一下&#xff1a; 二阶段&#xff0c;顾名思义&#xff0c;解决问题分为两步&#…

BUUCTF cmcc_simplerop

cmcc_simplerop 由于网络安全课程需要&#xff0c;从本篇开始记录BUU的做题记录及wp 常规操作&#xff0c;拿到文件先检查&#xff0c;保护开得不多 ida查看&#xff0c;存在明显溢出&#xff0c;并且提示要使用ROP 利用pwndbg得到偏移量为0x20 找到了系统调用int 0x80 整…

BUUCTF | [GXYCTF2019]BabySQli

BUUCTF | [GXYCTF2019]BabySQli 一、必备基础知识 当号被过滤了使用like&#xff0c;rlike绕过 当or被过滤了使用大小写绕过&#xff0c;双写绕过&#xff0c;&&绕过 二、实战化渗透 [GXYCTF2019]BabySQli 因为本道题目的请求方式是POST&#xff0c;直接上手抓包&a…

Cefsharp 与js交互

C# 部分代码 var setting new CefSettings(); setting.CefCommandLineArgs.Add("disable-gpu", "1"); if(!Directory.Exists(Application.StartupPath "\\BrowserCache")) { Directory.…