当前位置: 首页 > 科技观察

还在为NotDefined苦苦挣扎吗?

时间:2023-03-19 01:59:39 科技观察

在写JavaScript语言的时候,你是不是经常看到这种错误信息*isnotdefined?是不是经常出现undefined?这些都是因为此时变量访问无效或者不可用,而限制变量可用的代码作用域就是变量的作用域。那么什么是作用域?作用域编程语言最基本的是能够将值存储在一个变量中,并且以后可以访问或修改这个值,而作用域就是变量和函数的可访问范围。作用域主要有两种工作模型,词法作用域(静态作用域)和动态作用域:词法作用域:作用域是在定义的时候就确定的,也就是在写代码的时候确定把变量写到哪里,块作用域是什么,JavaScript使用词法范围。动态作用域:作用域是在运行时确定的,比如bash和Perl。JavaScript中存在三种作用域:全局作用域:最外层的代码。函数作用域:创建一个函数就创建了一个作用域,不管你调用与否,只要创建了函数,它就有一个独立的作用域。ES6的块级作用域:ES6引入了let和const关键字以及{}的组合,使得JavaScript具有块级作用域,下面将详细介绍。全局作用域的最外层是全局作用域,在脚本的任何地方都可以访问到。具有全局作用域的变量也称为“全局变量”。看看哪些变量具有全局作用域:在浏览器中,全局作用域中有一个全局对象窗口,可以直接使用。//获取窗口的文档显示区域的高度window.innerHeight定义在最外层的函数和变量vara=1console.log(window.a)//1---var中声明的a变成awindow的属性并且是全局变量functionfunc1(){console.log('hello')}func1()//hellowindow.func1()//直接分配给hello没有关键字的变量自动声明为具有全局范围并安装在窗口对象。b=2//全局变量functionfunc1(){c=2}func1()console.log(window.b)//2console.log(window.c)//2个省略关键字的变量,不管是afunction外面的b或者函数里面的c都是全局变量,挂载在window上,但是这样省略关键字是不规范的,也不利于维护,不推荐。变量提升将上面的a代码反转如下:console.log(window.a)//输出undefinedvara=1此时声明前a是可以访问的,但是输出的是undefined,也就是常说的“变量提升”.变量提升:通过var关键字声明的变量,无论实际声明在什么地方,都会被视为在当前作用域(包括函数和全局作用域)的顶部声明,因为JS引擎的工作方式是编译和执行两个阶段:首先解析代码,获取所有声明的变量;然后运行。所以下面两段代码是等价的:console.log(a);//输出undefinedvara=1;//等价于vara;控制台日志(一);//输出undefineda=1;对于vara=1,当编译器遇到vara时,会在scope中声明一个新的变量a,然后编译器会生成引擎运行所需的代码,处理console.log(a)和a=1引擎运行时,从当前作用域集合中获取变量a(此时未定义)并赋值给a1函数作用域函数作用域内的变量或内部函数,作用域为函数作用域,为不对外开放。从外部作用域不能直接访问函数内部的作用域,否则会报引用错误异常。如下:functionfunc1(){vara=1;returna}func1()//1函数内部是可访问的console.log(a)//UncaughtReferenceError:aisnotdefinedfunctiondeclaration在函数声明中,JS引擎在代码执行之前进行函数声明,函数定义是在执行上下文中生成。console.log(add(10,10))//正常返回20functionadd(a,b){returna+b}代码正常运行,可以在任何代码执行前读取函数声明,执行上下文为添加,即函数声明提升(与上面的变量声明提升相同)。函数表达式函数表达式必须等待代码执行到该行,然后才能在执行上下文中生成函数定义console.log(add(10,10))//UncaughtTypeError:addisnotafunctionvaradd=function(a,b){returna+b函数表达式varadd=function(){}是变量声明提升。在这种情况下,add是一个变量,所以这个变量的声明也会被提升到最上面,而变量的赋值仍然在原来的位置,所以此时的错误是变量add的类型错误.函数声明和变量声明我们提到了函数声明提升和变量声明提升,以及使用现象。让我们看一个两者共同使用的例子:test()//"executefunctiondeclaration"vartest=function(){console.log('执行函数表达式')}functiontest(a,b){console.log('执行函数声明')}test()//“执行函数表达式”先test()输出“执行函数语句”,secondtest()输出“executefunctionexpression”,因为它经历了函数声明提升和变量声明提升(函数提升优先于变量提升),代码相当于://函数声明提升到最上面的函数测试(a,b){console.log('executefunctiondeclaration')}//变量提升,变量提升不会覆盖(同名)函数提升,只有变量再次赋值时,才会被覆盖vartest//还在原处test()//“执行函数声明”test=function(){console.log('执行函数表达式')}test()//“执行函数表达式”块级作用域ES6newletandconst范围是一个块级范围,由最近的分隔一对花括号{},以let为例如下:{vara=1letb=2}console.log(a)//1console.log(b)//UncaughtReferenceError:bisnotdefinedVariablesdeclared在大括号内使用let在外部是不可访问的,即块级作用域。提前访问用let关键字声明的变量时:{console.log(a)//报错UncaughtReferenceError:aisnotdefinedleta=1}之所以报上面的错误是因为let有一个“临时死区”temporaryDeadzone:在声明变量之前,变量不可用。只要进入当前作用域,要使用的变量就已经存在,只是获取不到。只有出现声明变量的代码行,才能获取和使用。多变的。上面的代码可以等价理解为:{//让a,临时死区开始的地方console.log(a)//由于a=2在临时死区,所以报错a=1//Temporary死区结束的地方}constconst和let一样,也有“临时死区”,但有如下限制:const声明变量时,必须同时初始化为某个值,并且不能重新分配。https://back-media.51cto.com/editor?id=702422/h6e90be6-0eG33HhD作为对象赋值的const变量不能赋另一个引用值,但是对象的key不受限制(Object.freeze()可以是完全Frozen对象,键值对不可修改)consta=1a=1//UncaughtTypeError:Assignmenttoconstantvariable.//Objectconstobj={a:1,b:2}obj.b=3console.log//3var,let,const区别区别区别varletconst作用域函数作用域块作用域块作用域声明同一个作用域可以声明多次同一个作用域不能声明多次同一个作用域不能声明多次必须赋值同时,后续不可改变的特性变量提升(不加var是全局变量)临时死区临时死区常见例子:for循环for(vari=0;i<5;i++){setTimeout(function(){console.log(i)})}//55555for(leti=0;i<5;i++){setTimeout(function(){console.log(i)})}//01234var:全局变量,退出循环时迭代变量保存循环退出时的值,执行超时回调时,我都是同一个变量。let:块作用域,JS引擎为每次迭代循环声明一个新变量,每次超时回调调用不同的变量实例。//遍历对象keyfor(constkeyin{a:1,b:1}){console.log(key)//ab}//遍历numberfor(constvalof[1,2,3]){console.log(val)//123}evalval由于性能不佳、不安全、代码逻辑混乱等各种问题,一般不支持在代码中使用,但还是有必要去理解一下,话说网友:你可以远离它,但是要理解它,这个方法是一个完整的ES解释器,它接收一个参数,即一个要执行的ES(JavaScript)字符串,将对应的字符串解析成JavaScript代码并运行(将json字符串解析为JSON对象)。eval的简单用法:如果参数是字符串表达式,则对表达式求值如果参数是字符串并表示一个或多个JavaScript语句,则执行这些语句如果参数不是字符串,则保持参数不变返回自动eval("2+2")//输出4eval("console.log('hi')")//输出hieval(newString("2+2"))//String{'2+2'}eval对作用域的影响JavaScript中eval的调用有两种方式:直接调用和间接调用。直接调用时:eval中代码块作用域绑定当前作用域,直接调用eval()。functiontestEval(){eval('vara=111')console.log(a)//111}testEval()console.log(a)//上面的错误在testEval函数内部可以得到,所以eval修改了testEval功能范围。间接调用时:eval中代码块作用域绑定到全局作用域,使用window.eval()(IE8兼容问题),window.execScript(支持IE8及以下版本),为了解决兼容问题,也变量可以全局分配,然后在函数内使用。//IE兼容问题functiontestEval(){window.eval('vara=111')console.log(a)//111}testEval()console.log(a)//111eval定义的变量绑定到达全局范围//解决兼容性问题varevalExp=evalfunctiontestEval(){evalExp('vara=111')console.log(a)//111}testEval()console.log(a)//111eval变量定义绑定到全局范围eval变量提升问题通过eval()定义的任何变量和函数都不会被提升,因为它们在解析代码时包含在字符串中。它们仅在执行eval()时创建。下面是let、var和functions的不同作用,如下://functionsayHi()//error:sayHiisnotdefined,nofunctiondeclarationpromotedeval("functionsayHi(){console.log('hi');}");sayHi()//hi//varmsg//错误:msg未定义,未引发变量声明eval("varmsg='helloworld'")console.log(msg)//helloworld//leteval("letmsg='helloworld';console.log(msg)")////helloworldconsole.log(msg)//报错letscopecanonlybetheevalinternalscopechainwhenablockorfunctionisnestedof作用域发生在另一个块或函数内。作用域嵌套的查询规则如下:首先,JS引擎从当前执行作用域中查找变量。然后,如果未找到,引擎将继续在外部嵌套范围中查找。最后,直到找到变量,或者到达最外层的全局范围。这种由多个作用域组成的链表称为作用域链。例如:varc=1functionfunc(){varb=2functionadd(a){returna+b+c}returnadd}constaddTest=func()addTest(3)//6个作用域链是:executefuncTest():查找add函数的作用域,检查是否有a,有则获取传入作用域的值如果没有则查找上层作用域func,检查是否有b,然后获取当前作用域的值。2此时得到b的值后,再搜索c的值。如果在add函数作用域找不到c,则查找上层作用域func,还是找不到c,再去上层作用域查找,即全局作用域,查询a,获取作用域的值1andreturn6closureifyoufind是在定义时确认的,即函数内部的变量不能在定义的函数外访问,但是在上面作用域嵌套的例子中,addTest可以访问到函数内部的变量函数func,这是因为“闭包”的存在。闭包是定义在函数内部的函数,它可以记住和访问它所在的作用域。即使函数在当前词法范围之外执行,它也可以访问内部变量。varc=1functionfunc(){varb=2functionadd(a){returna+b+c}returnadd}constaddTest=func()addTest(3)//6首先函数作用域add()可以访问func()的内部作用域来执行func,并将内部函数add的引用赋值给外部变量addTest。此时addTest指针仍然指向addadd仍然持有func作用域的引用,这个引用被称为closed。包在外部执行addTest,即在外部执行add,可以通过闭包访问定义的范围。使用闭包时,原函数func不会被回收,会被包含在add的范围内,因此会比其他函数占用更多的内存,容易造成内存泄漏。从上面可以看出闭包的使用。闭包在代码中随处可见。再来看看使用场景:回调就像上面说的let循环的例子:for(leti=0;i<5;i++){setTimeout(function(){console.log(i)})}的setTimeout的回调函数会记住当前的词法范围。当循环结束,函数执行完毕,就可以访问当时作用域中的i。模块化//获取数组中的正反序functionarrOperate(){leterrorMsg='请传入一个数组'//正序functiongetPositiveArr(arr){if(Array.isArray(arr)){returnarr.sort((a,b)=>{returna-b})}else{throwerrorMsg}}//倒序函数getBackArr(arr){if(Array.isArray(arr)){returnarr.sort((a,b)=>{returnb-a})}else{throwerrorMsg}}return{getPositiveArr,getBackArr}}constarrObj=arrOperate()arrObj.getPositiveArr([1,10,5,89,46])//[1,5,10,46,89]arrObj.getBackArr([1,10,5,89,46])//[89,46,10,5,1]arrObj.getPositiveArr(123)//请未捕获传入数组这种模式在JavaScript中称为模块,arrOperate()返回一个对象,包含对内部函数的引用,内部函数getPositiveArr()和getBackArr()函数有闭包覆盖模块实例的内部作用域,获取errorMsg。总结Scope决定了访问变量的范围,代码随处可见,了解范围,避免使用不能访问的变量,减少文章开头的错误,代码质量会直线上升。参考资料《你不知道的 JavaScript》《JaveScript 高级程序设计》陈晨,微医第一利润中心前端团队,“命在于静”的程序员。