一、函数参数默认值中模糊的独立作用域

我在ES6入门学习函数拓展这一篇博客中有记录,当函数的参数使用默认值时,参数会在初始化过程中产生一个独立的作用域,初始化完成作用域会消失;如果不使用参数默认值,不会产生这个作用域;之所以要写这篇博客是因为对这段代码有所疑问:

var x = 1;
function foo(x, y = function () {x = 2;}) {
var x = 3;
y();
console.log(x);
};
foo();//
foo(4);//
console.log(x);//

老实说,ES6入门中关于这个独立作用域的描述十分抽象,当我的同事对于这个问题也提出疑问时,我发现自己确实不能很好的解释这个问题,原因很简单,我也似懂非懂;对此我做了一些测试,并尝试去模拟实现这个作用域,便于解释给同事听以及说服我自己。

为什么var x=3始终输出3,为什么去掉var后始终输出2,这个独立的作用域到底是怎么回事?

如果你对于这个问题了如指掌,相关笔试题轻松解答,这篇文章就不那么重要了;但如果你对这个作用域跟我一样有一些疑虑,那可以跟着我的思路来理一理,那么本文开始。

二、ES6带来的块级作用域

在改写这段代码前,有必要先把块级作用域说清楚。

我们都知道,在ES6之前JavaScript只存在全局作用域与函数作用域这两类,更有趣的是当我们使用var去声明一个变量或者一个函数,本质上是在往window对象上新增属性:

var name = "听风是风";
var age = 26;
window.name; //'听风是风'
window.age; //

这自然是不太好的做法,我们本想声明几个变量,结果原本干净的window对象被弄的一团糟,为了让变量声明与window对象不再有牵连,也是弥补变量提升等一些缺陷,ES6正式引入了let声明。

delete window.name;
let name = "听风是风";
window.name; //undefined

let还带来了一个比较重要的概念,块级作用域,当我们在一个花括号中使用let去声明一个变量,这个花括号就是一个块级作用域,块级作用域外无权访问这个变量。

{
let x = 1;
}
console.log(x)//报错,x未声明

当你在这个块级作用域外层再次声明x时,外层作用域中的x与块级作用域中的x就是不同的两个x了,互不影响:

let x = 2;
{
let x = 1;
console.log(x); //
}
console.log(x) // var y = 1;
{
let y = 2
}
console.log(y) //

但你不可以在同层作用域中使用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); //
x = 2;
let y = 2;
console.log(y); //
}
console.log(x); //
console.log(y);//
}

上述代码中子作用域中没let x,父作用域还是允许子作用域中访问修改自己的x;父子作用域中都let y,那两个作用域中的y就是完全不相关的变量。

最后一点,很多概念都说,外(上)层作用域是无权访问块级作用域的变量,这句话其实有歧义,准确来说,是无权访问块级作用域中使用了let的变量,我的同事就误会了这点:

{
let x = 1;
var y = 2;
z = 3;
}
console.log(y);//
console.log(z);//
console.log(x);//报错,x未定义

let x确实产生了一个块级作用域,但你只能限制外层访问产生块级作用域的x,我y用的var,z直接就全局,你们抓周树人跟我鲁迅有什么关系?这点千万要理解清楚。

介绍let可能花了点时间,明明是介绍函数参数默认值的作用域,怎么聊到let了。这是因为我在给同事说我的推测时,我发现他对于let存在部分误解,所以在理解我的思路上也花了一些时间。

 三、关于函数参数默认值独立作用域的推测与我的代码模拟思路

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(); //
foo(4); //
console.log(x); //

带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(); //
foo(4); //
console.log(x); //

这是执行效果图,仔细观察可以发现scope变化以及执行结果与没改之前一样,只是我觉得这样改写更为严谨。

四、最终结论与个人推测

所以我得到的最终结论是,并不是函数形参使用了默认值会产生独立的作用域,而是函数形参使用了默认值时,会让函数体内的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);//
};
fn();

3.当函数形参声明了x,函数体内未使用var或者let去声明x,函数体内可以直接修改和使用参数x的,此时共用的是同一个变量x,块级作用域也存在作用域链。

var x =2;
function fn(y = x){
x =3;
console.log(y);//
};
fn();
x//

4.当函数形参未声明x,但是参数内又有参数默认值使用了x,此时会从全局作用域继承x。

var x = 1;
function fn(y=x){
console.log(y);//
};
fn();

那么到这里,我大概模拟了函数参数默认值时产生独立作用域的过程,同时按照我的理解去解释了它。也许我的推测与底层代码实现有所偏差,但是这个模拟过程能够很直观的去推测正确的执行结果。

我写这篇文章也是为了两个目的,第一如果在面试中遇到,我能更好的解释它,而不是似懂非懂;其次,在日常开发中使用函数参数默认值时,我能更清晰的写出符合我预期结果的代码,此时的你应该也能做到这两点了。

本文中所有的代码都是可测的,若有问题,或者更好的推测欢迎留言讨论。

那么就写到这里了,端午节快乐!

ES6函数参数默认值作用域的模拟原理实现与个人的一些推测的更多相关文章

  1. Python函数参数默认值的陷阱和原理深究(转)

    add by zhj: 在Python文档中清楚的说明了默认参数是怎么工作的,如下 "Default parameter values are evaluated when the func ...

  2. Python函数参数默认值的陷阱和原理深究"

    本文将介绍使用mutable对象作为Python函数参数默认值潜在的危害,以及其实现原理和设计目的 本博客已经迁移至: http://cenalulu.github.io/ 本篇博文已经迁移,阅读全文 ...

  3. ES6学习 --函数参数默认值与解构赋值默认值

    1. ES6的解构ES6中引入了解构赋值的操作,其作用是:将值从数组Array或属性从对象Object提取到不同的变量中 即分为两种情况:从数组Array中解构,以及从对象Object中解构 ①.从数 ...

  4. ES6 - 函数扩展(函数参数默认值)

    函数参数默认值 ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法. function log(x, y) { y = y || 'World'; console.log(x, y); ...

  5. java函数参数默认值

    java函数参数默认值 今天,需要设定java函数参数的默认值,发现按照其它语言中的方法行不通 java中似乎只能通过函数的重载来实现 函数参数默认代码

  6. 【C#基础概念】函数参数默认值和指定传参和方法参数

    函数参数默认值和指定传参 最近在编写代码时发现介绍C#参数默认值不能像PL/SQL那样直接设置default,网上也没有太多详细的资料,自己琢磨并试验后整理成果如下: C#允许在函数声明部分定义默认值 ...

  7. ES6 学习笔记之三 函数参数默认值

    定义函数时为参数指定默认值的能力,是现代动态编程语言的标配.在ES6出现之前,JavaScript是没有这种能力的,框架为了实现参数默认值,用了很多技巧. ES6 的默认参数值功能,与其他语言的语法类 ...

  8. ES6中函数参数默认值问题

    参数默认值 // 以前的参数默认值写法 let fn = (a, b) => { a = typeof a === "undefined" ? 10 : a b = type ...

  9. python函数参数默认值及重要警告

    最有用的形式是对一个或多个参数指定一个默认值.这样创建的函数,可以用比定义时允许的更少的参数调用,比如: def ask_ok(prompt, retries=4, reminder='Please ...

随机推荐

  1. extjs中新建窗体时,给窗体添加背景图片不显示问题之一

    1.在extjs中新建窗体时,给窗体添加背景图片不显示,例如下面的代码. 不显示的原因:因为设置了  layout: 'fit', Ext.create('Ext.Window', { title: ...

  2. 2018-11-13-常用模块1 (time random os sys)

    1.时间模块 time 2.随机数模块 random 3.与操作系统交互模块 os 4.系统模块 sys 在我们真正开始学习之前我们先解决下面几个问题,打好学习模块的小基础,以便更好的学习模块. (1 ...

  3. static 静态域 类域 静态方法 工厂方法 he use of the static keyword to create fields and methods that belong to the class, rather than to an instance of the class 非访问修饰符

    总结: 1.无论一个类实例化多少对象,它的静态变量只有一份拷贝: 静态域属于类,而非由类构造的实例化的对象,所有类的实例对象共享静态域. class Employee { private static ...

  4. HDFS HBase Solr Which one?

    从访问模式角度决策 HDFS 压缩性能最优.扫描速度最快:不支持随机访问,仅支持昂贵.复杂的文件查询 HBase适合随机访问 Solr 适合检索需求 HBase访问单个记录的时间为毫秒级别,而HDFS ...

  5. Hadoop实战-Flume之Source interceptor(十一)(2017-05-16 22:40)

    a1.sources = r1 a1.sinks = k1 a1.channels = c1 # Describe/configure the source a1.sources.r1.type = ...

  6. Android笔记之获取显示器宽高

    原先的Display.getWidth().Display.getHeight()已废弃 推荐的获取Display宽高的方法如下 DisplayMetrics metrics = new Displa ...

  7. Linux随笔-鸟哥Linux服务器篇学习总结(全)

    作者:Danbo 时间:2015-7-17 在runlevel3启动级别下默认启动网络挂载(autofs)机制,我们可以通过命令将其关闭:chkconfig autofs off 或者 /etc/in ...

  8. struts2 session登录

    session:记录于服务器端的信息,当客户端传来信息时候,判断是不是指定的信息. 常见应用:判断用户是否登录. struts具体的实现不写了,写主要的. 在action的方法中加入: ActionC ...

  9. LaTeX常用的符号

    推荐新手使用的网站:http://latex.codecogs.com/eqneditor/editor.php \(\sum _{d|n}{u(d)F(\frac{n}{d})}\) \sum _{ ...

  10. 日志的打印 —— Java 支持

    1. java.util.logging.Logger 日志级别(logLevel) OFF,Integer.MAX_VALUE SEVERE,1000 WARNING,900 INFO,800 CO ...