1、函数参数默认值中模糊的独立做用域面试
我在ES6入门学习函数拓展这一篇博客中有记录,当函数的参数使用默认值时,参数会在初始化过程当中产生一个独立的做用域,初始化完成做用域会消失;若是不使用参数默认值,不会产生这个做用域;之因此要写这篇博客是由于对这段代码有所疑问:函数
var x = 1; function foo(x, y = function () {x = 2;}) { var x = 3; y(); console.log(x); }; foo();//3 foo(4);//3 console.log(x);//1
老实说,ES6入门中关于这个独立做用域的描述十分抽象,当个人同事对于这个问题也提出疑问时,我发现本身确实不能很好的解释这个问题,缘由很简单,我也似懂非懂;对此我作了一些测试,并尝试去模拟实现这个做用域,便于解释给同事听以及说服我本身。学习
为何var x=3始终输出3,为何去掉var后始终输出2,这个独立的做用域究竟是怎么回事?测试
若是你对于这个问题了如指掌,相关笔试题轻松解答,这篇文章就不那么重要了;但若是你对这个做用域跟我同样有一些疑虑,那能够跟着个人思路来理一理,那么本文开始。spa
2、ES6带来的块级做用域code
在改写这段代码前,有必要先把块级做用域说清楚。对象
咱们都知道,在ES6以前JavaScript只存在全局做用域与函数做用域这两类,更有趣的是当咱们使用var去声明一个变量或者一个函数,本质上是在往window对象上新增属性:blog
var name = "听风是风"; var age = 26; window.name; //'听风是风' window.age; //26
这天然是不太好的作法,咱们本想声明几个变量,结果本来干净的window对象被弄的一团糟,为了让变量声明与window对象再也不有牵连,也是弥补变量提高等一些缺陷,ES6正式引入了let声明。继承
delete window.name; let name = "听风是风"; window.name; //undefined
let还带来了一个比较重要的概念,块级做用域,当咱们在一个花括号中使用let去声明一个变量,这个花括号就是一个块级做用域,块级做用域外无权访问这个变量。ip
{ let x = 1; } console.log(x)//报错,x未声明
当你在这个块级做用域外层再次声明x时,外层做用域中的x与块级做用域中的x就是不一样的两个x了,互不影响:
let x = 2; { let x = 1; console.log(x); //1 } console.log(x) //2 var y = 1; { let y = 2 } console.log(y) //1
但你不能够在同层做用域中使用let声明一个变量后再次var 或者再次let相同变量:
let x = 1; var x; //报错,x已声明 let y = 1; let y; //报错,y已声明 var z = 1; let z; //报错,z已声明
块级做用域依旧存在做用域链,并非说你变成了块级做用域就六亲不认了,谁也别想用我块级里面的变量:
{ //父做用域 let x = 1; let y = 1; { //子做用域 console.log(x); //1 x = 2; let y = 2; console.log(y); //2 } console.log(x); //2 console.log(y);//1 }
上述代码中子做用域中没let x,父做用域仍是容许子做用域中访问修改本身的x;父子做用域中都let y,那两个做用域中的y就是彻底不相关的变量。
最后一点,不少概念都说,外(上)层做用域是无权访问块级做用域的变量,这句话其实有歧义,准确来讲,是无权访问块级做用域中使用了let的变量,个人同事就误会了这点:
{ let x = 1; var y = 2; z = 3; } console.log(y);//2 console.log(z);//3 console.log(x);//报错,x未定义
let x确实产生了一个块级做用域,但你只能限制外层访问产生块级做用域的x,我y用的var,z直接就全局,大家抓周树人跟我鲁迅有什么关系?这点千万要理解清楚。
介绍let可能花了点时间,明明是介绍函数参数默认值的做用域,怎么聊到let了。这是由于我在给同事说个人推测时,我发现他对于let存在部分误解,因此在理解个人思路上也花了一些时间。
3、关于函数参数默认值独立做用域的推测与个人代码模拟思路
1.改写函数参数
咱们都知道,函数的参数其实等同于在函数内部声明了一个局部变量,只是这个变量在函数调用时能与传递的参数一一对应进行赋值:
function fn(x) { console.log(x); }; fn(1); //等用于 function fn() { //函数内部声明了一个变量,传递的值会赋予给它 var x = 1; }; fn()
因此第一步,我将文章开头那段代码中的函数进行改写,将形参改写进函数内部:
function foo() { var x; var y = function () { x = 2; }; var x = 3; y(); console.log(x); };
2.模拟形参的独立做用域
改写后有个问题,此时形参与函数内部代码处于同一层做用域,这与咱们得知的概念不太相符,概念传达的意思是,函数参数使用默认值,会拥有独立的做用域,因此咱们用一个花括号将函数内代码隔离起来:
function foo() { var x; var y = function () { x = 2; }; { var x = 3; y(); console.log(x); } };
其次,由文章开头的代码结果咱们已经得知,var x =3这一行代码,若是带了var ,函数体内x变量就与参数内的x互不影响了,永远输出3;若是把var去掉呢,就能继承并修改参数中的变量x了,此时x始终输出2,这个效果能够本身复制文章开头的原代码测试。
我在上文介绍let块级做用域时有提到块级做用域也是有做用域链的;父子块级做用域,若是子做用域本身let一个父做用域已声明的变量,那么二者就互不影响,若是子不声明这个变量,仍是能够继承使用和修改父做用域的此变量。这个状况不就是示例代码的除去var和不除去var效果吗,只是咱们还缺个块级做用域才能知足这个条件,因此我将var x =3前面的var修改为了let,整个代码修改完毕:
function foo() { //父做用域 var x; var y = function () { x = 2; }; { // 子块级做用域 let x = 3; y(); console.log(x); } };
你确定要问,我为何要把var改成let?并非我根据结论强行倒推理,我在断点时发现了一个问题,带var的状况:
注意观察右边Scope的变化,当断点跑到var x = 3时,显示在block(块级做用域)下x是undefined,而后被赋值成了3,最后断点跑到console时,也是输出了block做用域下的x,并且在block做用域和local做用域中分别存在2个变量x,以下图:
函数内部明明没用let,也就是说,函数执行时,隐性建立了一个块级做用域包裹住了函数体内代码。当我把var去掉时,再看截图:
能够看到,当去掉var时,整个代码执行完,全程都不存在block做用域,并且从头至尾都只有local做用域下的一个x。
由此我推断var是产生块级做用域的缘由,因此将x变量前的var改成了let。
3.模拟代码测试阶段:
咱们最终修改后的代码就是这样:
var x = 1; function foo() { var x; var y = function () { x = 2; }; { let x = 3; y(); console.log(x); } }; foo(); //3 foo(4); //3 console.log(x); //1
带var分别输出3 3 1,咱们把var 改为了let,也是输出3 3 1。去var输出2 2 1,咱们把let去掉也是输出2 2 1,效果如出一辙。
咱们对比了修改先后,代码执行时scope的变化,是如出一辙的,能够说模拟还算成功。
4.最终模拟版本
而后我又发现了一个改写的大问题:
function fn(x=x){ }; fn();//报错
这段代码是会报错的,它会提示你,x未声明就使用了,这是let声明常见的错误。可是若是按照我前面说的将形参移到函数体内用var声明,那就不会报错了:
function fn(){ var x = x; }; fn()//不报错 function fn(){ let x = x; }; fn()//报错
因此我上面的初始代码改写后的最终版本是这样:
var x = 1; function foo() { let x; let y = function () { x = 2; }; { let x = 3; y(); console.log(x); } }; foo(); //3 foo(4); //3 console.log(x); //1
这是执行效果图,仔细观察能够发现scope变化以及执行结果与没改以前同样,只是我以为这样改写更为严谨。
4、最终结论与我的推测
因此我获得的最终结论是,并非函数形参使用了默认值会产生独立的做用域,而是函数形参使用了默认值时,会让函数体内的var声明隐性产生一个块级做用域,从而变相致使了函数参数所在做用域被隔离。不使用参数默认值或函数体内不使用var声明不会产生此做用域。
个人改写模拟思路是这样:
第一步,形参若是用了默认值,将形参移到函数体内并用let声明它们;
第二步,若是此时没报错,再用花括号将本来的函数体代码包裹起来,再将花括号中的var声明修改为let声明。
function fn(x, y = x) { let x = 1; console.log(x); }; //第一步: function fn() { let x; let y = x; let x = 1; console.log(x); };
好比上述这段代码,形参移动到函数体内其实你就已经会报错了,x变量被反复申明了,因此就不必再用花括号包裹执行体代码了。
我大概总结出了如下几个规律(能够按照个人思路改写,方便理解):
1.当函数形参声明了x,函数体内不能使用let再次声明x,不然会报错,缘由参照函数改写步骤1。
var x = 1; function fn(x){ let x =1;//报错 }; fn();
2.当函数形参声明了x,函数体内再次使用var声明x时,函数体内会隐性建立一个块级做用域,这个做用域会包裹执行体代码,也变相致使参数有了一个独立的做用域,此时两个x互不影响,缘由参照函数改写步骤2。
function fn(x =1){ var x =2; console.log(x);//2 }; fn();
3.当函数形参声明了x,函数体内未使用var或者let去声明x,函数体内能够直接修改和使用参数x的,此时共用的是同一个变量x,块级做用域也存在做用域链。
var x =2; function fn(y = x){ x =3; console.log(y);//2 }; fn(); x//3
4.当函数形参未声明x,可是参数内又有参数默认值使用了x,此时会从全局做用域继承x。
var x = 1; function fn(y=x){ console.log(y);//1 }; fn();
那么到这里,我大概模拟了函数参数默认值时产生独立做用域的过程,同时按照个人理解去解释了它。也许个人推测与底层代码实现有所误差,可是这个模拟过程可以很直观的去推测正确的执行结果。
我写这篇文章也是为了两个目的,第一若是在面试中遇到,我能更好的解释它,而不是似懂非懂;其次,在平常开发中使用函数参数默认值时,我能更清晰的写出符合我预期结果的代码,此时的你应该也能作到这两点了。
本文中全部的代码都是可测的,如有问题,或者更好的推测欢迎留言讨论。
那么就写到这里了,端午节快乐!