关于javascript:js烧脑面试题大赏

52次阅读

共计 12524 个字符,预计需要花费 32 分钟才能阅读完成。

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

第 1 题

let a = 1
function 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 = a
a.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] = 10
console.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]=123
a=456
console.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=a[‘[object Object]’]。</p>
</details>

第 9 题

var out = 25
var 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=1
switch(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 = 0
console.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 = 1
var a = 2
console.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 = 1
function 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 = 1
var 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 = 1
function a(){}
console.log(a)

var b
function b(){}
console.log(b)

function b(){}
var b
console.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>

正文完
 0