基础题

变量赋值

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

函数多个参数设置默认值,跟按顺序定义变量一样,存在暂时性死区。前面定义的变量不能引用后面还未定义的变量。

  1. let a = b = 10
  2. // 上句写法等同于 let a = 10; b = 10;
  3. ;(function(){
  4. let a = b = 20; // 等同于 let a = 20; b = 20;
  5. })()
  6. console.log(a)
  7. console.log(b)
  8. // 10 20
  1. var a = {n:1}
  2. var b = a
  3. a.x = a = {n:2} // . 操作符运算等级最高 a.x = {n:1,x:undefined},
  4. // 然后按照连等操作从右到左执行代码,a = {n: 2}
  5. // a指向新对象,之后a.x = a,由于a.x在最开始执行过
  6. // 此时等价于({n: 1, x: undefined}).x = b.x = a = {n: 2}
  7. console.log(a.x) //undefined
  8. console.log(b.x) // {n:2}

变量提升和作用域

  1. var scope = 'global scope'
  2. function a(){
  3. function b(){
  4. console.log(scope)
  5. }
  6. return b
  7. var scope = 'local scope'; // var会存在变量提升,如果是let,则会报错。
  8. }
  9. a()() // 输出undefined
  1. console.log(a)
  2. var a = 1
  3. var getNum = function() {
  4. a = 2
  5. }
  6. function getNum() {
  7. a = 3
  8. }
  9. console.log(a)
  10. getNum()
  11. console.log(a)
  12. // 答案 undefined 1 2
  1. var i = 1
  2. function b() {
  3. console.log(i) //使用的是全局作用域下的值
  4. }
  5. function a() {
  6. var i = 2
  7. b()
  8. }
  9. a();
  10. // 结果输出 1

变量声明和函数声明同时存在

  1. var a = 1
  2. function a(){}
  3. console.log(a)
  4. // 第一行的变量声明会被函数声明覆盖 FnA varA,执行到console.log时,又在之前进行了赋值a=1
  5. var b
  6. function b(){}
  7. console.log(b) // 打印出b函数
  8. function b(){}
  9. var b
  10. console.log(b) // 打印出b函数

输出结果: 1
[Function: b]
[Function: b]

  1. function Foo() {
  2. getName = function () { console.log(1) }
  3. return this; //this表示的是window
  4. }
  5. Foo.getName = function () { console.log(2) }
  6. Foo.prototype.getName = function () { console.log(3) }
  7. var getName = function () { console.log(4) }
  8. function getName() { console.log(5) }
  9. //请写出以下输出结果:
  10. Foo.getName()
  11. getName()
  12. Foo().getName()
  13. getName()
  14. new Foo.getName()
  15. new Foo().getName()
  16. new new Foo().getName()

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

  1. 执行Foo函数的静态方法,打印出2。
  2. 执行getName,当前getName是打印出4的那个函数
  3. 执行Foo函数,修改了全局变量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

结果:2、4、1、1、2、3、3

Es6+新语法

Object.assign

  1. console.log(Object.assign([1, 2, 3], [4, 5])); // [4,5,3]
  1. const obj = {
  2. a: {
  3. a: 1
  4. }
  5. };
  6. const obj1 = {
  7. a: {
  8. b: 1
  9. }
  10. };
  11. // assign方法执行的是浅拷贝
  12. console.log(Object.assign(obj, obj1)) //{a: { b: 1}}

for … of

  1. var arr = [0, 1, 2]
  2. arr[10] = 10
  3. // filter ES6之前的遍历方法都会跳过数组未赋值过的位置
  4. console.log(arr.filter(a=>a === undefined)); // []
  5. // for of为ES6新增方法,不会跳过undefined的位置
  6. for(let x of arr){
  7. console.log(arr[x], x);
  8. }

类型转换

  1. console.log(1 + NaN)
  2. console.log("1" + 3)
  3. console.log(1 + undefined)
  4. console.log(1 + null)
  5. console.log(1 + {})
  6. console.log(1 + [])
  7. console.log([] + {})
  8. // console.log([].toString()) //为空
  9. // console.log({}.toString()) //[object Object]

答案:NaN、13、NaN、1、1[object Object]、1、[object Object]

  1. 如果有一个操作数是字符串,那么把另一个操作数转成字符串执行连接
  2. 如果有一个操作数是对象,那么调用对象的valueOf方法转成原始值,如果没有该方法或调用后仍是非原始值,则调用toString方法
  3. 其他情况下,两个操作数都会被转成数字执行加法操作
  1. let a = {},
  2. b={key:'b'},
  3. c={key:'c'}
  4. a[b]=123
  5. a[c]=456
  6. console.log(a[b]) // 456
  1. console.log(typeof undefined == typeof NULL)
  2. console.log(typeof function () {} == typeof class {})

this指向

  1. var out = 25
  2. var inner = {
  3. out: 20,
  4. func: function () {
  5. var out = 30
  6. return this.out
  7. }
  8. };
  9. console.log((inner.func, inner.func)())
  10. console.log(inner.func())
  11. console.log((inner.func)())
  12. console.log((inner.func = inner.func)())

答案:25,20,20,25

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

    ++i和i++

    1. var x=1
    2. switch(x++)
    3. {
    4. case 0: ++x
    5. case 1: ++x
    6. case 2: ++x
    7. }
    8. console.log(x) // 4
    9. // 第一次 i++时,i为1,进入switch,执行case 1,先会让i++生效, i为2 然后++i,i为3;
    10. // 由于没break 执行后边case 2,然后++i,i为4;

    综合题

    事件循环【微任务和宏任务】

    1. setTimeout(function() {
    2. console.log(1)
    3. }, 0)
    4. new Promise(function(resolve) {
    5. console.log(2)
    6. for( var i=0 ; i<10000 ; i++ ) {
    7. i == 9999 && resolve()
    8. }
    9. console.log(3)
    10. }).then(function() {
    11. console.log(4)
    12. })
    13. console.log(5)
    14. 复制代码
    答案:2、3、5、4、1
    分析:js是一门单线程的语言,但是为了执行一些异步任务时不阻塞代码,以及避免等待期间的资源浪费,js存在事件循环的机制,单线程指的是执行js的线程,称作主线程,其他还有一些比如网络请求的线程、定时器的线程,主线程在运行时会产生执行栈,栈中的代码如果调用了异步api的话则会把事件添加到事件队列里,只要该异步任务有了结果便会把对应的回调放到【任务队列】里,当执行栈中的代码执行完毕后会去读取任务队列里的任务,放到主线程执行,当执行栈空了又会去检查,如此往复,也就是所谓的事件循环。
    异步任务又分为【宏任务】(比如setTimeout、setInterval)和【微任务】(比如promise),它们分别会进入不同的队列,执行栈为空完后会优先检查微任务队列,如果有微任务的话会一次性执行完所有的微任务,然后去宏任务队列里检查,如果有则取出一个任务到主线程执行,执行完后又会去检查微任务队列,如此循环。
    回到这题,首先整体代码作为一个宏任务开始执行,遇到setTimeout,相应回调会进入宏任务队列,然后是promise,promise的回调是同步代码,所以会打印出2,for循环结束后调用了resolve,所以then的回调会被放入微任务队列,然后打印出3,最后打印出5,到这里当前的执行栈就空了,那么先检查微任务队列,发现有一个任务,那么取出来放到主线程执行,打印出4,最后检查宏任务队列,把定时器的回调放入主线程执行,打印出1。
  1. console.log('1');
  2. setTimeout(function() {
  3. console.log('2');
  4. process.nextTick(function() {
  5. console.log('3');
  6. });
  7. new Promise(function(resolve) {
  8. console.log('4');
  9. resolve();
  10. }).then(function() {
  11. console.log('5');
  12. });
  13. });
  14. process.nextTick(function() {
  15. console.log('6');
  16. });
  17. new Promise(function(resolve) {
  18. console.log('7');
  19. resolve();
  20. }).then(function() {
  21. console.log('8');
  22. });
  23. setTimeout(function() {
  24. console.log('9');
  25. process.nextTick(function() {
  26. console.log('10');
  27. })
  28. new Promise(function(resolve) {
  29. console.log('11');
  30. resolve();
  31. }).then(function() {
  32. console.log('12')
  33. });
  34. })

答案:1、7、6、8、2、4、9、11、3、10、5、12
这道题和上一题差不多,但是出现了process.nextTick,所以显然是在node环境下,node也存在事件循环的概念,但是和浏览器的有点不一样,nodejs中的宏任务被分成了几种不同的阶段,两个定时器属于timers阶段,setImmediate属于check阶段,socket的关闭事件属于close callbacks阶段,其他所有的宏任务都属于poll阶段,除此之外,只要执行到前面说的某个阶段,那么会执行完该阶段所有的任务,这一点和浏览器不一样,浏览器是每次取一个宏任务出来执行,执行完后就跑去检查微任务队列了,但是nodejs是来都来了,一次全部执行完该阶段的任务好了,那么process.nextTick和微任务在什么阶段执行呢,在前面说的每个阶段的后面都会执行,但是process.nextTick会优先于微任务,一图胜千言:
js基础面试题 - 图1
理解了以后再来分析这道题就很简单了,首先执行整体代码,先打印出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,到这里是有队列都清空了。