网易JS面试题解析:深入理解Javascript词法作用域与变量提升

作者:小括号

时间:2026-10-25

浏览:537

本文通过一道网易JS面试题,深入解析JavaScript中词法作用域与变量提升机制。文章指出,函数在定义时确定作用域,局部变量声明会被提升至函数顶部,导致首次访问时值为undefined。通过对比不同代码写法,澄清了先调用后定义及变量隐藏等常见误区,帮助开发者正确理解JS作用域链与闭包基础。

文章目录

调用对象始终位于作用域链的最前端。在函数内部使用var声明的局部变量、函数参数以及Arguments对象均归属于该函数的作用域。这意味着,这些内部声明会屏蔽(隐藏)作用域链上层中任何同名的属性或变量。



网易JS面试题回顾

2010年9月14日,笔者参加了网易网页工程师的招聘笔试,应聘JavaScript工程师职位。虽然有幸进入笔试环节,但最终也遗憾地止步于笔试。这里略去无关细节,重点回顾一道印象极深的题目并重新分析。



题目要求:写出以下代码的输出结果,并进行详细分析。



代码如下:

 
var tt = 'aa';
function test(){
alert(tt);
var tt = 'dd';
alert(tt);
} 
test();

看到这道题时,我的第一反应是“太简单了”,于是轻率地给出了答案:先输出全局变量'aa',接着局部变量tt覆盖全局变量,因此第二次输出'dd'。解析逻辑是:第一次输出全局变量结果,第二次输出被局部变量覆盖后的结果。



如果有人这样作答,旁人可能会认为这是在扫盲——这种想法确实非常幼稚(笔者自己也这么认为)。



然而,网易作为技术公司,怎么可能接受这种肤浅的答案!



正确答案实际上是:undefined和dd。



词法作用域与变量提升机制

为什么第一次alert的结果是undefined?要清楚解释这一点,必须引入JavaScript的词法作用域概念。



JavaScript权威指南中有一句抽象而精辟的总结:函数“在定义它们的作用域里运行,而不是在执行它们的作用域里运行”。



JavaScript默认在全局作用域中执行逻辑。例如上述代码中的“var tt='aa';”定义了一个全局变量(假设代码未嵌套在其他函数中)。而test()函数内部的逻辑需要在原有的全局作用域链基础上,添加test函数自身的作用域(局部性)。这种层级结构在大多数编程语言中都是如此定义的,但JavaScript的特殊之处在于函数内部可以嵌套定义其他函数(这是闭包的基础。注:在JS中,函数是唯一的代码作用域形式)。



嵌套的内部函数可以调用外部函数(被嵌套的函数)的变量以及其他嵌套函数(因为函数也是一种数据)。如果在外部函数内部调用嵌套函数,调用对象保持不变;当外部函数执行完毕后,所有数据(包括外部函数和嵌套的内部函数)都将被垃圾回收机制收集——这一点尚未完全体现“闭包”的精华。有一种情况是,JavaScript允许外部调用嵌套的内部函数,即使被嵌套函数已经被“垃圾回收”。最常见的场景是在“某个函数”中用其嵌套的内部函数定义某些元素的响应事件。当页面载入时,被嵌套函数(“某个函数”)已经执行完毕(看似被垃圾回收),但当事件触发时,仍然会有响应动作,且响应函数中可能调用到在被嵌套函数中定义的变量的最终值(尽管它看似已被垃圾回收)。



关于闭包的知识及示例资料众多,此处不再赘述。



本文的重点在于以下非常重要的细节:



调用对象位于作用域链的前端,局部变量(在函数内部用var声明的变量)、函数参数及Arguments对象都在函数内的作用域中——这意味着它们隐藏了作用域链更上层的任何同名的属性。



具体到上述程序片段,test函数内部的“var tt='dd'”会导致“var tt='aa'”在test函数被调用时完全被隐藏。而且,tt是在第一个alert语句之后才定义的,因此在执行到第一个alert时,tt尚未被赋值。这样解释可能更清晰:在定义test函数时,当定义第一个alert(tt)时,系统会记录tt是作用域链中的一个变量,但不会记录它的值。函数定义完毕后,tt才被添加到作用域里。因此,第一个alert语句能够找到该作用域里的tt(即,相当于找到一个已在函数内部声明但未被赋值的变量)。



上述程序片段的执行结果与以下片段的结果相同:



代码如下:

 
var tt = 'aa';
function test(){
var tt;
alert(tt);
tt = 'dd';
alert(tt);
} 
test();

JavaScript与C++作用域差异及代码规范建议

JavaScript的作用域不能简单地用C++等语言的思维来理解。在C++中,调用函数之前必须先声明或定义,而JavaScript则无需如此。在JavaScript中,可以先调用函数,后再定义(无需在调用之前做任何声明)。因为在调用函数时,JavaScript是向作用域链查找函数的定义(函数在定义它们的作用域里运行,而不是在执行它们的作用域里运行)。



例如,将上述代码改写为:


代码如下:

 
var tt = 'aa';
test(); //先调用后再定义
function test(){
alert(tt); //undefined
var tt = 'dd';
alert(tt); //dd
} 

虽然以上代码片段能得到相同的结果,但强烈不建议这样编写。这种习惯不好,且代码难以维护。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章