JavaScript是前端开发中非常重要的语言,浏览器是它主要运行的地方。JavaScript是一门非常有趣的语言,但它有许多人们常常忽略的概念。比如原型、闭包、原型链、事件循环等概念,很多JS开发者研究的并不多。那么今天,我就和大家一起看看以下几个问题。你可以先想一想,试着回答。面试八题问题一:下面的代码在浏览器控制台会打印什么?问题2:如果我们用let或const代替var,输出是否一样。问题3:“newArray”中有哪些元素?问题四:如果我们在浏览器控制台运行'foo'函数,会不会导致堆栈溢出错误?问题5:如果我在控制台运行如下功能,页面(tab)是否有响应?问题6:我们能否以某种方式对以下语句使用展开运算符而不会导致类型错误。问题7:运行以下代码片段时,控制台会打印什么?问题8:xGetter()将打印什么值?回答之前的问题我们都给出了例子,接下来我们会从头到尾对这些问题的回答一一进行分析,给大家一些学习思路。问题一:使用var关键字声明的变量在JavaScript中会被提升,在内存中开辟空间。由于没有赋值,无法定义数值类型,所以赋值默认值undefined。var声明的变量,实际值初始化,发生在你确定赋值的地方。同时我们要知道var声明的变量是函数作用域的,也就是要区分局部变量和全局变量,而let和const是块作用域的。所以我们这个问题的运行过程是这样的:vara=10;//全局作用域,全局变量。a=10functionfoo(){//vara//声明会被提升到函数的顶部。//例如:varaconsole.log(a);//打印undefined//实际初始化值20只出现在这里vara=20;//localscope}的图示如下,很容易理解。所以问题1的答案是:未定义问题2:let和const声明允许将变量的范围限制在它们所在的块、语句或表达式中。与var不同的是,这两者声明的变量不会被提升。我们将有一个叫做临时死区(TDZ)的东西。如果访问TDZ中的变量,会报ReferenceError,因为它们的作用域在声明的位置,不会有提升。因此,必须在语句执行的位置访问它。vara=10;//全局使用域functionfoo(){//TDZstart//Createduninitialized'a'console.log(a);//ReferenceError//TDZend,'a'只在这里初始化,值为20leta=20;}图:问题2答案:ReferenceError:aundefined。问题三:这个问题是循环结构会给你带来对块级作用域的误解。在for循环头部用var声明的变量是单声明变量绑定(单存储空间)。在循环过程中,这个var声明的i变量会随着循环而变化。但是循环中执行的数组push方法实际上是在循环结束时压入了3的值。所以最后push的都是3//scope误区:认为有块级scopevararray=[];for(vari=0;i<3;i++){//三个body中的每个'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());console.log(newArray);//[0,1,2]这个问题还有另一种解决方案一种解决方案是使用闭包。letarray=[];for(vari=0;i<3;i++){array[i]=(function(x){returnfunction(){returnx;};})(i);}constnewArray=array.map(el=>el());console.log(newArray);//[0,1,2]问题3答案:3,3,3问题4JavaScript的并发模式是基于我们常说的“事件循环”。浏览器为我们提供了执行JS代码的运行环境。浏览器的主要组件包括调用栈、事件循环、任务队列和WEBAPI。常用的定时器setTimeout、setInterval等全局函数并不是JavaScript的一部分,而是由WEBAPI提供给我们的。JS调用堆栈是后进先出(LIFO)。引擎一次从堆栈中弹出一个函数,然后从上到下依次运行代码。每当遇到像setTimeout这样的异步代码时,它就会将其交给WebAPI(箭头1)。因此,无论何时触发事件,回调都会被发送到任务队列(箭头2)。事件循环(Eventloop)不断地监控任务队列(TaskQueue),并按照回调的排队顺序一次一个地处理回调。每当调用堆栈为空时,Event循环获取回调并将其放入堆栈(箭头3)以进行处理。请记住,如果调用堆栈不为空,事件循环不会将任何回调压入堆栈。好了,有了前面的知识,我们可以看看这道题的解释过程:实现步骤:调用foo()会将foo函数放入调用栈(callstack)。JS引擎在处理内部代码时遇到setTimeout。然后将foo回调函数传递给WebAPI(箭头1)并从函数返回,调用堆栈再次为空并且定时器设置为0,因此foo将被发送到任务队列(箭头2)。由于调用堆栈是空的,事件循环将选择foo回调并将其压入调用堆栈以进行处理。再次重复该过程,堆栈没有溢出。第4题答案:栈不会溢出。问题5:很多时候很多做前端开发的同学都认为循环事件图中只会有一个任务列表。但实际上并非如此,我们可以有多个任务列表。浏览器选择其中一个队列并对该队列执行处理回调。从底层来看,JavaScript可以有宏,也可以有微任务。例如,setTimeout回调是宏任务,Promise回调是微任务。它们之间有什么区别?主要区别在于它们的实现方式。宏任务在单个周期中一次堆叠一个,但微任务队列总是在执行后清空,然后返回事件。因此,如果您以处理条目的速度向该队列添加条目,那么您将永远处理微任务。只有当微任务队列为空时,事件循环才会重新渲染页面。那我们回到前面讲的问题5:functionfoo(){returnPromise.resolve().then(foo);};我们的代码,每次调用[foo],都会在microtask的队列中再添加一个[foo]回调,这样事件循环就无法继续处理其他事件(比如滚动,点击事件等),直到队列完全清空。因此,将不会执行渲染并将被阻止。问题5的答案:无响应。问题6:我们在做面试题的时候,展开遍历可迭代对象的语法和for-of语句来定义要遍历的数据。当我们要使用迭代器时,Array和Map都内置了迭代器,默认有迭代操作。但是,对象是不可迭代的,也就是说,在我们的问题中,这是一个对象的集合。但是我们可以使用iterable和iterator协议使其可迭代。我们在研究对象的时候,如果一个对象实现了@@iterator方法,那么它就可以被迭代。这意味着这个对象(它的原型链上的一个对象)必须是@@iterator键的一个属性,然后我们可以使用这个键通过常量Symbol.iterator来获取它。下面是写这道题的例子:varobj={x:1,y:2,z:3};obj[Symbol.iterator]=function(){//iterator是一个有next方法的对象,//它的返回至少有一个对象//两个属性:value&done。//返回一个迭代器对象return{next:function(){if(this._countDown===3){constlastValue=this._countDown;return{value:this._countDown,done:true};}thisthis._countDown=this._countDown+1;return{value:this._countDown,done:false};},_countDown:0};};[...obj];//打印[1,2,3]第6题答案:同上避免TypeError异常的解决方案。问题七:看这道题的时候,首先要理解for-in循环遍历本身的可枚举属性和继承自对象原始原型的属性。可枚举属性是可以在for-in循环期间访问的属性。当我们知道了这个知识点的前提之后,我们在看这道题,就会知道这道题打印出来的其实只是打印这些具体的属性。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-in来遍历这个对象集合,也就是只能遍历可枚举的属性for(letpropinobj){console.log(prop);}//只能打印//a//b//c图解7题答案:a,b,c8题:首先我们可以看出varx是全局遍历,如果不是在严格模式下,这个X直接是window对象的一个??属性。在这段代码中,我们最重要的是理解this的对象指向问题,this始终指向调用方法的对象。所以,对于foo,xGetter(),this指向foo对象,返回的是foo中的属性x,值为90。但是在xGetter()的情况下,它直接调用了foo的getx()方法,但是this的指向是在xGetter的作用域内,也就是指向的window对象,而这里指向的是全局变量x时间。取值也是10。varx=10;//全局变量varfoo={x:90,//foo对象的内部属性getX:function(){returnthis.x;}};foo.getX();//Thisisthefooobjectpointedto,//所以打印的是X属性的值是90letxGetter=foo.getX;//xGetter是在全局范围内的,//这里this指向的是window对象xGetter();//打印10道题和8道答案:10道终于ok了,8道题我们都解决了,如果你把答案都写对了,那你就太棒了!去面试前端工作至少12k起步。即使你做不到或犯错误也没关系,我们都从错误中学习。只有一步一步地了解错误,了解错误背后的原因,才能取得进步。
