之所以说是两道“变态”的面试题,因为这两类JavaScript代码在运行时不按常理出牌,究其根本原因就是:在运行的过程中,会多出一个私有的块级上下文,从而导致运行结果与常规不同。下面我们就来具体分析一下;
- 变态一
{
function foo(){}
foo = 1
}
console.log(foo);
上面这段代码在老版本浏览器(IE10以下)和在新版本浏览器中运行出来的结果是完全不同。先不管最终运行结果是什么,我们先来分析一下在新老版本中的运行步骤
- 老版本浏览器:
- 首先还是开辟一块栈内存(ECStack)供代码执行
- 接下来形成一个全局上下文EC(G),然后变量提升,声明并定义一个函数foo
- 代码执行,因为老版本浏览器不存在块级上下文,所以这里代码块中的代码也是直接在全局上下文中执行。第一句代码在变量提升时已经处理过,直接跳过执行第二句代码“foo=1”,这里foo由函数被变更为值1
- 执行console.log(foo) 输出值1
- 新版本浏览器:
- 第一步也是先开辟栈内存(ECStack)供代码执行
- 第二步形成全局上下文EC(G),然后变量提升,这里就开始出现不同了。这里有这么一套运行机制:
- ==在全局上下文中,除了函数/对象以外的大括号,如果在其它大括号(循环体、判断体、代码块等)中出现let/const/function,则会再单独形成一个块级私有上下文==
- ==如果在除函数/对象以外的其它大括号中出现function时,则变量提升只是声明不再定义==
- 第三步变量提升:依照上面的第二条规则,代码块中出现了function,所以在全局上下文中只声明一个foo,并不定义
- 第四步代码执行:依照第二步第一条规则,代码块中出现了function,所以这里会单独再形成一个私有块级上下文,而该块级上下文的上级上下文就是全局上下文。
- 私有上下文中代码执行:在新形成的块级上下文中同样也有变量提升,于是在私有上下文中函数foo又被提升了一次,并且这次是声明+定义。
- 这里开始出现变态机制:变量提升过后,当代码执行到function foo(){}时,按常理在变量提升阶段已经处理过了,这里就不需要再处理了,但是为了同时兼容ES3/ES5和ES6,这里会产生一个特殊的机制:
- ==把当前这行代码【之前】对foo的所有操作,都映射给全局一份,但是之后再对foo操作则都认为是自己私有的了==
- 在全局变量提升时foo仅仅声明没有定义,依照上面的规则:代码执行到function foo(){}之前是在块级上下文中变量提=》声明定义了foo,所以这里定义的foo会再给全局一份,此时全局的foo也就被定义了。
- 继续执行:在私有上下文中foo被赋值为1,这时则认为是操作的是私有的foo,跟全局就没有关系了。
- 而最终全局中的foo依然是一个函数。打印输出f foo(){}
将上面的代码简单改造一下,结果又会不一样了,如下我们在foo=1后面又加了一句 "function foo(){}",而最终的运行结果就变为1了。 这是因为:无论在全局上下文还是在私有块级上下文中都会进行两次变量提升,执行步骤与上面的基本一致,但是当代码执行到第二个“function foo(){}”时,依然会遵照上面的规则:“==把当前这行代码【之前】对foo的所有操作,都映射给全局一份==”,而当前这行代码之前则是给foo赋值为1,所以全局中的foo值也被映射为1了
{
function foo(){}
foo = 1
function foo(){}
}
console.log(foo);
- 变态二
var x = 1;
function func(x, y = function anonmymous1(){x=2}){
x = 3;
y();
console.log(x);
}
console.log(x);
这道题并不变态,按照我们正常的逻辑执行就可以了;需要注意的是:形参y的值是一个函数anonmymous1,并且是在func的上下文中创建的,所以anonmymous1的作用域就是func的上下文,在func的上下文中有变量x值由5 变为3,当执行函数y时,在函数y中并不存在变量x,所以到上级上下为中寻找,也就是到func的上下文中查找,发现有变量x,然后将其值改为2,所以在func里输出x的值为2。而全局变量x的值仍为1 但是下面我们把上述代码稍作改动,立马就变成了一道变态题:我们只需要在func中的x=3前加一个var,运行机制立马就不一样了。看下面:
var x = 1;
function func(x, y = function anonmymous1(){x=2}){
var x = 3;
y();
console.log(x);
}
console.log(x);
仅仅加了个var就造成了不同的结果,这是因为在ECMA中有如下规范:
- 如果函数中定义了形参,并且给 ==形参设置了默认值==(不管值为啥,也不管是否传递了实参)
- ==并且在函数体中出现了基于let/var/const等声明的变量==(let和const声明的变量不能与形参同名,否则会报错)
- 这样在函数执行时,除了函数本身会形成 ==一个私有上下文== 外,还会基于函数体中的代码形成一个 ==“全新的块级上下文”==,这个块级上下文的作用域就是函数形成是私有上下文,==并且函数中的代码不再执行,而是拿到新的块级上下文中去执行==。
- 如果块级上下文中声明的变量和函数私有上下文中的形参名称一致,则在块级上下文执行代码之前,会把形参变量值同步给块级上下文中的变量。
下面我们来初步分析:
- 首先还是开辟栈内存(ECStack)供代码执行
- 形成全局上下文EC(G);变量提升,声明变量x,声明并定义函数func
- 全局上下文中代码执行:x赋值为1,调用函数func
- 函数func执行:形成一个私有上下文EC(FUNC)
- 在私有上下文中:初始化作用域链<EC(FUNC), EC(G)>
- 初始化this
- 形参赋值,x赋值为5;创建函数anonymous1(作用域为:当前上下文EC(FUNC))
- 该函数使命到此结束,同时形成一个新的私有块级上下文EC(B)(作用域为:当前上下文EC(FUNC))
- 本来函数中的代码都将会拿到新的块级上下文EC(B)中执行;在块级上下文中同样要初始化作用域链,变量提升(声明私有变量x,同时将上级上下文中x的值同步过来),然后是代码执行
- 块级上下文中代码执行:x赋值为3,调用函数y,此时y并不是当前上下文中的变量,于是向上级上下文EC(FUNC)中查找
- 函数y(anonymous1)执行:形成私有上下文,发现在y形成的私有上下文中并不存在变量x,同样向上级上下文EC(FUNC)中查找,并将上级上下文中的x值改为2。
- 函数y执行结束,内存释放,重新回到块级上下文EC(B)中,执行代码“console.log(x);”,发现x就是当前上下文中的私有变量(值为3)于是直接输出结果3.
- 函数执行结束,回到全局上下文,整个过程中全局上下文中的x并未受到影响,所以还是原来的值1