前端高频面试题整理(一)

377 阅读8分钟

1、typeof类型判断

typeof 是否能正确判断类型? instanceof 能正确判断对象的原理是什么

  • typeof 对于原始类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof 对于对象来说,除了函数都会显示 object ,所以说 typeof 并 不能准确判断变量到底是什么类型

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

如果我们想判断⼀个对象的正确类型,这时候可以考虑使⽤ instanceof , 因为内部机制是通过原型链来判断的

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true

对于原始类型来说,你想直接通过 instanceof 来判断类型是不⾏的

2、类型转换

⾸先我们要知道,在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串 转Boolean

在条件判断时,除了 undefined , null , false , NaN , '' , 0 , -0 ,其他所有值都转为 true ,包括所有对象 对象转原始类型 对象在转换类型的时候,会调⽤内置的 ToPrimitive 函数,对于该函数来说,算法逻辑⼀般来说如下

  • 如果已经是原始类型了,那就不需要转换了
  • 调⽤ x.valueOf() ,如果转换为基础类型,就返回转换的值
  • 调⽤ x.toString() ,如果转换为基础类型,就返回转换的值
  • 如果都没有返回原始类型,就会报错

当然你也可以重写 Symbol.toPrimitive ,该⽅法在转原始类型时调⽤优先级最⾼。

var a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
  [Symbol.toPrimitive]() {
    return 2;
  },
};
console.log(1 + a); // 3

var b = {
  valueOf() {
    return 11;
  },
  toString() {
    return 10;
  },
};
console.log(1 + b); // 12

var c = {
  toString() {
    return 5;
  },
};
console.log(1 + c); // 6
    

四则运算符

它有以下⼏个特点:

  • 运算中其中⼀⽅为字符串,那么就会把另⼀⽅也转换为字符串
  • 如果⼀⽅不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
  • 对于第⼀⾏代码来说,触发特点⼀,所以将数字 1 转换为字符串,得到结果 '11'
  • 对于第⼆⾏代码来说,触发特点⼆,所以将 true 转为数字 1
  • 对于第三⾏代码来说,触发特点⼆,所以将数组通过 toString 转为字符串 1,2,3 ,得到结果 41,2,3

另外对于加法还需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
  • 因为 + 'b' 等于 NaN ,所以结果为 "aNaN" ,你可能也会在⼀些代码中看到过 +'1' 的形式来快速获取 number 类型。
  • 那么对于除了加法的运算符来说,只要其中⼀⽅是数字,那么另⼀⽅就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

⽐较运算符

  • 如果是对象,就通过 toPrimitive 转换对象
  • 如果是字符串,就通过 unicode 字符索引来⽐较
let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
};
a > -1; // true

在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再⽐较值。

3、This

我们先来看⼏个函数调⽤的场景

function foo() {
  console.log(this.a);
}
var a = 1;
foo();
const obj = {
  a: 2,
  foo: foo,
};
obj.foo();
const c = new foo();
  • 对于直接调⽤ foo 来说,不管 foo 函数被放在了什么地⽅, this ⼀定是 window
  • 对于 obj.foo() 来说,我们只需要记住,谁调⽤了函数,谁就是 this ,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 对于 new 的⽅式来说, this 被永远绑定在了 c 上⾯,不会被任何⽅式改变 this

说完了以上⼏种情况,其实很多代码中的 this 应该就没什么问题了,下⾯ 让我们看看箭头函数中的 this

function a() {
  return () => {
    return () => {
      console.log(this);
    };
  };
}
console.log(a()()());
  • ⾸先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第⼀个普通函数的 this 。在这个例⼦中,因为包裹箭头函数的第⼀个普通函数是 a ,所以此时的 this 是 window 。另外对箭头函数使⽤ bind 这类函数是⽆效的。
  • 最后种情况也就是 bind 这些改变上下⽂的 API 了,对于这些函数来说, this 取决于第⼀个参数,如果第⼀个参数为空,那么就是 window 。
  • 那么说到 bind ,不知道⼤家是否考虑过,如果对⼀个函数进⾏多次 bind ,那么上下⽂会是什么呢?
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?

如果你认为输出结果是 a ,那么你就错了,其实我们可以把上述代码转换成另⼀种形式

// fn.bind().bind(a) 等于
let fn2 = function fn1() {
  return function () {
    return fn.apply();
  }.apply(a);
};
fn2();

可以从上述代码中发现,不管我们给函数 bind ⼏次, fn 中的 this 永远由第⼀次 bind 决定,所以结果永远是 window

let a = { name: 'poetries' };
function foo() {
  console.log(this.name);
}
foo.bind(a)(); // => 'poetries'

以上就是 this 的规则了,但是可能会发⽣多个规则同时出现的情况,这时候不同的规则之间会根据优先级最⾼的来决定 this 最终指向哪⾥。

⾸先, new 的⽅式优先级最⾼,接下来是 bind 这些函数,然后是obj.foo() 这种调⽤⽅式,最后是 foo 这种调⽤⽅式,同时,箭头函数的this ⼀旦被绑定,就不会再被任何⽅式所改变。

4、== 和 === 有什么区别

对于 == 来说,如果对⽐双⽅的类型不⼀样的话,就会进⾏类型转换 假如我们需要对⽐ x 和 y 是否相同,就会进⾏如下判断流程

  1. ⾸先会判断两者类型是否相同。相同的话就是⽐⼤⼩了
  2. 类型不相同的话,那么就会进⾏类型转换
  3. 会先判断是否在对⽐ null 和 undefined ,是的话就会返回 true
  4. 判断两者类型是否为 string 和 number ,是的话就会将字符串转换为 number
1 == '1'  ->  1 == 1
  1. 判断其中⼀⽅是否为 boolean ,是的话就会把 boolean 转为 number 再进⾏判断
'1' == true  ->  '1' == 1   ->  1 == 1
  1. 判断其中⼀⽅是否为 object 且另⼀⽅为 string 、 number 或者 symbol ,是的话就会把 object 转为原始类型再进⾏判断
'1' == { name: 'yck' }  ->   '1' == '[object Object]'

对于 === 来说就简单多了,就是判断两者类型和值是否相同

5、闭包

闭包的定义其实很简单:函数 A 内部有⼀个函数 B ,函数 B 可以访问到 函数 A 中的变量,那么函数 B 就是闭包

function A() {
  let a = 1;
  window.B = function () {
    console.log(a);
  };
}
A();
B(); // 1

闭包存在的意义就是让我们可以间接访问函数内部的变量

经典⾯试题,循环中使⽤闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

⾸先因为 setTimeout 是个异步函数,所以会先把循环全部执⾏完毕,这时候 i 就是 6 了,所以会输出⼀堆 6 解决办法有三种

  1. 第⼀种是使⽤闭包的⽅式
for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

在上述代码中,我们⾸先使⽤了⽴即执⾏函数将 i 传⼊函数内部,这个时候值就被固定在了参数 j 上⾯不会改变,当下次执⾏ timer 这个闭包的时候,就可以使⽤外部函数的变量 j ,从⽽达到⽬的

  1. 第⼆种就是使⽤ setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传⼊
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j);
    },
    i * 1000,
    i
  );
}
  1. 第三种就是使⽤ let 定义 i 了来解决问题了,这个也是最为推荐的⽅式
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

6、深浅拷⻉

浅拷⻉

⾸先可以通过 Object.assign 来解决这个问题,很多⼈认为这个函数是⽤来深拷⻉的。其实并不是, Object.assign 只会拷⻉所有的属性值到新的对象中,如果属性值是对象的话,拷⻉的是地址,所以并不是深拷⻉

let a = {
  age: 1,
};
let b = Object.assign({}, a);
a.age = 2;
console.log(b.age); // 1

另外我们还可以通过展开运算符 ... 来实现浅拷⻉

let a = {
  age: 1,
};
let b = { ...a };
a.age = 2;
console.log(b.age); // 1

通常浅拷⻉就能解决⼤部分问题了,但是当我们遇到如下情况就可能需要使⽤到深拷⻉了

let a = {
  age: 1,
  jobs: {
    first: 'FE',
  },
};
let b = { ...a };
a.jobs.first = 'native';
console.log(b.jobs.first); // native

浅拷⻉只解决了第⼀层的问题,如果接下去的值中还有对象的话,那么就⼜回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使⽤深拷⻉了。

深拷⻉

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

let a = {
  age: 1,
  jobs: {
    first: 'FE',
  },
};
let b = JSON.parse(JSON.stringify(a));
a.jobs.first = 'native';
console.log(b.jobs.first); // FE

但是该⽅法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引⽤的对象

7、原型

原型链就是多个对象通过 __proto__ 的⽅式连接了起来。为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了 valueOf 函数

  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • 函数的 prototype 是⼀个对象
  • 对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链

8、var、let 及 const 区别

涉及⾯试题:什么是提升?什么是暂时性死区?var、let 及 const 区别?

  • 函数提升优先于变量提升,函数提升会把整个函数挪到作⽤域顶部,变量提升只会把声明挪到作⽤域顶部
  • var 存在提升,我们能在声明之前使⽤。 let const 因为暂时性死区的原因,不能在声明前使⽤
  • var 在全局作⽤域下声明变量会导致变量挂载在 window 上,其他两者不会
  • letconst 作⽤基本⼀致,但是后者声明的变量不能再次赋值