收集一些常见的烧脑题(上)

虽然一般用不到,但是能加深对js运行机制的理解

第1题

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

答案: Cannot access ‘c’ before initialization

解析: 给函数多个参数设置默认值实际上跟按顺序定义变量一样,所以会存在暂时性死区,即前面定义的变量不能引用后面还未定义的变量,二后面的可以访问前面的

第2题

1
2
3
4
5
6
let a = b = 10
;(function(){
let a = b = 20
})()
console.log(a)
console.log(b)

答案: 10 、 20

解析: 连等操作是从右向左执行,相当于 b = 10 , let a = b, 很明显b没有声明就直接赋值了,所以会隐式创建为一个全局变量,函数内的也是一样,并没有声明b,直接就对b赋值了,因为作用域链,会一层一层向上查找,找了到全局的b,所以全局的b就被修改为20了,而函数内的a因为重新声明了,所以只是局部变量,不影响全局的a,所以a还是10。

第3题

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

答案:undefined、{n: 2}

解析运算符优先级最高,所以会先执行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}。

第4题

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

答案:[]

解析:arr[10]=10,那么索引3到9位置上都是undefined,arr[3]等打印出来也确实是undefined,但是,这里其实涉及到ECMAScript版本不同对应方法行为不同的问题,ES6之前的遍历方法都会跳过数组未赋值过的位置,也就是空位,但是ES6新增的for of方法就不会跳过

第5题

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

答案:[4,5,3]

解析:assign方法可以用于处理数组,不过会把数组视为对象,比如这里会把目标数组视为是属性为0、1、2的对象,所以源数组的0、1属性的值覆盖了目标对象的值。

第6题

1
2
3
4
5
6
7
8
9
10
var x=1
switch(x++)
{
case 0: ++x
case 1: ++x
break
case 2: ++x

}
console.log(x)

答案:4

解析后缀版的自增运算符会在语句被求值后才发生,所以x会仍以1的值去匹配case分支,那么显然匹配到为1的分支,此时,x++生效,x变成2,再执行++x,变成3,因为没有break语句,所以会进入当前case后面的分支,所以再次++x,最终变成4。

第7题

1
2
3
4
5
6
7
8
9
10
11
12
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)())

答案:25, 20, 20,25

解析:这道题考察this指向问题

  1. 逗号操作符会返回表达式中的最后一个值,这里是inner.func对应的函数,ƒ () { var out = 3 return this.out }(函数本身),然后执行此函数,此函数并不是通过对象的方法调用,而是在全局环境下调用,所以this指向window, 打印出来的当然是window下的out
  2. 这个显然是以对象的方法调用,那么this指向该对象
  3. 加了个括号,看起来有点迷惑人,但实际上(inner.func)和inner.func完全相等的,所以还是作为对象的方法调用
  4. 赋值表达式和逗号表达式相似,都是返回的值本身,所以也相对于在全局环境下调用函数

第8题

1
2
3
4
5
6
7
8
var obj = {
name: 'abc',
fn: () => {
console.log(this.name)
}
};
obj.name = 'bcd'
obj.fn()

答案:undefined

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

第9题

1
2
3
4
5
6
7
8
9
10
11
12
console.log(a)
var a = 1
var getNum = function() {
a = 2
}
function getNum() {
a = 3
}
console.log(a)
getNum()
console.log(a)

答案:undefined

解析

  • 首先因为var声明的变量提升作用,所以a变量被提升到顶部,未赋值,所以第一个打印出来的是undefined。

  • 接下来是函数声明和函数表达式的区别,函数声明会有提升作用,在代码执行前就把函数提升到顶部,在执行上下文上中生成函数定义,所以第二个getNum会被最先提升到顶部,然后是var声明getNum的提升,但是因为getNum函数已经被声明了,所以就不需要再声明一个同名变量,接下来开始执行代码,执行到var getNum = fun…时,虽然声明被提前了,但是赋值操作还是留在这里,所以getNum被赋值为了一个函数,下面的函数声明直接跳过,最后,getNum函数执行前a打印出来还是1,执行后,a被修改成了2,所以最后打印出来的2。

第10题

1
2
3
4
5
function fn (){ 
console.log(this)
}
var arr = [fn]
arr[0]()

答案:打印出arr数组本身

解析:函数作为某个对象的方法调用,this指向该对象,数组显然也是对象,只不过我们都习惯了对象引用属性的方法:obj.fn ,但是实际上obj['fn']引用也是可以的。

第11题

1
2
3
4
5
6
7
8
9
10
11
var a = 1
function a(){}
console.log(a)

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

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

答案:1、b函数本身、b函数本身

解析:这三小题都涉及到函数声明和var声明,两者都会发生提升,但是函数会优先提升,所以如果变量和函数同名的话,变量的提升就忽略

  • 提升完后,执行到赋值代码,a被赋值成了1,函数因为已经声明提升了,所以跳过,最后打印a就是1。

  • 和第一题类似,只是b没有赋值操作,那么执行到这两行相当于都没有操作,b当然是函数。

  • 和第二题类似,只是先后顺序换了一下,但是并不影响两者的提升顺序,仍是函数优先,同名的var声明提升忽略,所以打印出b还是函数。

函数和变量提升的区别

第12题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()

答案

2、4、1、1、2、3、3

解析

这是一道综合性题目,首先getName函数声明会先提升,然后getName函数表达式提升,但是因为函数声明提升在前,所以 忽略函数表达式提升,然后开始执行代码,执行到var getName= …时,修改了getName的值,赋值成了打印4的新函数

  1. 执行Foo函数的静态方法,打印出2。

  2. 执行getName,当前getName是打印出4的那个函数。

  3. 执行Foo函数,(函数内没有声明getName)修改了全局变量getName,赋值成了打印1的函数,然后返回this,因为是在全局环境下执行,所以this指向window,因为getName已经被修改了,所以打印出1。

  4. 因为getName没有被重新赋值,所以再执行仍然打印出1。

  5. new操作符是用来调用函数的,所以new Foo.getName()相当于new (Foo.getName)(),所以new的是Foo的静态方法getName,打印出2。

  6. 因为点运算符(.)的优先级和new是一样高的,所以从左往右执行,相当于(new Foo()).getName(),对Foo使用new调用会返回一个新创建的对象(实例),然后执行该对象的getName方法,该对象本身并没有该方法,所以会从Foo的原型对象上查找,找到了,所以打印出3。

  7. 和上题一样,点运算符(.)的优先级和new一样高,另外new是用来调用函数的,所以new new Foo().getName()相当于new ((new Foo()).getName)(),括号里面的就是上一题,所以最后找到的是Foo原型上的方法,无论是直接调用,还是通过new调用,都会执行该方法,所以打印出3。

第13题

1
2
3
4
5
6
7
8
9
10
11
var a = {
i: 1,
toString: function () {
return a.i++;
}
}
console.log(a == 1 && a == 2 && a == 3) // true

var a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true

答案:true

解析

  1. 两个类型不同时进行==比较时,会将一个类型转为另一个类型,然后再进行比较。比如Object类型与Number类型进行比较时,Object类型会转换为Number类型。 Object转换为Number时,会尝试调用Object.valueOf()Object.toString()来获取对应的数字基本类型。
  2. 数组调用toString()会隐含调用Array.join()方法 而数组shift方法的用法:shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。如果数组是空的,那么 shift() 方法将不进行任何操作,返回 undefined 值。请注意,该方法不创建新数组,而是直接修改原有的 数组。 所以我们可以看到 a == 1时会调用toString(),toString()调用join(),join()等于shift,则转换为Number类型后为1.

第14题

0.1 + 0.2 != 0.3

第15题

1
2
3
4
5
6
7
8
9
var x = 1;
function f(x, y = function () { x = 3; console.log(x); }) {
console.log(x)
var x = 2
y()
console.log(x)
}
f()
console.log(x)

答案:undefined、3、2、1

解析

  • 函数参数也是一个单独的作用域,所以相当于参数作用域定义了一个x , y (并且参数作用域是在函数执行产生的作用域之上)
  • 第一个x访问参数作用域的x,undefined
  • var x = 2 ,在函数作用域定义了一个x
  • 执行y()函数,访问的默认值是参数作用域的x,它被修改成了3,打印的也是3
  • f函数内部访问x,访问的最近作用域,也就是内层函数作用域,访问到的一定是2

看似简单的题,席卷几十个前端群,王红元老师都亲自出面解答