作者:ChidumeNnamdi译者:前端小智来源:smashingmagazine喜欢再看一遍,养成习惯分类,也整理了很多我的文档,教程资料。欢迎来到星和完美。面试时可参考考点复习。我希望我们能在一起。JavaScript是一种有趣的语言,我们都喜欢它,因为它的本质。浏览器是JavaScript主要运行的地方,两者在我们的服务中协同工作。JS的概念人们往往会轻视,有时甚至会忽略。原型、闭包和事件循环等概念仍然是大多数JS开发人员绕道而行的模糊领域之一。众所周知,无知是一件危险的事情,它会导致错误。想阅读更多优质文章,请【戳GitHub博客】[1]\,一年有上百篇优质文章等着你!接下来我们来看几个问题,大家也可以试着去想一想,然后再作答。问题一:浏览器控制台会打印什么?变量a=10;函数foo(){console.log(a);//??变量a=20;}foo();问题2:如果我们使用let或const代替var,输出是否相同vara=10;functionfoo(){console.log(a);//??让一个=20;}foo();问题3:“newArray”中有哪些元素?vararray=[];for(vari=0;i<3;i++){array.push(()=>i);}varnewArray=array.map(el=>el());console.log(新数组);//??问题四:如果我们在浏览器控制台运行'foo'函数,会不会导致堆栈溢出错误?函数foo(){setTimeout(foo,0);//是否有堆栈溢出错误?};问题5:如果我在控制台运行以下功能,页面(选项卡)的UI是否仍然响应functionfoo(){returnPromise.resolve().then(foo);};问题6:我们能否以某种方式对以下语句使用展开运算符而不会导致类型错误varobj={x:1,y:2,z:3};[...obj];//TypeError问题7:运行下面的代码片段时,控制台会打印什么?varobj={a:1,b:2};Object.setPrototypeOf(obj,{c:3});Object.defineProperty(obj,'d',{value:4,enumerable:false});//什么当我们运行for-in循环时,属性会被打印出来吗?for(letpropinobj){console.log(prop);}问题8:xGetter()会打印什么值?变量x=10;varfoo={x:90,getX:函数(){返回这个.x;}};foo.getX();//打印90varxGetter=foo.getX;xGetter();//印刷??回答现在让我们从头到尾回答每个问题我会给你一个简短的解释,同时试图揭开这些行为的神秘面纱并提供一些参考。问题一:undefined解析:用var关键字声明的变量会在JavaScript中被提升,在内存中赋值为undefined。但是初始化恰好发生在你为变量赋值的地方。此外,var声明的变量是[function-scoped][2],而let和const是block-scoped。所以,这就是这个过程的样子:vara=10;//全局作用域functionfoo(){//vara的声明将被提升到函数的顶部。//例如:varaconsole.log(a);//打印undefined//实际初始化值20只发生在这里vara=20;//本地范围}-问题2:ReferenceError:未定义。解析:let和const声明允许将变量的范围限制在使用它们的块、语句或表达式中。与var不同,这些变量不会被提升,并且有一个所谓的临时死区(TDZ)。尝试访问TDZ中的这些变量将引发ReferenceError,因为只有在执行到声明时才能访问它们。变量a=10;//全局作用域functionfoo(){//TDZ开始//创建未初始化的'a'console.log(a);//ReferenceError//TDZ结束,'a'仅在此处用值20进行初始化leta=20;}下表总结了JavaScript中使用不同关键字声明的变量对应的提升行为和使用域:-问题3:[3,3,3]解析:在for在循环的头部用var关键字声明一个变量会为该变量创建一个绑定(存储空间)。阅读更多关于[闭包][3]的内容。让我们再看看for循环。//对作用域的误解:认为存在块作用域vararray=[];for(vari=0;i<3;i++){//三个箭头函数体中的每个`'i'`都指向相同的绑定,//这就是为什么它们在最后返回相同的值'3'的循环。array.push(()=>i);}varnewArray=array.map(el=>el());console.log(newArray);//[3,3,3]如果使用let在第一级范围内声明一个带有变量的块,则为每个循环迭代创建一个新的绑定。//使用ES6块作用域vararray=[];for(leti=0;i<3;i++){//这一次,每个“i”指的是一个新的绑定,并保持当前值。//因此,每个箭头函数返回不同的值。array.push(()=>i);}varnewArray=array.map(el=>el());控制台日志(新数组);//[0,1,2]解决这个问题的另一种方法方法是使用[closure][4]。让数组=[];对于(vari=0;i<3;i++){array[i]=(function(x){returnfunction(){returnx;};})(i);}constnewArray=array.map(el=>el());console.log(newArray);//[0,1,2]-问题4:没有溢出解析:JavaScript并发模型是基于“事件循环”的。当我们说“浏览器是JS的家”时,我真正的意思是浏览器提供了运行时环境来执行我们的JS代码。浏览器的主要组件包括调用堆栈、事件循环**、任务队列和WebAPI**。setTimeout、setInterval和Promise等全局函数不是JavaScript的一部分,而是WebAPI的一部分。JavaScript环境的可视化如下所示:JS调用堆栈是后进先出(LIFO)。引擎一次从堆栈中弹出一个函数,然后从上到下依次运行代码。每当遇到像setTimeout这样的异步代码时,它就会将其交给WebAPI(箭头1)。因此,无论何时触发事件,回调都会被发送到任务队列(箭头2)。事件循环(Eventloop)不断地监控任务队列(TaskQueue),并按照回调的排队顺序一次一个地处理回调。每当调用堆栈为空时,Event循环获取回调并将其放入堆栈(箭头3)以进行处理。请记住,如果调用堆栈不为空,事件循环不会将任何回调压入堆栈。现在,有了这些知识,让我们回答之前提出的问题:对foo()的步骤调用将foo函数放入调用堆栈。JS引擎在处理内部代码时遇到setTimeout。然后将foo回调函数传递给WebAPI(箭头1)并从函数返回,调用堆栈再次为空,定时器设置为0,因此foo将被发送到任务队列(箭头2)。由于调用堆栈是空的,事件循环将选择foo回调并将其压入调用堆栈以进行处理。再次重复该过程,堆栈没有溢出。运行示意图如下:\![图片说明][5]-问题5:不会响应解析:大多数时候,开发人员假设事件循环图中只有一个任务队列。但事实并非如此,我们可以有多个任务队列。浏览器选择这些队列之一,并在该队列上处理回调。在底层,JavaScript中有宏任务和微任务。setTimeout回调是宏任务,而Promise回调是微任务。主要区别在于它们的实现方式。宏任务在单个循环周期中一次一个地被压入堆栈,但微任务队列总是在执行后清空,然后返回事件循环。因此,如果您以处理条目的速度向该队列添加条目,那么您将永远处理微任务。只有当微任务队列为空时,事件循环才会重新渲染页面。现在,当您在控制台中运行以下代码片段时foo(){returnPromise.resolve().then(foo);};每次调用“foo”都会在微任务队列中继续添加另一个“foo”回调,因此事件循环无法继续处理其他事件(滚动、点击等),直到该队列完全清空。因此,它会阻止渲染。-问题六:会导致TypeError错误解析:[ExpansionSyntax][6]和[for-of][7]语句遍历iterable对象来定义要遍历的数据。Array或Map是具有默认迭代行为的内置迭代器。对象不可迭代,但可以通过使用[iterable][8]和[iterator][9]协议使它们可迭代。在Mozilla文档中,如果一个对象实现了@@iterator方法,那么该对象是可迭代的,这意味着该对象(或其原型链上的对象)必须具有带有@@iterator键的属性,键可以通过常量获得符号.迭代器。上面的语句可能看起来有点冗长,但下面的例子会更有意义:varobj={x:1,y:2,z:3};obj[Symbol.iterator]=function(){//迭代器是一个具有next方法的对象,//它返回至少一个对象//具有两个属性:value和done。//返回一个迭代器对象return{next:function(){if(this._countDown===3){constlastValue=this._countDown;返回{值:this._countDown,完成:真};}this._countDown=this._countDown+1;返回{值:this._countDown,完成:false};},_countDown:0};};[...obj];//print[1,2,3]也可以使用[generator][10]函数自定义对象的迭代行为:varobj={x:1,y:2,z:3}obj[Symbol.iterator]=function*(){yield1;产量2;产量3;}[...obj];//打印[1,2,3]-第7题:a,b,c解析:for-in循环遍历对象本身的[可枚举属性][11]和对象从其原型继承的属性。可枚举属性是可以在for-in循环中包含和访问的属性。varobj={a:1,b:2};vardescriptor=Object.getOwnPropertyDescriptor(obj,"a");console.log(descriptor.enumerable);//trueconsole.log(descriptor);//{value:1,writable:true,enumerable:true,configurable:true}现在你已经了解了这些知识,应该很容易理解为什么我们的代码会打印这些特定的属性varobj={a:1,b:2};//a,b都是可枚举属性//将{c:3}设置为'obj'的原型,我们知道//for-in循环也会迭代obj//从其原型继承的属性,'c'也可以访问。Object.setPrototypeOf(obj,{c:3});//我们在'obj'中定义了另一个属性'd',但是//将'enumerable'设置为false。这意味着'd'将被忽略。Object.defineProperty(obj,"d",{value:4,enumerable:false});for(letpropinobj){console.log(prop);}//print//a//b//c-\#\#\#\#问题8:10解析:x全局初始化时,它成为窗口对象的属性(不是在严格模式下)。看看下面的代码:varx=10;//全局作用域varfoo={x:90,getX:function(){returnthis.x;}};foo.getX();//打印90letxGetter=foo.getX;xGetter();//打印10我们可以断言:window.x===10;//truethis总是指向调用该方法的对象。所以,在foo.getx()的情况下,它指向foo对象,返回值90。在xGetter()的情况下,this指向window对象,并返回窗口中x的值,即10。要获取foo.x的值,请使用Function.prototype.bind将this的值绑定到foo对象,从而创建一个新函数。让getFooX=foo.getX.bind(foo);getFooX();//90就是这样!如果您的所有答案都正确,那就太好了。我们都从犯错误中学习。这就是了解其背后的“原因”。-无法实时获知代码部署后可能出现的bug。之后为了解决这些bug,花费了大量的时间在日志调试上。顺便推荐一个好用的bug监控工具Fundebug。原文:https://dev.to/aman_singh/so-...干货交流系列文章总结如下。我觉得订个Star挺好的。欢迎加群,互相学习。https://github.com/qq44924588...我是小智,公众号《大招天下》的作者,前端技术爱好者。我会经常分享自己学习看到的干货,在进步的路上互相鼓励!关注公众号,后台回复福利,就能看到福利,你懂的。[1]:https://www.fundebug.com/?utm\_source=xiaozhi
