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

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

我在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

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

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

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

二、ES6带来的块级作用域

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

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

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

这自然是不太好的做法,我们本想声明几个变量,结果原本干净的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); //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存在部分误解,所以在理解我的思路上也花了一些时间。

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

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变化以及执行结果与没改之前一样,只是我觉得这样改写更为严谨。

四、最终结论与个人推测

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

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

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

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

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

原文地址:https://www.cnblogs.com/echolun/p/10983436.html

时间: 2024-10-12 12:56:17

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

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

add by zhj: 在Python文档中清楚的说明了默认参数是怎么工作的,如下 "Default parameter values are evaluated when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used

C# 函数参数默认值

namespace 函数参数默认值 { class Program { public static void Test(int i =100) { Console.WriteLine("{0}",i); } static void Main(string[] args) { Test(); Test(222); Console.Read(); } } } 输出:100 222

java函数参数默认值

java通过函数的重载来实现函数参数默认值 public class ParameterDefault { /** * @param args */ public String getName(String givenName,String familyName){ return givenName+"."+familyName; } public String getName(String givenName){ return getName(givenName,"Xie&

js设置函数参数默认值的3种方法

js默认是不支持funtion f($a=a){}这种写法的,如果想设置函数参数的默认值,可以参考如下三种方法: 第一种方法: 使用arguments,函数所有的参数都会存入arguments数组离去,所以我们可以从中获取相应的参数然后赋值 function example(a,b){ var a = arguments[0] ? arguments[0] : 1;//设置参数a的默认值为1 var b = arguments[1] ? arguments[1] : 2;//设置参数b的默认值为

3.函数参数默认值

//默认参数要指定放在右边 1 #include <iostream> 2 #include <cstdarg> 3 using namespace std; 4 5 int add(int a,int b,int c = 1) 6 { 7 return a + b + c; 8 } 9 10 void main() 11 { 12 cout << add(1, 2); 13 14 } 原文地址:https://www.cnblogs.com/xiaochi/p/854

ES6 函数参数的默认值

基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采取变通的方法. function log(x,y){ y = y||'world'; console.log(x,y); } log('kkk');//kkk world 这种写法的缺点在于: 如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用.如果在调用函数的时候,传入的y参数是一个空字符串,那么y就会被修改为默认值. 避免这个问题,需要先判断一下:1.通过判断值是否等于undefined,2.判断arguments.

ES6笔记之参数默认值(译)

原文链接:http://dmitrysoshnikov.com/ 原文作者:Dmitry Soshnikov 译者做了少量补充.这样的的文字是译者加的,可以选择忽略. 在这个简短的笔记中我们聊一聊ES6的又一特性:带默认值的函数参数.正如我们即将看到的,有些较为微妙的CASE. ES5及以下手动处理默认值 在ES6默认值特性出现前,手动处理默认值有几种方式: function log(message, level) { level = level || 'warning'; console.lo

38 py改变函数参数的值关键字参数和参数默认值函数中可变参数将序列中的元素值作为函数对应的参数值传

第五课:改变函数参数的值 一个python函数可以有任意多个参数,在一个函数的外部来定义变量,然后把变量作为参数传入到函数内,并且在函数的内部来修改函数的参数值,函数结束之后,这些变量的值在如何变化呢? 给函数传递值有2种:1种是值传递,1种是引用传递 # 改变函数参数的值 # 值传递(数值.字符串.布尔 这些都是值传递) 在函数的内部修改变量值,不改变原参数定义的参数值,解释为: 这个在函数中不会改变原来定义(函数外部)的值 这是因为函数里面会有一个占的概念,外边的变量的值会复制给 占 里面,

C++函数带默认值的几种情形

1. 默认参数的规则 默认值定义 C++支持函数参数带默认值.这里所谓的默认值,指的是在调用函数时,可以不指定某些参数,编译器会自动把默认值传递到函数调用中. 默认值位置 指定默认值的参数必须位于形参列表最右端,从右往左.否则会出现二义性,编译器无法处理. 默认值设置 默认值可以在函数声明或函数定义中设置,只要保证不出现参数重定义即可. 2.默认参数示例 在函数声明中设置默认值 这是最常见的一种情形.主要有以下两种声明方式: 声明中给参数赋默认值 声明中用类型指定默认值 float area(f