自用前端面试题(看代码说输出)

615 阅读10分钟

1.下面程序输出的结果是什么?(变量提升)

function sayHi() {  
  console.log(name);  
  console.log(age);  
  var name = "Lydia";  
  let age = 21;  
}  
  
sayHi();

在函数中,我们首先使用var关键字声明了name变量。这意味着变量在创建阶段会被提升(JavaScript会在创建变量创建阶段为其分配内存空间),默认值为undefined,直到我们实际执行到使用该变量的行。我们还没有为name变量赋值,所以它仍然保持undefined的值。

使用let关键字(和const)声明的变量也会存在变量提升,但与var不同,初始化没有被提升。在我们声明(初始化)它们之前,它们是不可访问的。这被称为“暂时死区”。当我们在声明变量之前尝试访问变量时,JavaScript会抛出一个ReferenceError。

关于let的是否存在变量提升,我们可以用下面的例子来验证:

let name = 'ConardLi'  
{  
  console.log(name) // Uncaught ReferenceError: name is not defined  
  let name = 'code秘密花园'  
}

let变量如果不存在变量提升,console.log(name)就会输出ConardLi,结果却抛出了ReferenceError,那么这很好的说明了,let也存在变量提升,但是它存在一个“暂时死区”,在变量未初始化或赋值前不允许访问。

变量的赋值可以分为三个阶段:

  • 创建变量,在内存中开辟空间
  • 初始化变量,将变量初始化为undefined
  • 真正赋值

关于letvarfunction

  • let的「创建」过程被提升了,但是初始化没有提升。
  • var的「创建」和「初始化」都被提升了。
  • function的「创建」「初始化」和「赋值」都被提升了。

2.下面代码输出什么?(变量提升)

var a = 10;  
(function () {  
    console.log(a)  
    a = 5  
    console.log(window.a)  
    var a = 20;  
    console.log(a)  
})()

依次输出:undefined -> 10 -> 20

在立即执行函数中,var a = 20; 语句定义了一个局部变量 a,由于js的变量声明提升机制,局部变量a的声明会被提升至立即执行函数的函数体最上方,且由于这样的提升并不包括赋值,因此第一条打印语句会打印undefined,最后一条语句会打印20。

由于变量声明提升,a = 5; 这条语句执行时,局部的变量a已经声明,因此它产生的效果是对局部的变量a赋值,此时window.a 依旧是最开始赋值的10。

3.下面的输出结果是什么?(静态方法)

class Chameleon {  
  static colorChange(newColor) {  
    this.newColor = newColor;  
  }  
  
  constructor({ newColor = "green" } = {}) {  
    this.newColor = newColor;  
  }  
}  
  
const freddie = new Chameleon({ newColor"purple" });  
freddie.colorChange("orange");
  • A: orange
  • B: purple
  • C: green
  • D: TypeError

答案: D

colorChange方法是静态的。静态方法仅在创建它们的构造函数中存在,并且不能传递给任何子级。由于freddie是一个子级对象,函数不会传递,所以在freddie实例上不存在freddie方法:抛出TypeError

4.下面代码中什么时候会输出1?

var a = ?;  
if(a == 1 && a == 2 && a == 3){  
     conso.log(1);  
}

因为==会进行隐式类型转换 所以我们重写toString方法就可以:

var a = {  
  i1,  
  toString() {  
    return a.i++;  
  }  
}  
  
if( a == 1 && a == 2 && a == 3 ) {  
  console.log(1);  
}

5.下面的输出结果是什么?

var obj = {  
    '2'3,  
    '3'4,  
    'length'2,  
    'splice'Array.prototype.splice,  
    'push'Array.prototype.push  
}  
obj.push(1)  
obj.push(2)  
console.log(obj)
  1. 使用第一次push,obj对象的push方法设置 obj[2]=1;obj.length+=1
  2. 使用第二次push,obj对象的push方法设置 obj[3]=2;obj.length+=1
  3. 使用console.log输出的时候,因为obj具有 length 属性和 splice 方法,故将其作为数组进行打印
  4. 打印时因为数组未设置下标为 0 1 处的值,故打印为empty,主动 obj[0] 获取为 undefined

image.png

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

var a = {n1};  
var b = a;  
a.x = a = {n2};  
  
console.log(a.x)       
console.log(b.x)

答案:
undefined
{n:2}

首先,a和b同时引用了{n:2}对象,接着执行到a.x = a = {n:2}语句,尽管赋值是从右到左的没错,但是.的优先级比=要高,所以这里首先执行a.x,相当于为a(或者b)所指向的{n:1}对象新增了一个属性x,即此时对象将变为{n:1;x:undefined}。之后按正常情况,从右到左进行赋值,此时执行a ={n:2}的时候,a的引用改变,指向了新对象{n:2},而b依然指向的是旧对象。之后执行a.x = {n:2}的时候,并不会重新解析一遍a,而是沿用最初解析a.x时候的a,也即旧对象,故此时旧对象的x的值为{n:2},旧对象为 {n:1;x:{n:2}},它被b引用着。
后面输出a.x的时候,又要解析a了,此时的a是指向新对象的a,而这个新对象是没有x属性的,故访问时输出undefined;而访问b.x的时候,将输出旧对象的x的值,即{n:2}。

7.下面代码的输出是什么?(相等比较)

function checkAge(data) {
if (data === { age: 18 }) {
console.log("You are an adult!");
} else if (data == { age: 18 }) {
console.log("You are still an adult.");
} else {
console.log(Hmm.. You don't have an age I guess);
}
}

checkAge({ age: 18 });

答案:Hmm.. You don't have an age I guess

在比较相等性,原始类型通过它们的值进行比较,而对象通过它们的引用进行比较。JavaScript检查对象是否具有对内存中相同位置的引用。 我们作为参数传递的对象和我们用于检查相等性的对象在内存中位于不同位置,所以它们的引用是不同的。 这就是为什么{ age: 18 } === { age: 18 }和 { age: 18 } == { age: 18 }返回 false的原因

8.下面代码的输出是什么?(属性key判断)

const obj = { 1"a"2"b"3"c" };  
const set = new Set([12345]);  
  
obj.hasOwnProperty("1");  
obj.hasOwnProperty(1);  
set.has("1");  
set.has(1);

答案:true true false true

所有对象键(不包括Symbols)都会被存储为字符串,即使你没有给定字符串类型的键。这就是为什么obj.hasOwnProperty('1')也返回true

上面的说法不适用于Set。在我们的Set中没有“1”set.has('1')返回false。它有数字类型1set.has(1)返回true

9.下面代码的输出是什么?(键名的转换)

// example 1  
var a={}, b='123', c=123;    
a[b]='b';  
a[c]='c';    
console.log(a[b]);  
  
---------------------  
// example 2  
var a={}, b=Symbol('123'), c=Symbol('123');    
a[b]='b';  
a[c]='c';    
console.log(a[b]);  
  
---------------------  
// example 3  
var a={}, b={key:'123'}, c={key:'456'};    
a[b]='b';  
a[c]='c';    
console.log(a[b]);

这题考察的是对象的键名的转换:

  • 对象的键名只能是字符串和 Symbol 类型。
  • 其他类型的键名会被转换成字符串类型。
  • 对象转字符串默认会调用 toString 方法。
// example 1  
var a={}, b='123', c=123;  
a[b]='b';  
// c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。  
a[c]='c';    
// 输出 c  
console.log(a[b]);  
  
  
// example 2  
var a={}, b=Symbol('123'), c=Symbol('123');    
// b 是 Symbol 类型,不需要转换。  
a[b]='b';  
// c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的,所以不会覆盖掉 b。  
a[c]='c';  
// 输出 b  
console.log(a[b]);  
  
  
// example 3  
var a={}, b={key:'123'}, c={key:'456'};    
// b 不是字符串也不是 Symbol 类型,需要转换成字符串。  
// 对象类型会调用 toString 方法转换成字符串 [object Object]。  
a[b]='b';  
// c 不是字符串也不是 Symbol 类型,需要转换成字符串。  
// 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉。  
a[c]='c';    
// 输出 c  
console.log(a[b]);

10.下面代码的输出是什么?(作用域)

(() => {
  let x, y;
  try {
    throw new Error();
  } catch (x) {
    (x = 1), (y = 2);
    console.log(x);
  }
  console.log(x);
  console.log(y);
})();

答案:undefined 2

catch块接收参数x。当我们传递参数时,这与变量的x不同。这个变量x是属于catch作用域的。之后,我们将这个块级作用域的变量设置为1,并设置变量y的值。现在,我们打印块级作用域的变量x,它等于1。在catch块之外,x仍然是undefined,而y是2。当我们想在catch块之外的console.log(x)时,它返回undefined,而y返回2。

11.下面代码的输出是什么?(原型)

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
Foo.prototype.a = function() {
    console.log(3)
}
Foo.a = function() {
    console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();

答案:4 2 1

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
// 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行

Foo.prototype.a = function() {
    console.log(3)
}
// 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3

Foo.a = function() {
    console.log(4)
}
// 现在在 Foo 上挂载了直接方法 a ,输出值为 4

Foo.a();
// 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以
// # 输出 4

12.看代码说输出

console.log(a)  //输出undefined
var a=1;
console.log(b);
let b=2  //报错

13.看代码说输出

for(var i=0;i<5;i++){
    setTimeout(()=>{
        console.log(i)
    })
}
// 输出 5 5 5 5 5

14.看代码说输出

function o() {
    this.name = '222'
    return {
        a() {
            console.log(this);
        },
        b: () => {
            console.log(this);
        }
    }
};
o().a(); // { a: f, b: f }
o().b(); // Window
const c = o.a;
const d = o.b;
c(); // Window
d(); // Window

15.看代码说输出

new Promise((re, rj) => {
    re(1);
    rj(2);
    console.log(3);
}).then((res) => {
    console.log(res);
}).catch((err) => {
    console.log(err);
}); // 3 1 rj代码不会执行,因为Promise状态只能变更一次

16.看代码说输出

const log = console.log;
log(1);
setTimeout(() => {
    log(2);
});
new Promise((r) => {
    log(3);
    r(4);
    log(5);
})
    .then((res) => {
        log(res);
    }); log(6);
(async () => {
    log(7);
    await new Promise((r) => {
        log(8);
        r();
    });
    log(9);
})(); // 1 3 5 6 7 8 4 9 2

19.看代码说输出

var a = 1; 
var fn = () => console.log(this.a);
var obj = {
    a: 2,
    b: fn
}
console.log(obj.b()) 
// 输出1
// 如果第一行是let a=1; 那输出结果是 undefined 

20.看代码说输出

setImmediate(function(){
  console.log(1);
},0);
setTimeout(function(){
  console.log(2);
},0);
new Promise(function(resolve){
  console.log(3);
  resolve();
  console.log(4);
}).then(function(){
  console.log(5);
});
console.log(6);
process.nextTick(function(){
  console.log(7);
});
console.log(8);

答案:3 4 6 8 7 5 2 1 首先要区分微任务和宏任务: macro-task(宏任务): script (整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering.
micro-task(微任务): process.nextTick, Promise(原生),Object.observe,MutationObserver

第一步
script整体代码被执行,执行过程为

  • 创建setImmediate macro-task
  • 创建setTimeout macro-task
  • 创建micro-task Promise.then 的回调,并执行script console.log(3); resolve(); console.log(4); 此时输出3和4,虽然resolve调用了,执行了但是整体代码还没执行完,无法进入Promise.then 流程。
  • console.log(6)输出6
  • process.nextTick 创建micro-task
  • console.log(8) 输出8
  • 第一个过程过后,已经输出了3 4 6 8

第二步
由于其他micro-task 的 优先级高于macro-task。

此时micro-task 中有两个任务按照优先级process.nextTick 高于 Promise。

所以先输出7,再输出5

第三步
micro-task 任务列表已经执行完毕,接下来执行macro-task. 由于setTimeout的优先级高于setIImmediate,所以先输出2,再输出1。

21.看代码说输出

//加入两个nextTick的回调函数
process.nextTick(function () {
  console.log('nextTick延迟执行1');
});
process.nextTick(function () { 
  console.log('nextTick延迟执行2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
  console.log('setImmediate延迟执行1'); 
  // 进入下次循环 
  process.nextTick(function () {
    console.log('强势插入');
  });
});
setImmediate(function () {
  console.log('setImmediate延迟执行2'); 
});
 
console.log('正常执行');

答案:
正常执行
nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
setImmediate延迟执行2
强势插入

在新版的Node中,process.nextTick执行完后,会循环遍历setImmediate,将setImmediate都执行完毕后再跳出循环。所以两个setImmediate执行完后队列里只剩下第一个setImmediate里的process.nextTick。最后输出”强势插入”

22.事件优先级判断标准的总结

严格的总结

用观察者优先级来体现:在每次轮训检查中,各观察者的优先级分别是idle观察者 > I/O观察者 > check观察者

idle观察者:process.nextTick
I/O观察者:一般性的I/O回调,如网络,文件,数据库I/O等
check观察者:setTimeout>setImmediate

直白的总结

  • 同步代码执行顺序优先级高于异步代码执行顺序优先级;
  • new Promise(fn)中的fn是同步执行;
  • process.nextTick()>Promise.then()>setTimeout>setImmediate。

23.看代码说输出

class A {
method1(){ console.log(this) }
method2=()=>{console.log(this)}
}
const a = new A();
a.method1(); //a
a.method2(); //a
a.method1.bind({name:'test'})(); // name:test
a.method2.bind({name:'test'})(); // name:test //a
a.method1.bind({name:'test'}).call(window) //name:test
a.method2.bind({name:'test'}).call(window) //a

25.说输出 (异步事件优先级)(TODO)

image.png

26.说输出(变量提升)

function a (){
    console.log(name)
    console.log(age)
    let age = 22
    var name = "字节"
}

//undefined
//Uncaught ReferenceError: age is not defined

详解:zhuanlan.zhihu.com/p/438563024

27.说输出(对象声明{})

function Pet(name){
    this.name = name
    this.getName = ()=>this.name
}

const cat = new Pet('cat')
cat.getName()  // 输出cat

const {getName} = cat
getName()   // 输出cat

28.'1'-['2','3']和'1'-['2']结果是什么?(JS类型转换)

NaN
-1
解析见 juejin.cn/post/719031… 中的类型转换原则