第1题leta=1functionb(a){a=2console.log(a)}b(a)console.log(a)答案2、1分析首先数据的基本类型是by传值,所以当函数b执行时,b的参数a接收到的值为1,参数a相当于函数内部的一个变量。当这个作用域中存在与上层作用域同名的变量时,无法访问上层变量,所以无论a在函数中怎么修改,都不会影响到上层,所以在函数内部打印了aas2,打印出来还是1。问题2functiona(b=c,c=1){console.log(b,c)}a()答案是错误分析。为函数的多个参数设置默认值其实和顺序定义变量是一样的,所以会出现临时死区的问题,即前面定义的变量不能引用没有定义的变量定义在后面,后面可以访问前面的。问题3leta=b=10;(function(){leta=b=20})()console.log(a)console.log(b)回答10、20、解析等操作是从右到左进行的,这相当于b=10,令a=b。显然,b是没有声明直接赋值的,所以会隐式创建为全局变量。因为作用域链会逐层往上查找,找到全局b,所以全局b改为20,而函数中的a只是一个局部变量,因为重新声明了,不影响全局a,所以a还是10。问题4的答案vara={n:1}varb=aa.x=a={n:2}console.log(a.x)console.log(b.x)未定义,{n:2}parsing对不起我的天赋,这道题的作者犯了一次错误。无论如何,根据互联网上的大多数解释,因为.operator具有最高优先级,a.x将首先执行。这时a和b指向的{n:1}变成了{n:1,x:undefined},然后按照链式操作从右往左执行代码,a={n:2},显然,a现在指向一个新的对象,然后a.x=a,因为a.x一开始就已经执行过了,所以这其实相当于:({n:1,x:undefined}).x=b.x=a={?:2}。第5题vararr=[0,1,2]arr[10]=10console.log(arr.filter(function(x){returnx===undefined}))回答[]这题比较简单,arr[10]=10,那么index3到9是undefined,打印出来arr[3]确实是undefined。不过这其实涉及到不同ECMAScript版本对应的方法表现不同的问题,ES6之前的遍历方法会跳转到数组未赋值的位置,也就是空的地方,但是ES6新的forof方法不会跳过。问题6varname='World';(function(){if(typeofname==='undefined'){varname="Jack"console.info('Goodbye'+name)}else{console.info('Hello'+name)}})()AnswerGoodbyeJackAnalysis本题考查变量提升的问题。var在声明变量的时候,会自动将变量提升到当前作用域的顶部,所以函数中的名字虽然在if分支中声明了,但是也会提升到外层。因为它和全局变量名重名,所以无法访问外层的名字。最后因为已经声明但没有赋值的变量的值都是undefined,所以if的第一个分支满足条件。问题7console.log(1+NaN)console.log("1"+3)console.log(1+undefined)console.log(1+null)console.log(1+{})console.log(1+[])console.log([]+{})AnswersNaN,13,NaN,1,1[objectObject],1,[objectObject]这道题的分析很明显是+号的行为:1.如果其中一个操作数是字符串,则将另一个操作数转换为字符串进行拼接2.如果其中一个操作数是对象,则调用对象的valueOf方法将其转换为原始值,如果有没有这个方法或者调用后如果还是非原始值,调用toString方法3.其他情况会将两个操作数转换成数字进行加法运算。第8题vara={},b={key:'b'},c={key:'c'}a[b]=123a[c]=456console.log(a[b])回答456解析对象设置和引用属性有两种方式,obj.name和obj['name'],方括号中的字符串,数字,变量设置可以是表达式等,但最终计算的是字符串。对于上面的b和c,它们都是对象,所以会调用toString()方法将它们转换成字符串,对象转换成字符串不同于数组,与内容无关。结果是[objectObject],所以a[b]=a[c]=a['[objectObject]']。问题9varout=25varinner={out:20,func:function(){varout=30returnthis.out}};console.log((inner.func,inner.func)())console.log(inner.func())console.log((inner.func)())console.log((inner.func=inner.func)())答案25,20,20,25解析本题考察本题所指的问题:1.逗号运算符会返回表达式中的最后一个值,这里是inner.func对应的函数,注意是函数本身,然后执行函数,函数不是被对象的方法调用,而是调用in全局环境,所以this指向window,打印出来的当然是window下的out2。This明显是调用了对象的方法,所以this指向了对象3.加括号好像有点乱,但实际上(inner.func)和inner.func是完全等价的,所以还是调用对象方法a,b,c}={c:3,b:2,a:1}console.log(a,b,c)答案1、2、3分析本题考察变量解构赋值的问题,数组解构赋值是对应位置的,只要对象与变量和属性同名,顺序随意。第11题console.log(Object.assign([1,2,3],[4,5]))回答[4,5,3]分析数组从来没有用assign方法合并过?assign方法可用于处理数组,但它会将数组视为一个对象。例如,目标数组会被视为具有属性0、1、2的对象,因此源数组的0和1属性的值覆盖目标对象的值。价值。第12题varx=1switch(x++){case0:++xcase1:++xcase2:++x}console.log(x)第4题分析本题考查自增运算符的前缀和后缀版本,在switch的语法,自增运算符的后缀版本只会在语句求值后出现,所以x还是会匹配值为1的case分支,那么显然匹配的是1的分支,此时,x++生效,x变成2,然后执行++x,变成3。因为没有break语句,会进入当前case后面的分支,所以再次++x,最后变成4。第13题console.log(typeofundefined==typeofNULL)console.log(typeofffunction(){}==typeofclass{})回答正确,正确分析1.首先不要把NULL当成null,js的关键字是区分大小写,所以这是一个普通变量,没有声明,typeof在使用未声明的变量时不会报错,并返回'undefined',typeof对于undefined也是'undefined',所以两者是equal2.typeof是对函数的使用返回'function',而class只是es6新增的一个语法糖。本质上还是一个函数,所以两者是相等的。第14题varcount=0console.log(typeofcount==="number")console.log(!!typeofcount==="number")答案真假分析1.没什么好说的,typeof返回'number'为数字类型。2.本题考查运营商优先级的问题。逻辑非!优先级比congruent===高,所以先执行!!typeofcount,结果为true,再执行true==='number',结果当然是false,可以点这里查看优先级列表:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table。第15题"usestrict"a=1vara=2console.log(window.a)console.log(a)回答2、2解析var声明会将变量提升到当前作用域的顶部,所以a=1不会报一个错误,另外,如果你在全局范围内使用var声明一个变量,这个变量就会成为窗口的一个属性。以上两点与你是否处于严格模式无关。第16题vari=1functionb(){console.log(i)}functiona(){vari=2b()}a()答案1分析本题考查scope的问题,其实就是一组变量根据搜索rules,每个函数在执行的时候都会创建一个执行上下文,这个执行上下文会关联一个变量对象,也就是它的作用域,里面存放了该函数可以访问的所有变量。将创建作用域链。如果在当前范围内没有找到标识符,它将继续沿着外部范围搜索,直到顶部的全局范围。因为js是词法作用域,它的作用域就已经确定了,换句话说就是函数定义的时候就确定了,而不是执行的时候就确定了,所以函数a是在全局作用域定义的。虽然在b函数中调用,但是只能访问全局作用域的Go,不能访问b函数的作用域。第17题varobj={name:'abc',fn:()=>{console.log(this.name)}};obj.name='bcd'obj.fn()答案是undefined查分析thisquestion是this的指向性问题。执行箭头函数时,context不会绑定this,所以里面的this依赖于外层的this。函数执行时,外层是全局作用域,所以this指向window,window对象下没有name属性,所以是undefined。问题18constobj={a:{a:1}};constobj1={a:{b:1}};console.log(Object.assign(obj,obj1))answer{a:{b:1}}解析这道题很简单,因为assign方法进行的是浅拷贝,所以源对象的a属性会直接覆盖目标对象的a属性。问题19console.log(a)vara=1vargetNum=function(){a=2}functiongetNum(){a=3}console.log(a)getNum()console.log(a)回答undefined,1,2解析首先,由于var声明的变量提升作用,a变量被提升到最顶层,并没有被赋值,所以首先打印出来的是undefined。接下来是函数声明和函数表达式之间的区别。函数声明会起到促进作用。在代码执行之前,函数会被提升到最顶层,函数定义会在执行上下文中生成,所以第二个getNum会先被提升。在最上面,提升了var语句getNum,但是因为getNum函数已经声明过,所以不需要再声明一个同名变量,然后开始执行代码。当vargetNum=fun...时,虽然声明提前了,但是赋值操作还在,所以getNum赋值给了一个函数,后面的函数声明直接跳过。最后在getNum函数执行之前,a还是打印为1,执行完后a变成了2,所以最后打印出了2。第20题varscope='globalscope'functiona(){functionb(){console.log(scope)}returnbvarscope='localscope'}a()()答案undefined分析这道题还是关于变量提升和作用域的。虽然var声明在return语句后面,但它仍然会被提升到a函数作用域的顶部,而且由于作用域是在定义函数时确定的,与调用位置无关,所以b的upperscope是a函数,在b本身的作用域内没有找到该作用域,向上查找找到自动提升未赋值的作用域变量,所以打印出undefined。第21题functionfn(){console.log(this)}vararr=[fn]arr[0]()答案打印出arr数组本身。解析函数作为一个对象的方法被调用,this指向对象,而数组显然是一个对象,但是我们都习惯了对象引用属性的方法:obj.fn,但其实obj['fn']引用也是可以的。第22题vara=1functiona(){}console.log(a)varbfunctionb(){}console.log(b)functionb(){}varbconsole.log(b)回答1,b函数本身,b函数本身分析这个的三道小题都涉及函数声明和var声明,两者都会提升,但是函数会先提升,所以如果变量和函数同名,变量的提升会被忽略。1、提升完成后,执行赋值代码,将a赋值给1。该函数因为已经声明提升而被跳过,最后打印a为1。2、和第一题类似,除了b没有赋值操作,那么Execution到这两行就相当于没有操作,b当然是一个函数。3.和第二个问题类似,只是顺序变了,但是不影响两者的晋升顺序。还是函数优先,同名var声明的提升被忽略,所以打印出来的b依然是函数。问题23functionFoo(){getName=function(){console.log(1)}returnthis}Foo.getName=function(){console.log(2)}Foo.prototype.getName=function(){console.log(3)}vargetName=function(){console.log(4)}functiongetName(){console.log(5)}//请写出如下输出:Foo.getName()getName()Foo().getName()getName()newFoo.getName()newFoo().getName()newnewFoo().getName()回答2,4,1,1,2,3,3分析这是一道综合题,首先getName函数声明它会先提升,再提升getName函数表达式,但是因为在线提升了函数声明,忽略了函数表达式的提升,再执行代码。执行到vargetName=...,修改getName的值,赋值变成打印4的Newfunction。1.执行Foo函数的静态方法,打印2。2.执行getName。当前的getName是打印4的函数。3.执行Foo函数,修改全局变量getName,赋值给一个打印1的函数,然后返回this,因为是在全局环境下执行的,所以this指向window,因为getName被修改了,所以打印1。4.因为getName没有被重新赋值,所以执行还是打印1。5.调用函数时使用了new操作符,所以newFoo.getName()是等价的tonew(Foo.getName)(),所以new是Foo的静态方法getName,打印2。6.因为点运算符(.)和new的优先级相同,所以从左到右执行,相当于(newFoo()).getName(),是的,Foo会通过调用new返回一个新创建的对象,然后执行该对象的getName方法。对象本身没有这个方法,所以会从Foo的原型对象中查找,找到,然后打印3。7、同上题,点运算符(.)与new的优先级相同,new是用来调用函数的,所以newnewFoo().getName()等价于new((newFoo()).getName)(),括号里的就是上一题,所以最终找到了Foo原型上的方法,无论是直接调用还是通过new调用,都会执行该方法,所以打印了3。问题24constperson={address:{country:"china",city:"hangzhou"},say:function(){console.log(`it's${this.name},from${this.address.country}`)},setCountry:function(country){this.address.country=country}}constp1=Object.create(person)constp2=Object.create(person)p1.name="Matthew"p1.setCountry("美国")p2.name="Bob"p2.setCountry("England")p1.say()p2.say()答案是Matthew,来自Englandit'sBob,来自England解析Object.create方法会创建一个对象,对象的__proto__属性指向传入的对象,所以p1和p2这两个对象的原型对象指向同一个对象,然后给p1添加一个name属性,然后调用p1的setCountry方法,p1本身是没有这个方法的,所以它会沿着原型链查找,在它的原型,也就是person对象上找到这个方法。执行这个方法会设置address对象的country属性传入的值,p1本身是没有address属性的,但是和name属性不同的是,address属性是在prototype对象上找的,而且因为是引用value,它的country属性就会修改成功,然后对p2的操作也是一样的,而且因为原型中有引用值,所以会在所有实例中共享,所以p1和p2引用的地址为也是同一个对象。如果修改了一个instance,会反映到所有instance上,所以p2的修改会覆盖p1的修改,country最终的值为England。问题25setTimeout(function(){console.log(1)},0)newPromise(function(resolve){console.log(2)for(vari=0;i<10000;i++){i==9999&&resolve()}console.log(3)}).then(function(){console.log(4)})console.log(5)答案2,3,5,4,1分析这道题很明显考查的是事件循环知识点。js是单线程语言,但是为了在执行一些异步任务的时候不阻塞代码,也为了避免等待期间的资源浪费,js有一个事件循环机制。单线程是指执行js的线程,称为主线程。还有其他线程,例如网络请求线程和定时器线程。主线程在运行时会产生一个执行栈。如果堆栈中的代码调用异步API,事件将被添加到事件队列中。只要异步任务有结果时,就会把对应的回调放到【任务队列】中。当执行栈中的代码执行时,会读取任务队列中的任务,放入主线程中执行。当执行栈为空时,它会再次检查。等等,这就是所谓的事件循环。异步任务分为【宏任务】(如setTimeout、setInterval)和【微任务】(如promises)。他们将分别进入不同的队列。执行栈为空后,会先检查微任务队列。如果有microtasks的话,所有的microtasks会一次性执行,然后查入macrotask队列。如果有,就会取出一个task在主线程上执行。执行完后会在微任务队列中再次检查,以此类推。回到这个问题,首先整体代码开始作为宏任务执行。遇到setTimeout时,对应的回调会进入宏任务队列,然后promise。promise的回调是同步代码,所以会打印2,在for循环结束后调用。resolve,所以then的回调会被放入microtask队列,然后打印出3,最后打印出5,而这里当前执行栈为空,那么首先查看microtask队列,发现有任务,则拿出来放到主线程去执行,打印出4,最后查看宏任务队列,把定时器的回调放到主线程去执行,打印出1。问题26console.log('1');setTimeout(function(){console.log('2');process.nextTick(function(){console.log('3');});newPromise(function(resolve){console.log('4');resolve();}).then(function(){console.log('5');});});process.nextTick(function(){console.日志('6');});newPromise(函数(解析){console.log('7');resolve();}).then(函数(){console.log('8');});setTimeout(function(){console.log('9');process.nextTick(function(){console.log('10');})newPromise(function(resolve){console.log('11');resolve();}).then(function(){console.log('12')});})答案1,7,6,8,2,4,9,11,3,10,5,12分析这道题和上一道题类似,但是出现了process.nextTick,所以很明显在node环境下,node也有事件循环的概念,只是和浏览器有点区别,nodejs中的宏任务是它分为几个不同的阶段。两个定时器属于timers阶段,setImmediate属于check阶段,socket关闭事件属于closecallbacks阶段,其他所有宏任务属于poll阶段。在某个阶段,这个阶段的所有任务都会被执行。这与浏览器不同。浏览器每次抓取一个宏任务并执行。执行完之后跑去查看microtask队列,但是nodejs是全在的,这个阶段的任务全部一次性执行,那么process.nextTick和microtasks是在什么阶段执行的呢?它们会在上面提到的每个stage之后执行,但是process.nextTick会优先于microtasks,一张图胜过千言万语:理解后分析这道题很简单,先实现整个正文代码,先打印出1,setTimeout回调扔进timers队列,nextTick扔进nextTick队列,promise回调是同步代码,执行完后,打印出7,然后回调扔进microtask队列,再抛一个setTimeout回调它进入定时器队列,这里当前节点结束。检查nextTick和微任务队列。nextTick队列中有任务。执行完后打印6,microtask队列也打印8。接下来按顺序查看各个stage,查看队列closecallbacks队列中没有任务。在计时器阶段,找到两个任务。先执行第一个,打印出2,然后将nextTick丢入nextTick队列,执行promise打印出4,再将回调丢入microtask队列,再执行第二个setTimeout回调,打印出9,然后,和之前一样,把nextTick丢进nextTick队列,执行promise打印出11,然后把callback丢进microtask队列,这里timers阶段结束,执行NextTick队列任务,又找到两个任务,依次执行,打印out3和10,然后查看microtask队列,也是两个task,依次执行,打印出5和12,这里队列全部清空
