function demo(a){
console.log(a);
var a = 1;
function a(){
console.log('222');
}
console.log(a);
}
demo(2);
运行后可知,第一个 console 输出的是函数声明“function a”的函数体,第二个 console 输出的是 1。
为什么会这样?
很多人会用“变量提升和函数提升,函数提升优于变量提升”来解答,这么说好像解释了问题,但是好像又没有解释清楚“为什么”。而且“函数提升优于变量提升”这个说法,好像也有点模棱两可,到底是怎么个“优于”法呢?
执行上下文
要解释这个为什么,还得回到更基础的概念 —— 执行上下文。
一个函数在执行之前,先要创建执行上下文,执行上下文会做这么几件事(包括但不限于):
- 确定作用域链
- 创建变量对象(variable object)
- 确定 this
其中所谓的“变量提升和函数提升”都发生在“创建变量对象”这里。
创建变量对象
js 在创建变量对象的时候,会进行三个步骤:
- 把函数的参数声明放到变量对象中
- 遍历函数体内的函数声明,把函数声明放到变量对象中,这一步也即是所谓的“函数提升”,注意,执行这一步的时候,会直接把变量对象中函数名的指针指向函数声明的函数体,如果存在同名参数,会被函数体覆盖。而如果函数体内存在多个同名的函数声明,会采用最后一个。
- 遍历函数体内的变量声明,如果变量对象中已存在该变量名,则跳过。注意,提升的只是变量声明,而不包含变量的赋值操作。变量的赋值操作将会在函数执行阶段才进行。
结论
在前面的代码里,当我们运行 demo(2) 的时候,js 会在创建变量对象的时候是这样进行的:
- 将参数 a 放入变量对象,将值设置为 2。
- 扫描 demo 的函数体,发现函数声明 function a,变量对象中已存在 a,将 a 指针指向 function a 的函数体。
- 扫描 demo 的函数体,发现变量声明 var a,变量对象中已存在 a,跳过。
此时 a 的值是 function a 的函数体。
下面进入到执行阶段。
- 执行第一个 console.log(a) ,输出 function a 的函数体。
- 执行 a = 1 的赋值操作,将 a 的值改写为 1。
- 跳过函数声明 function a。
- 执行第二个 console.log(a),输出 1。
补充
从这个面试题我们可以领悟到什么?
1、不要用 var 声明变量,如果把代码中的 var 改成 let ,因为 let 不允许重复声明,js 将直接抛出语法错误。即使没有语法错误,let 声明的变量在声明之前被访问,也会因为触发“暂时性死区”而抛出引用错误。问题就可以化繁为简,代码也会少很多 bug。
2、参数名、变量名、函数名不要太随意,多用 eslint 之类的工具来检查是否存在重复声明、shadow name 等问题。
3、关于函数表达式和函数声明的取舍,个人建议,在不需要函数提升特性的情况下,应优先采用函数表达式,这样代码的书写顺序和执行顺序是一致的,更容易理解
转载于:王峰
链接:https://www.zhihu.com/question/489790136/answer/2148247810
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。