本文精选了20多道具备肯定迷惑性的js题,次要考查的是类型判断、作用域、this指向、原型、事件循环等知识点,每道题都配有笔者具体傻瓜式的解析,偏差于初学者,大佬请随便。

第1题

let a = 1function b(a) {  a = 2  console.log(a)}b(a)console.log(a)

<details>
<summary>点击查看答案</summary>
<p>2、1</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>首先根本类型数据是按值传递的,所以执行b函数时,b的参数a接管的值为1,参数a相当于函数外部的变量,当本作用域有和下层作用域同名的变量时,无法访问到下层变量,所以函数内无论怎么批改a,都不影响下层,所以函数外部打印的a是2,里面打印的仍是1。</p>
</details>

第2题

function a (b = c, c = 1) {  console.log(b, c)}a()

<details>
<summary>点击查看答案</summary>
<p>报错</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>给函数多个参数设置默认值实际上跟按程序定义变量一样,所以会存在暂时性死区的问题,即后面定义的变量不能引用前面还未定义的变量,而前面的能够拜访后面的。</p>
</details>

第3题

let a = b = 10;(function(){   let a = b = 20 })()console.log(a)console.log(b)

<details>
<summary>点击查看答案</summary>
<p>10、20</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>连等操作是从右向左执行的,相当于b = 10、let a = b,很显著b没有申明就间接赋值了,所以会隐式创立为一个全局变量,函数内的也是一样,并没有申明b,间接就对b赋值了,因为作用域链,会一层一层向上查找,找了到全局的b,所以全局的b就被批改为20了,而函数内的a因为从新申明了,所以只是局部变量,不影响全局的a,所以a还是10。</p>
</details>

第4题

var a = {n:1}var b = aa.x = a = {n:2}console.log(a.x)console.log(b.x)

<details>
<summary>点击查看答案</summary>
<p>undefined、{n: 2}</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>恕笔者不才,这道题笔者做一次错一次。</p>
<p>反正依照网上大部分的解释是因为.运算符优先级最高,所以会先执行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 = {n: 2}。</p>
</details>

第5题

var arr = [0, 1, 2]arr[10] = 10console.log(arr.filter(function (x) {  return x === undefined}))

<details>
<summary>点击查看答案</summary>
<p>[]</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这题比较简单,arr[10]=10,那么索引3到9地位上都是undefined,arr[3]等打印进去也的确是undefined,然而,这里其实波及到ECMAScript版本不同对应办法行为不同的问题,ES6之前的遍历办法都会跳过数组未赋值过的地位,也就是空位,然而ES6新增的for of办法就不会跳过。</p>
</details>

第6题

var name = 'World';(function () {  if (typeof name === 'undefined') {    var name = "Jack"    console.info('Goodbye ' + name)  } else {    console.info('Hello ' + name)  }})()

<details>
<summary>点击查看答案</summary>
<p>Goodbye Jack</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这道题考查的是变量晋升的问题,var申明变量时会把变量主动晋升到以后作用域顶部,所以函数内的name尽管是在if分支里申明的,然而也会晋升到外层,因为和全局的变量name重名,所以拜访不到外层的name,最初因为已申明未赋值的变量的值都为undefined,导致if的第一个分支满足条件。</p>
</details>

第7题

console.log(1 + NaN)console.log("1" + 3)console.log(1 + undefined)console.log(1 + null)console.log(1 + {})console.log(1 + [])console.log([] + {})

<details>
<summary>点击查看答案</summary>
<p>NaN、13、NaN、1、1[object Object]、1、[object Object]</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这道题考查的显然是+号的行为:</p>
<p>1.如果有一个操作数是字符串,那么把另一个操作数转成字符串执行连贯</p>
<p>2.如果有一个操作数是对象,那么调用对象的valueOf办法转成原始值,如果没有该办法或调用后仍是非原始值,则调用toString办法</p>
<p>3.其余状况下,两个操作数都会被转成数字执行加法操作</p>
</details>

第8题

var a={},    b={key:'b'},    c={key:'c'}a[b]=123a[c]=456console.log(a[b])

<details>
<summary>点击查看答案</summary>
<p>456</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>对象有两种办法设置和援用属性,obj.name和obj['name'],方括号里能够字符串、数字和变量设置是表达式等,然而最终计算出来得是一个字符串,对于下面的b和c,它们两个都是对象,所以会调用toString()办法转成字符串,对象转成字符串和数组不一样,和内容无关,后果都是[object Obejct],所以a[b]=a[c]=a['[object Object]']。</p>
</details>

第9题

var out = 25var inner = {  out: 20,  func: function () {    var out = 30    return this.out  }};console.log((inner.func, inner.func)())console.log(inner.func())console.log((inner.func)())console.log((inner.func = inner.func)())

<details>
<summary>点击查看答案</summary>
<p>25、20、20、25</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这道题考查的是this指向问题:</p>
<p>1.逗号操作符会返回表达式中的最初一个值,这里为inner.func对应的函数,留神是函数自身,而后执行该函数,该函数并不是通过对象的办法调用,而是在全局环境下调用,所以this指向window,打印进去的当然是window下的out</p>
<p>2.这个显然是以对象的办法调用,那么this指向该对象</p>
<p>3.加了个括号,看起来有点蛊惑人,但实际上(inner.func)和inner.func是齐全相等的,所以还是作为对象的办法调用</p>
<p>4.赋值表达式和逗号表达式类似,都是返回的值自身,所以也绝对于在全局环境下调用函数</p>
</details>

第10题

let {a,b,c} = { c:3, b:2, a:1 }console.log(a, b, c)

<details>
<summary>点击查看答案</summary>
<p>1、2、3</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这题考查的是变量解构赋值的问题,数组解构赋值是按地位对应的,而对象只有变量与属性同名,程序随便。</p>
</details>

第11题

console.log(Object.assign([1, 2, 3], [4, 5]))

<details>
<summary>点击查看答案</summary>
<p>[4, 5, 3]</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>是不是素来没有用assign办法合并过数组?assign办法能够用于解决数组,不过会把数组视为对象,比方这里会把指标数组视为是属性为0、1、2的对象,所以源数组的0、1属性的值笼罩了指标对象的值。</p>
</details>

第12题

var x=1switch(x++){  case 0: ++x  case 1: ++x  case 2: ++x}console.log(x)

<details>
<summary>点击查看答案</summary>
<p>4</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这题考查的是自增运算符的前缀版和后缀版,以及switch的语法,后缀版的自增运算符会在语句被求值后才产生,所以x会仍以1的值去匹配case分支,那么显然匹配到为1的分支,此时,x++失效,x变成2,再执行++x,变成3,因为没有break语句,所以会进入以后case前面的分支,所以再次++x,最终变成4。</p>
</details>

第13题

console.log(typeof undefined == typeof NULL)console.log(typeof function () {} == typeof class {})

<details>
<summary>点击查看答案</summary>
<p>true、true</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>1.首先不要把NULL看成是null,js的关键字是辨别大小写的,所以这就是一个一般的变量,而且没有申明,typeof对没有申明的变量应用是不会报错的,返回'undefined',typeof对undefined应用也是'undefined',所以两者相等</p>
<p>2.typeof对函数应用返回'function',class只是es6新增的语法糖,实质上还是函数,所以两者相等</p>
</details>

第14题

var count = 0console.log(typeof count === "number")console.log(!!typeof count === "number")

<details>
<summary>点击查看答案</summary>
<p>true、false</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>1.没啥好说的,typeof对数字类型返回'number'。</p>
<p>2.这题考查的是运算符优先级的问题,逻辑非!的优先级比全等===高,所以先执行!!typeof count,后果为true,而后执行true === 'number',后果当然为false,能够点击这里查看优先级列表:点我。</p>
</details>

第15题

"use strict"a = 1var a = 2console.log(window.a)console.log(a)

<details>
<summary>点击查看答案</summary>
<p>2、2</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>var申明会把变量晋升到以后作用域顶部,所以a=1并不会报错,另外在全局作用域下应用var申明变量,该变量会变成window的一个属性,以上两点都和是否在严格模式下无关。</p>
</details>

第16题

var i = 1function b() {  console.log(i)}function a() {  var i = 2  b()}a()

<details>
<summary>点击查看答案</summary>
<p>1</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这道题考查的是作用域的问题,作用域其实就是一套变量的查找规定,每个函数在执行时都会创立一个执行上下文,其中会关联一个变量对象,也就是它的作用域,下面保留着该函数能拜访的所有变量,另外上下文中的代码在执行时还会创立一个作用域链,如果某个标识符在以后作用域中没有找到,会沿着外层作用域持续查找,直到最顶端的全局作用域,因为js是词法作用域,在写代码阶段就作用域就曾经确定了,换句话说,是在函数定义的时候确定的,而不是执行的时候,所以a函数是在全局作用域中定义的,尽管在b函数内调用,然而它只能拜访到全局的作用域而不能拜访到b函数的作用域。</p>
</details>

第17题

var obj = {  name: 'abc',  fn: () => {    console.log(this.name)  }};obj.name = 'bcd'obj.fn()

<details>
<summary>点击查看答案</summary>
<p>undefined</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这道题考查的是this的指向问题,箭头函数执行的时候上下文是不会绑定this的,所以它外面的this取决于外层的this,这里函数执行的时候外层是全局作用域,所以this指向window,window对象下没有name属性,所以是undefined。</p>
</details>

第18题

const obj = {  a: {    a: 1  }};const obj1 = {  a: {    b: 1  }};console.log(Object.assign(obj, obj1))

<details>
<summary>点击查看答案</summary>
<p>{a: {b: 1}}</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这道题很简略,因为assign办法执行的是浅拷贝,所以源对象的a属性会间接笼罩指标对象的a属性。</p>
</details>

第19题

console.log(a)var a = 1var getNum = function() {  a = 2}function getNum() {  a = 3}console.log(a)getNum()console.log(a)

<details>
<summary>点击查看答案</summary>
<p>undefined、1、2</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>首先因为var申明的变量晋升作用,所以a变量被晋升到顶部,未赋值,所以第一个打印进去的是undefined。</p>
<p>接下来是函数申明和函数表达式的区别,函数申明会有晋升作用,在代码执行前就把函数晋升到顶部,在执行上下文上中生成函数定义,所以第二个getNum会被最先晋升到顶部,而后是var申明getNum的晋升,然而因为getNum函数曾经被申明了,所以就不须要再申明一个同名变量,接下来开始执行代码,执行到var getNum = fun...时,尽管申明被提前了,然而赋值操作还是留在这里,所以getNum被赋值为了一个函数,上面的函数申明间接跳过,最初,getNum函数执行前a打印进去还是1,执行后,a被批改成了2,所以最初打印进去的2。</p>
</details>

第20题

var scope = 'global scope'function a(){  function b(){     console.log(scope)  }  return b  var scope = 'local scope'}a()()

<details>
<summary>点击查看答案</summary>
<p>undefined</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这题考查的还是变量晋升和作用域的问题,尽管var申明是在return语句前面,但还是会晋升到a函数作用域的顶部,而后又因为作用域是在函数定义的时候确定的,与调用地位无关,所以b的下层作用域是a函数,scope在b本身的作用域里没有找到,向上查找找到了主动晋升的并且未赋值的scope变量,所以打印出undefined。</p>
</details>

第21题

function fn (){   console.log(this) }var arr = [fn]arr[0]()

<details>
<summary>点击查看答案</summary>
<p>打印出arr数组自身</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>函数作为某个对象的办法调用,this指向该对象,数组显然也是对象,只不过咱们都习惯了对象援用属性的办法:obj.fn,然而实际上obj['fn']援用也是能够的。</p>
</details>

第22题

var a = 1function a(){}console.log(a)var bfunction b(){}console.log(b)function b(){}var bconsole.log(b)

<details>
<summary>点击查看答案</summary>
<p>1、b函数自身、b函数自身</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这三小题都波及到函数申明和var申明,这两者都会产生晋升,然而函数会优先晋升,所以如果变量和函数同名的话,变量的晋升就疏忽了。</p>
<p>1.晋升完后,执行到赋值代码,a被赋值成了1,函数因为曾经申明晋升了,所以跳过,最初打印a就是1。</p>
<p>2.和第一题相似,只是b没有赋值操作,那么执行到这两行相当于都没有操作,b当然是函数。</p>
<p>3.和第二题相似,只是先后顺序换了一下,然而并不影响两者的晋升程序,仍是函数优先,同名的var申明晋升疏忽,所以打印出b还是函数。</p>
</details>

第23题

function Foo() {  getName = function () { console.log(1) }  return this}Foo.getName = function () { console.log(2) }Foo.prototype.getName = function () { console.log(3) }var getName = function () { console.log(4) }function getName() { console.log(5) }//请写出以下输入后果:Foo.getName()getName()Foo().getName()getName()new Foo.getName()new Foo().getName()new new Foo().getName()

<details>
<summary>点击查看答案</summary>
<p>2、4、1、1、2、3、3</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这是一道综合性题目,首先getName函数申明会先晋升,而后getName函数表达式晋升,然而因为函数申明晋升在线,所以疏忽函数表达式的晋升,而后开始执行代码,执行到var getName= ...时,批改了getName的值,赋值成了打印4的新函数。</p>
<p>1.执行Foo函数的静态方法,打印出2。</p>
<p>2.执行getName,以后getName是打印出4的那个函数。</p>
<p>3.执行Foo函数,批改了全局变量getName,赋值成了打印1的函数,而后返回this,因为是在全局环境下执行,所以this指向window,因为getName曾经被批改了,所以打印出1。</p>
<p>4.因为getName没有被从新赋值,所以再执行依然打印出1。</p>
<p>5.new操作符是用来调用函数的,所以new Foo.getName()相当于new (Foo.getName)(),所以new的是Foo的静态方法getName,打印出2。</p>
<p>6.因为点运算符(.)的优先级和new是一样高的,所以从左往右执行,相当于(new Foo()).getName(),对Foo应用new调用会返回一个新创建的对象,而后执行该对象的getName办法,该对象自身并没有该办法,所以会从Foo的原型对象上查找,找到了,所以打印出3。</p>
<p>7.和上题一样,点运算符(.)的优先级和new一样高,另外new是用来调用函数的,所以new new Foo().getName()相当于new ((new Foo()).getName)(),括号外面的就是上一题,所以最初找到的是Foo原型上的办法,无论是间接调用,还是通过new调用,都会执行该办法,所以打印出3。</p>
</details>

第24题

const person = {    address: {        country:"china",        city:"hangzhou"    },    say: function () {        console.log(`it's ${this.name}, from ${this.address.country}`)    },    setCountry:function (country) {        this.address.country=country    }}const p1 = Object.create(person)const p2 = Object.create(person)p1.name = "Matthew"p1.setCountry("American")p2.name = "Bob"p2.setCountry("England")p1.say()p2.say()

<details>
<summary>点击查看答案</summary>
<p>it's Matthew, from England</p>
<p>it's Bob, from England</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>Object.create办法会创立一个对象,并且将该对象的__proto__属性指向传入的对象,所以p1和p2两个对象的原型对象指向了同一个对象,接着给p1增加了一个name属性,而后调用了p1的setCountry办法,p1自身是没有这个办法的,所以会沿着原型链进行查找,在它的原型上,也就是person对象上找到了这个办法,执行这个办法会给address对象的country属性设置传入的值,p1自身也是没有address属性的,然而和name属性不一样,address属性在原型对象上找到了,并且因为是个援用值,所以会胜利批改它的country属性,接着对p2的操作也是一样,而后因为原型中存在援用值会在所有实例中共享,所以p1和p2它们援用的address也是同一个对象,一个实例批改了,会反映到所有实例上,所以p2的批改会笼罩p1的批改,最终country的值为England。</p>
</details>

第25题

setTimeout(function() {  console.log(1)}, 0)new Promise(function(resolve) {  console.log(2)  for( var i=0 ; i<10000 ; i++ ) {    i == 9999 && resolve()  }  console.log(3)}).then(function() {  console.log(4)})console.log(5)

<details>
<summary>点击查看答案</summary>
<p>2、3、5、4、1</p>
</details>
<details>
<summary>点击查看解析</summary>
<p>这道题显然考查的是事件循环的知识点。</p>
<p>js是一门单线程的语言,然而为了执行一些异步工作时不阻塞代码,以及防止期待期间的资源节约,js存在事件循环的机制,单线程指的是执行js的线程,称作主线程,其余还有一些比方网络申请的线程、定时器的线程,主线程在运行时会产生执行栈,栈中的代码如果调用了异步api的话则会把事件增加到事件队列里,只有该异步工作有了后果便会把对应的回调放到【工作队列】里,当执行栈中的代码执行结束后会去读取工作队列里的工作,放到主线程执行,当执行栈空了又会去查看,如此往返,也就是所谓的事件循环。</p>

<p>异步工作又分为【宏工作】(比方setTimeout、setInterval)和【微工作】(比方promise),它们别离会进入不同的队列,执行栈为空完后会优先查看微工作队列,如果有微工作的话会一次性执行完所有的微工作,而后去宏工作队列里查看,如果有则取出一个工作到主线程执行,执行完后又会去查看微工作队列,如此循环。</p>

<p>回到这题,首先整体代码作为一个宏工作开始执行,遇到setTimeout,相应回调会进入宏工作队列,而后是promise,promise的回调是同步代码,所以会打印出2,for循环完结后调用了resolve,所以then的回调会被放入微工作队列,而后打印出3,最初打印出5,到这里以后的执行栈就空了,那么先查看微工作队列,发现有一个工作,那么取出来放到主线程执行,打印出4,最初查看宏工作队列,把定时器的回调放入主线程执行,打印出1。</p>
</details>

第26题

console.log('1');setTimeout(function() {  console.log('2');  process.nextTick(function() {    console.log('3');  });  new Promise(function(resolve) {    console.log('4');    resolve();  }).then(function() {    console.log('5');  });}); process.nextTick(function() {  console.log('6');});new Promise(function(resolve) {  console.log('7');  resolve();}).then(function() {  console.log('8');});setTimeout(function() {  console.log('9');  process.nextTick(function() {    console.log('10');  })   new Promise(function(resolve) {    console.log('11');    resolve();  }).then(function() {    console.log('12')  });})

<details>
<summary>点击查看答案</summary>
<p>1、7、6、8、2、4、9、11、3、10、5、12</p>
</details>

<details>
<summary>点击查看解析</summary>
<p>这道题和上一题差不多,然而呈现了process.nextTick,所以显然是在node环境下,node也存在事件循环的概念,然而和浏览器的有点不一样,nodejs中的宏工作被分成了几种不同的阶段,两个定时器属于timers阶段,setImmediate属于check阶段,socket的敞开事件属于close callbacks阶段,其余所有的宏工作都属于poll阶段,除此之外,只有执行到后面说的某个阶段,那么会执行完该阶段所有的工作,这一点和浏览器不一样,浏览器是每次取一个宏工作进去执行,执行完后就跑去查看微工作队列了,然而nodejs是来都来了,一次全副执行完该阶段的工作好了,那么process.nextTick和微工作在什么阶段执行呢,在后面说的每个阶段的前面都会执行,然而process.nextTick会优先于微工作,一图胜千言:</p>
<p>

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54c016c3cf344149a27b28574264f624~tplv-k3u1fbpfcp-watermark.image"/>

</p>
<p>了解了当前再来剖析这道题就很简略了,首先执行整体代码,先打印出1,setTimeout回调扔进timers队列,nextTick的扔进nextTick的队列,promise的回调是同步代码,执行后打印出7,then回调扔进微工作队列,而后又是一个setTimeout回调扔进timers队列,到这里以后节点就完结了,查看nextTick和微工作队列,nextTick队列有工作,执行后打印出6,微工作队列也有,打印出8,接下来按程序查看各个阶段,check队列、close callbacks队列都没有工作,到了timers阶段,发现有两个工作,先执行第一个,打印出2,而后nextTick的扔进nextTick的队列,执行promise打印出4,then回调扔进微工作队列,再执行第二个setTimeout的回调,打印出9,而后和方才一样,nextTick的扔进nextTick的队列,执行promise打印出11,then回调扔进微工作队列,到这里timers阶段也完结了,执行nextTick队列的工作,发现又两个工作,顺次执行,打印出3和10,而后查看微工作队列,也是两个工作,顺次执行,打印出5和12,到这里是有队列都清空了。</p>
</details>