面题解析|看似简单,实则内容丰富

277 阅读12分钟

引言

本博客旨在学习记录,请指正,勿苛责

面题来源:前端JS选择题

1.请问JS中的基本类型有几种?

  • A.5
  • B.6
  • C.7
答案

答案: C

解析:JS的数据类型共8种,ES5,6种:Boolean、undefined、Null、String、Number、object。ES6 中新增了一种 Symbol 。这种类型的对象是唯一的,目的是为了解决属性名冲突的问题,作为标记。

谷歌67版本中还出现了一种 bigInt。是指安全存储、操作大整数,但是很多人不把这个做为一个类型,并且暂时没有进入ES版本中

2.下面代码的输出是什么?

function sayHi(){
    console.log(name);
    console.log(age);
    var name = "TJH";
    let age = 24;
}
sayHi();
  • A.TJH和undefined
  • B.TJH和ReferenceError
  • C.ReferenceError和24
  • undefined和ReferenceError
答案

答案: D

解析:在函数内部,我们首先通过 var 关键字声明了 name 变量。这意味着变量被提升了(内存空间在创建阶段就被设置好了),直到程序运行到定义变量位置之前默认值都是 undefined 。因此当我们打印 name 变量时还没有执行到定义变量的位置,因此变量的值保持为 undefined。

通过 let 和 const 关键字声明的变量也会提升,但是和 var 不同,它们不会被初始化。在我们声明(初始化)之前是不能访问它们的。这个行为被称之为暂时性死区。当我们试图在声明之前访问它们时,JavaScript 将会抛出一个 ReferenceError 错误。

思考:let和var的区别,1.var变量会发生变量提升,let则不会进行变量提升,即未声明变量的调用表现不同,var:undefined,let:报错ReferenceError,这边会有一个暂时性死区的约束;2.重复声明同一变量,var:后声明的覆盖前声明的,let:报错SyntaxError,即只能声明一次;3.作用域不同,var是全局作用域,即声明后均可调用,let是块级作用域,只有当前块级环境可以调用

PS:JS环境中程序先寻找当前块级环境,找不到再逐层向外寻找

3.下面代码的输出是什么?

for(var i =0 ;i<3;i++){
    setTimeout(()=>console.log(i),1)
}

for(let i =0;i<3;i++){
    setTimeout(()=>console.log(i),1)
}
  • A. 0 1 2 and 0 1 2
  • B. 0 1 2 and 3 3 3
  • C. 3 3 3 and 0 1 2
答案

答案: C

解析:由于 JavaScript 的事件循环,setTimeout 回调会在遍历结束后才执行。因为在第一个遍历中遍历 i 是通过 var 关键字声明的,所以这个值是全局作用域下的。在遍历过程中,我们通过一元操作符 ++ 来每次递增 i 的值。当 setTimeout 回调执行的时候,i 的值等于 3。

在第二个遍历中,遍历 i 是通过 let 关键字声明的:通过 let 和 const 关键字声明的变量是拥有块级作用域(指的是任何在 {} 中的内容)。在每次的遍历过程中,i 都有一个新值,并且每个值都在循环内的作用域中。

思考:我的理解是第一个函数的 i 是一块并且跟着循环(被调用了),function是一块(循环) ,setTimeout是一块,而第二个函数的 function是一块,i 和setTimeout是一块,每次循环丢到一个地方(堆/栈?)等着执行,题目呢,我大概是这样理解的,但是上面的事件循环有必要进行深入地了解。

4.下面代码的输出是什么?

let a = 666;
let b = new Number(666);
let c = 666;
console.log(a == b);
console.log(a === b);
console.log(b === c);
  • A. true false true
  • B. false false true
  • C. true false false
  • D.false true true
答案

答案: C

解析:new Number() 是一个内建的函数构造器。虽然它看着像是一个 number,但它实际上并不是一个真实的 number:它有一堆额外的功能并且它是一个对象。

当我们使用 == 操作符时,它只会检查两者是否拥有相同的值。因为它们的值都是 3,因此返回 true。

然后,当我们使用 === 操作符时,两者的值以及类型都应该是相同的。new Number() 是一个对象而不是 number,因此返回 false。

思考:这一块比较好理解,new Number() 是一个对象,它会输出一个值。 == 比较的是值,而===比较的不仅是值,还有类型、引用的内存地址等是否都相等,所以不相等

5.下面代码的输出是什么?

const a = {};
const b= { key:"b" };
const c = { key :"c" };
a[b] = 123 ;
a[c] = 456;
console.log(a[b]);
  • A.123
  • B.456
  • C.undefined
  • D.ReferenceError
答案

答案: B

解析:用对象作为key,先会被隐式转换为字符串,其值为[object object],所以a[b]其实是a[object object]后面的a[c]也是一样,转换后的key值一样,所以被重写,值为456.

思考:我尝试了一下function也与上述相同,字符串和Number正常被重写,是不是基本类型的会被重写,引用类型的则跟上述一样,这只是我的猜测,未得到明确的答案

6.下面代码的输出是什么?

const numbers = [1,2,3];
numbers[10] = 11;
console.log(numbers);
  • A.[1,2,3,7x null,11]
  • B.[1,2,3,11]
  • C.[1,2,3,7x empty,11]
  • D.SyntaxError
答案

答案: C

解析:数组中的empty,即空元素,相当于undefined,但是使用forEach方法会直接跳过,forEach()方法按升序为数组中含有有效值的每一项执行一次callback函数,哪些已删除或者未初始化的项将被跳过。可以用for循环解决这个问题:

for(let number of numbers){

console.log(number)

}

此时会打印出1、2、3、7个undefined、11

7.下面代码的输出是什么?

let number = 0;
console.log(number++);
console.log(++number);
console.log(number);
  • A.1 1 2
  • B.1 2 2
  • C.0 2 2
  • D.0 1 2
答案

答案: C

解析:这个知识点比较简单,JS的算数运算符,++在后先输出再计算,++在前先计算再输出

8.下面代码的输出是什么?

let obj1 = {
    name:'obj1_name',
    print:function(){
        return ()=>console.log(this.name)
    }
}
let obj2 = {name:'obj2_name'}
obj1.print()()
obj1.print().call(obj2)
obj1.print.call(obj2)()
  • A.obj1_name obj2_name obj2_name
  • B.obj2_name obj1_name obj2_name
  • C.obj1_name obj1_name obj2_name
答案

答案: C

解析:先看下第一个调用:拆开两部分,第一部分是obj.print,第二部分是()(),第二部分分别未两个匿名函数function(){}以及()=>{},当执行obj.print()()时,this的指向是print函数的调用者,也就是obj1,所以输出'obj1_name'

第二个调用:与第一个调用相同,不同点是call()无法改变this的指向,因为是箭头函数,所以输出的仍然是'obj1_name'

第三个调用:我们只需要看前面一部分,obj.print.call(obj2),注意这里先是改变调用然后再执行函数,因为这边调用的先是属性然后才是方法的执行,翻译过来也就是obj2.print(),然后再接上(),根据箭头函数的定义,这里this的指向是obj2,所以输出'obj2_name'

9.下面代码的输出是什么?

const obj = { 1:"a", 2:"b",3:"c"};
const set = new Set ([1,2,3,4,5]);
obj.hasOwnProperty("1");
obj.hasOwnProperty(1);
set.has("1");
set.has(1);
  • A. false true false true
  • B.false true true true
  • C.true true false true
  • D.true true true true
答案

答案: C

解析:与5的知识点有点关系,Number作为key的时候会被隐式转换成字符串,所以前两个是一样的,都输出true,那么下面的set方法我就不多赘述了,false true 因为一个是字符串,一个是Number

10.下面代码的输出是什么?

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();
  • A.4 2 1 1 2 3 3
  • B.2 1 4 1 2 3 3
  • C.2 4 1 1 3 2 3
  • D.2 4 1 1 2 3 3
答案

答案: D

解析:这是一个比较复杂的点,包含原型链、函数声明(function)与函数表达式(var ...)的区别、new关键字,先来看下,new关键字做了什么事:

1.创建一个新的对象,这个对象的类型是object;

2.设置这个新的对象的内部、可访问性和prototype属性为构造函数

3.执行构造函数、当this关键字被提及的时候,使用新创建的对象的属性,返回新创建的对象

4.在创建对象成功之后,如果调用一个新对象没有属性的时候,javaScript会延原型链向止逐层查找对应的内容,这类似于传统的类继承。

下面我们来看调用,

Foo.getName()//这边输出2没什么问题

getName();//函数声明会提前,所以function getName()会被var getName()覆盖,所以输出4,对于这个仍有疑问可以参考:函数声明与函数表达式

Foo().getName()//这个输出1没什么问题,同时函数外的getName方法又被覆盖了,因为下面的return this

getName();//由于上面,所以输出1

new Foo.getName() //首先看运算优先级把,new Foo() >Foo() > new Foo ,先运算Foo.getName = function(){}结果为2,再new一个Foo的实例对象,进行输出2

new Foo().getName() //先执行new Foo(),构建了一个新的实例对象,并继承了Foo()这个构造函数中的getName方法,再执行Foo.prototype.getName ,所以输出了3

new new Foo().getName() //同样先执行new Foo(),变成了new Foo实例对象.getName(),然后再执行 Foo实例对象.getName(),又回到了Foo.prototype.getName,所以还是输出3;关于这一块有疑虑可以参考:new

11.下面代码的输出是什么?

async function async1(){
    console.log('async1 start');
    await async2()
    console.log('async1 end');
}

async function async2(){
    console.log('async2')
}

console.log('script start')

setTimeout(function(){
    console.log('setTimeout0')
},0)

setTimeout(function(){
    console.log('setTimeout3')
},3)

setImmediate(()=>console.log('setImmediate'));

process.nextTick(()=>console.log('nextTick'));

async1();

new Promise(function(resolve){
    console.log('promise1');
    resolve();
    console.log('promise2');
}).then(function(){
    console.log('promise3');
})

console.log('script end')
  • A.script start - async2 start - async1 - promise1 - promise2 -script end - nextTick - async1 end - promise3 - setTimeout0 - setImmediate - setTimeout3
  • B.script start - async1 start - async2 - promise2 - promise1 -script end - nextTick - async1 end - promise3 - setTimeout0 - setImmediate - setTimeout3
  • C.script start - async1 start - async2 - promise1 - promise2 -script end - nextTick - async1 end - promise3 - setTimeout3 - setImmediate - setTimeout0
  • D.script start - async1 start - async2 - promise1 - promise2 -script end - nextTick - async1 end - promise3 - setTimeout0 - setImmediate - setTimeout3
答案

答案: D

PS:这题我的理解和答案不一致,可参考,勿全信,这题知识点比较多,我自己也半懂不懂,请慎重!!!跪求大神能给出完整的解析!!!

解析:这考察的是js中的事件循环和回调队列

先汇总一下Event Loop事件循环的执行顺序:

  • 1.首先执行同步任务(宏任务)
  • 2.执行完所有同步任务代码之后,执行栈为空,查询是否有异步代码需要执行
  • 3.执行完所有的微任务,然后会渲染页面
  • 4.开始下一轮的Event Loop,执行宏任务中的异步代码,也就是setTimeout中的回调函数

宏任务:

  • 1.script
  • 2.setTimeout
  • 3.setInterval
  • 4.setImmediate
  • 5.I/O
  • 6.UI rendering

微任务:

  • 1.process.nextTick(node 独有)
  • 2.promise
  • 3.MutationObserver
  • 4.setImmediate
  • 5.I/O
  • 6.UI rendering

ok,我们首先看下题目,整理简化下

下面标记为代码行

1 async function async1(){}

2 async function async2(){}

3 console.log('script start')

4 setTimeout(0)

5 setTimeout(3)

6 setImmediate()

7 nextTick()

8 async1();

9 new Promise()

10 console.log('script end')

然后按顺序执行,首先3被调用执行,输出script start,然后执行到4,setTimeout是宏任务,会等到执行栈(调用栈)清空之后,微任务全部执行完毕之后,才会去执行,所以4会被挂起,最后执行,同样5会被挂起在4后面,下面说的 4 5 6 7 都会被挂起,然后说一下6,这个方法按照网上资料来说一般不推荐使用,但是遇到了就先看一看。

首先进入的是timers阶段,如果我们的机器性能一般,那么进入timers阶段,一毫秒已经过去了(setTimeout(fn, 0)等价于setTimeout(fn, 1)),那么setTimeout的回调会首先执行。

如果没有到一毫秒,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,此时有代码被setImmediate(),于是先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。而我们在执行代码的时候,进入timers的时间延迟其实是随机的,并不是确定的,所以会出现函数执行顺序随机的情况。

所以在我的电脑上的顺序是 3 6 4 5 ,而process.nextTick()采用的是idle观察者,它可以强势插入,在第一轮结束的时候,所以现在的顺序是 3 7 6 4 5 然后 8 9 10,根据上面挂起的情况,所以 3 8 9 10 7 6 4 5,await和.then方法开启第二轮调用,所以nextTick在 promise3前面 ,script end后面

再看看第一轮循环,也就是3 8 9 ,3没什么问题,然后看下8,首先输出1 start没问题,await 意思是等一下,等着async2()这个函数执行完毕,然后再继续,等到之后,对于await来说,分2个情况:不是promise对象、是promise对象

如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果,如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。所以就执行就先进入async2中,打印出async2,然后打印出async1 end ,因为返回的是非promise,所以先执行外面的同步代码,然后执行Promise,先打印出promise1,resolve()是用来表示promise的状态为fullfilled,相当于只是定义了一个有状态的Promise,但是并没有调用它;promise调用then的前提是promise的状态为fullfilled;只有promise调用then的时候,then里面的函数才会被推入微任务中

所以按照我的理解,它的输出应该是:

script start - async1 start - async2 -async1 end-promise1 - promise2-script end -nextTick - promise3 - setImmediate -setTimeout0 - setTimeout3