当前位置: 首页 > Web前端 > HTML

深入理解JavaScript作用域和作用域链

时间:2023-04-02 17:58:08 HTML

前言JavaScript有一个特性叫做作用域(Scope)。虽然对于很多开发新手来说,作用域的概念并不是很容易理解。在这篇文章中,我会尽量用最简单的方式来解释作用域和作用域链。希望你有所收获!想阅读更多优质文章,请戳GitHub博客作用域(Scope)1.什么是作用域Scope是运行时代码中某些部分变量、函数和对象的可访问性。换句话说,范围决定了代码块中变量和其他资源的可见性。可能这两句话不好理解,先看一个例子:functionoutFun2(){varinVariable="innervariable2";}outFun2();//必须先执行这个函数,否则不执行知道里面有什么什么是console.log(inVariable);//UncaughtReferenceError:inVariableisnotdefined从上面的例子,你可以理解作用域的概念。变量inVariable没有声明在全局范围内,所以在全局范围内取值会报错。我们可以这样理解:作用域是一个独立的站点,这样变量就不会泄露或暴露。也就是说作用域最大的用处就是隔离变量,不同作用域的同名变量不会发生冲突。在ES6之前,JavaScript没有块级作用域,只有全局作用域和函数作用域。ES6的到来为我们提供了“块级范围”,这可以通过添加命令let和const来体现。2.全局作用域和函数作用域代码中任何地方都可以访问的对象,都具有全局作用域。一般来说,以下几种情况具有全局作用域:最外层函数和定义在最外层函数外的变量具有全局作用域varoutVariable="我是最外层变量";//最外层变量functionoutFun(){//最外层函数varinVariable="Innervariable";functioninnerFun(){//内部函数console.log(inVariable);}innerFun();}console.log(outVariable);//我是最外层变量outFun();//内部变量console.log(inVariable);//inVariable未定义innerFun();//innerFun未定义所有未定义直接赋值的变量都自动声明为具有全局作用域函数outFun2(){variable="undefineddirectassignmentvariable";varinVariable2="innervariable2";}outFun2();//必须先执行这个函数,否则不知道里面是什么console.log(variable);//未定义直接赋值变量console.log(inVariable2);//inVariable2未定义window对象的所有属性都具有全局作用域。一般来说,window对象的内置属性都是全局作用域的,比如window.name、window.location、window.top等。全局作用域有一个缺点:如果我们写了很多行JS代码,函数中没有包含变量定义,那么它们都在全局作用域中。这样会污染全局命名空间,容易造成命名冲突。//张三写的代码中,vardata={a:100}//李四写的代码中,vardata={x:true}这就是为什么jQuery、Zepto等库的源码会放在(function(){....})()中。因为放在里面的所有变量都不会泄露和暴露,不会污染外面,也不会影响其他库或JS脚本。这是功能范围的体现。函数作用域是指在函数内部声明的变量。与全局作用域相反,局部作用域通常只能在固定代码段内访问,最常见的是在函数内部。functiondoSomething(){varblogName="乘风破浪";函数innerSay(){警报(博客名称);}innerSay();}alert(blogName);//脚本错误innerSay();//scripterrorscope是有层次的,内层作用域可以访问外层作用域的变量,反之则不行。让我们看一个例子。用气泡比喻作用域可能更容易理解:最终输出结果为2、4、12。气泡1为全局作用域,标识符为foo;气泡2是作用域foo并具有标识符Symbolsa、bar、b;气泡3是范围栏,只有标识符c。值得注意的是,块语句(花括号“{}”之间的语句),例如if和switch条件语句或for和while循环语句,与函数不同,不会创建新的作用域。在块语句中定义的变量将保留在它们已经存在的范围内。if(true){//'if'条件块不创建新作用域varname='Hammad';//name仍在全局范围内}console.log(name);//logs'Hammad'JS初学者通常需要时间来习惯变量提升,不理解这种特殊行为会导致错误。正因如此,ES6引入了块级作用域,让变量的生命周期更加可控。3、块级作用域块级作用域可以通过新的命令let和const来声明,声明的变量在指定块的作用域外是不能访问的。当let声明的语法与函数内代码块(由一对大括号括起来)中的var语法匹配时,将创建块级作用域。您基本上可以使用let而不是var进行变量声明,但它会将变量的范围限制在当前代码块内。块级作用域具有以下特点:声明变量不会被提升到代码块的顶部;let/const声明不会被提升到当前代码块的顶部,因此您需要手动将let/const声明放在顶部,以便变量在整个代码块中可用。functiongetValue(condition){if(condition){letvalue="blue";returnvalue;}else{//这里没有valuereturnnull;}//这里没有value}如果一个If,则禁止重复语句标识符已经在代码块内定义,在此代码块内对let声明使用相同的标识符将导致抛出错误。例子:varcount=30;letcount=40;//UncaughtSyntaxError:Identifier'count'hasalreadybeendeclared在此示例中,count变量被声明了两次:一次使用var,一次使用let。因为let不能在同一个范围内重新声明一个已经存在的标识符,所以这里的let声明会抛出一个错误。但是如果你使用let在嵌套范围内声明一个同名的新变量,则不会抛出错误。varcount=30;//不会抛出错误if(condition){letcount=40;//othercode}循环中绑定块作用域的魔力开发者可能最想实现for的块级作用域loop是的,因为声明的计数器变量可以限制在循环中,例如:for(leti=0;i<10;i++){//...}console.log(i);//ReferenceError:iisnotdefined上面代码中,计数器i只在for循环体内有效,在循环外引用会报错。var=[];对于(vari=0;i<10;i++){a[i]=function(){console.log(i);};}a[6]();//10上面的代码中,变量i是通过var命令声明的,全局有效,所以全局只有一个变量i。每次循环,变量i的值都会变化,循环中函数里面的console.log(i)赋值给数组a,里面的i指向全局的i。也就是说,数组a的所有成员中的i指向同一个i,导致运行时输出最后一轮i的值,即10。如果使用let,则声明的变量为只在块级范围内有效,最终输出为6。vara=[];for(leti=0;i<10;i++){a[i]=function(){console.log(i);};}a[6]();//6上面的代码中,变量i是通过let声明的,而当前的i只在当前循环中有效,所以每次循环中的i其实都是一个新的变量,所以最后输出的是6。大家可能会问,如果每次循环都重新声明变量i,它怎么知道上一个循环的值来计算当前循环的值呢?这是因为JavaScript引擎内部会记住上一个循环的值,在初始化当前循环的变量i时,是在上一个循环的基础上计算的。此外,for循环还有一个特殊之处,就是设置循环变量的部分是一个父作用域,而循环体内是一个单独的子作用域。for(leti=0;i<3;i++){让i='abc';console.log(i);}//abc//abc//abc上面代码运行正确,输出了3次abc。这说明函数内部的变量i和循环变量i不在同一个作用域内,有各自独立的作用域。作用域链1.什么是自由变量首先我们来了解一下什么叫自由变量。在下面的代码中,console.log(a)需要获取一个变量,但是a在当前范围内没有定义(比较b)。当前范围内未定义的变量,this成为自由变量。如何获取自由变量的值——从父作用域中寻找(注意:这种说法并不严谨,下面会解释)。vara=100functionfn(){varb=200console.log(a)//这里的a是一个自由变量console.log(b)}fn()2.如果parent也不是,作用域链是什么?然后一层层往上找,直到全局作用域都找到或者没有找到,然后放弃。这种逐层关系就是作用域链。vara=100functionF1(){varb=200functionF2(){varc=300console.log(a)//自由变量,沿着作用域链到父作用域找到console.log(b)//free变量,沿着作用域链找到console.log(c)//这个作用域内的变量}F2()}F1()3.关于自由变量的值关于自由变量的值,上面有提到得到它来自父作用域,实际上,有时这种解释会引起歧义。varx=10functionfn(){console.log(x)}functionshow(f){varx=20(function(){f()//10,not20})()}show(fn)in在fn函数中,取自由变量x的值时,应该在哪个范围内取值?——要取自创建fn函数的作用域,不管fn函数会在什么地方被调用。所以,停止使用上面的语句。相比之下,用这句话来形容会更合适:**转到创建这个函数的域。范围内的值,这里强调的是“创建”而不是“调用”**,切记切记——其实这就是所谓的“静态作用域”vara=10functionfn(){varb=20functionbar(){console.log(a+b)//30}returnbar}varx=fn(),b=200x()//bar()fn()返回bar函数,赋值给x,执行x(),即执行bar函数的代码,当取b的值时,直接从fn的作用域中取出来,拿a的时候试图获取fn作用域中的值,但是获取不到,我们只能转向创建fn的作用域中去寻找,找到了,所以最后的结果是30scope和executioncontext,很多开发者经常混淆functionDomain和ExecutionContext的概念,误以为它们是同一个概念,其实不然。我们知道JavaScript是一种解释型语言。JavaScript的执行分为解释和执行两个阶段。这两个阶段做的事情不一样:解释阶段:词法分析语法分析作用域规则确定执行阶段:创建执行上下文执行函数代码垃圾回收JavaScript解释阶段会确定作用域规则,所以作用域在定义函数的时候就确定了,不是在调用函数时,而是在执行函数之前创建执行上下文。关于执行上下文最明显的一点是,this的要点是在执行时确定的。作用域访问的变量由编写代码的结构决定。scope和executioncontext最大的区别是:executioncontext是在运行时确定的,随时可能改变;范围在定义时确定并且不会改变。一个范围可能包含多个上下文。可能从来没有上下文(函数从未被调用过);可能有过,现在调用函数后上下文被销毁;可能同时有一个或多个(关闭)。在同一个作用域下,不同的调用会产生不同的执行上下文,进而产生不同的变量值。向大家推荐一款好用的BUG监控工具Fundebug,欢迎免费试用!欢迎关注公众号:前端工匠,让我们一起见证你的成长!如果觉得有所收获,欢迎打赏鼓励我输出更多优质的开源内容参考文章和书籍深入理解javascript原型和闭包系列Web前端面试指南和高-频率试题分析深入理解JS中的语句提升和作用Scope(chain)和this关键字JavaScript高级开发:理解JavaScript作用域和作用域链JavaScript作用域和作用域链深入理解ES6