前端面试题记录

前端面试题记录。✒️

写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么

‘key’是一个创建列表元素的时候需要包含的一个特殊的字符串属性。用来帮助React来确定某一项的添加,删除和修改。一般使用唯一的IDs来作为标识。如果没有可以使用数组的下标(不推荐)。

['1', '2', '3'].map(parseInt) what & why

parseInt

parseInt(string, radix) 将一个字符串 string 转换为 radix 进制的整数, radix 为介于2-36之间的数。

string:要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString抽象操作)。字符串开头的空白符将会被忽略。

radix:一个介于2和36之间的整数(数学系统的基础),表示上述字符串的基数。比如参数 10 表示使用十进制数值系统。

返回值:返回解析后的整数值。 如果被解析参数的第一个字符无法被转化成数值类型,则返回 NaN。

注意:
radix为 undefined,或者radix为 0 或者没有指定的情况下,JavaScript 作如下处理:

  • 如果字符串 string 以”0x”或者”0X”开头, 则基数是16 (16进制).
  • 如果字符串 string 以”0”开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
  • 如果字符串 string 以其它任何值开头,则基数是10 (十进制)。

所以这题的实际代码为

1
2
3
['1', '2', '3'].map((item, index) => {
return parseInt(item, index)
})

即返回值是:

1
2
3
parseInt('1', 0) // 1
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN, 3不是二进制

所以这道题的答案是:['1', '2', '3'].map(parseInt) // 1, NaN, NaN

什么是防抖和节流?有什么区别?如何实现

  1. 防抖

触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间

  • 思路:

每次触发事件时都取消之前的延时调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function debounce(fn) {
let timeout = null; // 创建一个标记用来存放定时器的返回值
return function () {
clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
fn.apply(this, arguments);
}, 500);
};
}
function sayHi() {
console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖
  1. 节流

高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率

  • 思路:

每次触发事件时都判断当前是否有等待执行的延时函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function throttle(fn) {
let canRun = true; // 通过闭包保存一个标记
return function () {
if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
canRun = false; // 立即设置为false
setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

介绍下 Set、Map、WeakSet 和 WeakMap 的区别

详情🔎

ES5/ES6 的继承除了写法以外还有什么区别

  1. ES5 和 ES6 子类 this 生成顺序不同。ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例,ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。
  2. ES6子类可以直接通过 proto 寻址到父类。而通过 ES5 的方式,Sub.proto === Function.prototype
1
2
3
4
5
6
class Super {}
class Sub extends Super {}

const sub = new Sub();

Sub.__proto__ === Super;
1
2
3
4
5
6
7
8
9
function Super() {}
function Sub() {}

Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

var sub = new Sub();

Sub.__proto__ === Function.prototype;
  1. 区别于ES5的继承,ES6的继承实现在于使用super关键字调用父类,反观ES5是通过call或者apply回调方法调用父类。

setTimeout、Promise、Async/Await 的区别

主要是来考察三者在事件循环中的区别。事件循环中分为宏观任务队列微观任务队列

JS是单线程的,对于异步操作只能先将其放在一边,按照某种规则按先后顺序放在某个容器中(其实就是宏观任务队列和微观任务队列)。先处理同步任务,再处理异步任务。异步任务分为宏观任务队列和微观任务队列。

宏观任务

  • script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI交互事件
  • postMessage
  • MessageChannel
  • setImmediate(Node.js 环境)

微观任务

  • Promise.then
  • MutaionObserver
  • process.nextTick(Node.js 环境)
  • async/await实际上是promise+generator的语法糖,也就是promise,也就是微观任务

执行顺序是同步任务结束后,先处理微观任务后处理宏观任务

setTimeout

1
2
3
4
5
6
console.log('script start')	//1. 打印 script start
setTimeout(() => {
console.log('setTimeout') // 4. 打印 settimeout
}, 0) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->setTimeout

注:就算设置的是0秒后执行,输出顺序也不会变

Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('script start') // 1.同步任务
let promise1 = new Promise(function (resolve) {
console.log('promise1') // 2. 同步任务
resolve(console.log('resolve end')) // 3.同步任务
console.log('promise1 end') // 4. 同步任务
}).then(function () {
console.log('promise2') // 6. 微观任务
})
setTimeout(function(){
console.log('setTimeout') // 7. 宏观任务
})
console.log('script end') // 5. 同步任务
// 输出顺序: script start -> promise1 -> resolve end -> promise1 end -> script end -> promise2 -> setTimeout

Promise本身是同步的立即执行函数,而Promise.then()才是微观任务

Async/Await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2 start')
await async3();
console.log('async2 end')
}

let async3 = () => {
console.log('async3')
}

console.log('script start');
async1();
console.log('script end')

// 输出顺序 script start -> async1 start -> async2 start -> async3 -> script end -> async2 end -> async1 end

async返回一个Promise函数 ,遇到await会立即执行await后面的代码。而await下面的代码会放在微观任务队列中。

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

Async/Await 如何通过同步的方式实现异步

占位

异步笔试题

请写出下面代码的运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

运行结果:script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout

先大体看下代码:

从上往下看,先走同步队列,再走异步队列(包含微观任务队列和宏观任务队列)。

同步队列:script start → async1 start → async2 → promise1 → script end

异步队列:包括微观任务和宏观任务。

微观任务:async1 end → promise2

宏观任务: setTimeout →

算法手写题

已知如下数组:

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

1
Array.from(new Set(arr.flat(Infinity))).sort((a, b) => a-b)

arr.flat(Infinity)用于将嵌套的数组拉平

new Set(arr)用于去重,返回一个类数组的Set数据结构

Array.from(set)将一个类数组的数据结构转换为真正的数组

sort用于数组的大小排序

array

JS 异步解决方案的发展历程以及优缺点

回调函数(callback)

1
2
3
setTimeout(() => {
// callback 函数体
}, 1000)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return

回调地狱的根本问题在于:

  • 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转
  • 嵌套函数过多的多话,很难处理错误
1
2
3
4
5
6
7
8
9
ajax('XXX1', () => {
// callback 函数体
ajax('XXX2', () => {
// callback 函数体
ajax('XXX3', () => {
// callback 函数体
})
})
})

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)

Promise

Promise就是为了解决callback的问题而产生的。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题

1
2
3
4
5
6
7
8
9
10
ajax('XXX1')
.then(res => {
// 操作逻辑
return ajax('XXX2')
}).then(res => {
// 操作逻辑
return ajax('XXX3')
}).then(res => {
// 操作逻辑
})

缺点:无法取消 Promise ,错误需要通过回调函数来捕获

Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

1
2
3
4
5
6
7
8
9
function *fetch() {
yield ajax('XXX1', () => {})
yield ajax('XXX2', () => {})
yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

Async/await

async、await 是异步的终极解决方案

优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

1
2
3
4
5
6
7
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch('XXX1')
await fetch('XXX2')
await fetch('XXX3')
}

下面来看一个使用 await 的例子:

1
2
3
4
5
6
7
8
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1

对于以上代码你可能会有疑惑,让我来解释下原因

  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generatorgenerator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
  • 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。

Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

由下图可知,Promise构造函数是同步的,then方法是异步的

  • Promise.then是微观任务
  • setTimeout是宏观任务
  • 执行顺序:同步 -> 微观 -> 宏观

promise

如何实现一个 new

new运算符的工作原理

  1. 一个新对象被创建,它继承自构造函数的prototypefoo.prototype
  2. 构造函数被执行,执行的时候,相应的参数会被传入,同时上下文(this)会被指定为这个新的实例。new foonew foo()相同,只能用在不传递任何参数的情况。
  3. 如果构造函数返回了一个对象,那么这个对象会取代返回出来的结果。如果构造函数没有返回出来对象,那么new出来的结果为步骤1创建的结果。
1
2
3
4
5
function _new(fn, ...arg) {
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret : obj;
}

React 中 setState 什么时候是同步的,什么时候是异步的?

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

原因:在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state

这里所说的同步异步, 并不是真正的同步异步, 它还是同步执行的。

这里的异步指的是多个state会合成到一起进行批量更新。

React setState 笔试题,下面的代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}

componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log

this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log

setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log

this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log
}, 0);
}

render() {
return null;
}
};

答案为:0,0,2,3

  1. 第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdatestrue,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。

  2. 两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。

  3. setTimeout 中的代码,触发时isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。

isBatchingUpdates 默认值为 false,当 react 自身的事件处理函数或 react 生命周期触发时,isBatchingUpdates 会被赋值为 true,当更新完成时又会被复原为 false。

有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

1. Object.prototype.toString.call()

每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。

1
2
3
const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"

这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。

1
2
3
4
5
6
7
Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"

Object.prototype.toString.call() 常用于判断浏览器内置对象时。

2. instanceof

instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false

1
[]  instanceof Array; // true

instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。

1
[]  instanceof Object; // true

3. Array.isArray()

  • 功能:用来判断对象是否为数组

  • instanceof 与 isArray

    当检测Array实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    xArray = window.frames[window.frames.length-1].Array;
    var arr = new xArray(1,2,3); // [1,2,3]

    // Correctly checking for Array
    Array.isArray(arr); // true
    Object.prototype.toString.call(arr); // true
    // Considered harmful, because doesn't work though iframes
    arr instanceof Array; // false
  • Array.isArray()Object.prototype.toString.call()

    Array.isArray()是ES5新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

    1
    2
    3
    4
    5
    if (!Array.isArray) {
    Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
    };
    }

介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景

观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助第三方来实现调度的,发布者和订阅者之间互不感知

  1. 发布-订阅模式就好像报社, 邮局和个人的关系,报纸的订阅和分发是由邮局来完成的。报社只负责将报纸发送给邮局。
  2. 观察者模式就好像 个体奶农和个人的关系。奶农负责统计有多少人订了产品,所以个人都会有一个相同拿牛奶的方法。奶农有新奶了就负责调用这个方法。

全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?

在ES5中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。

1
2
3
4
5
var a = 12;
function f(){};

console.log(window.a); // 12
console.log(window.f); // f(){}

但ES6规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

1
2
3
4
5
let aa = 1;
const bb = 2;

console.log(window.aa); // undefined
console.log(window.bb); // undefined

在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中

怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window(global)呗。

1
2
3
4
5
let aa = 1;
const bb = 2;

console.log(aa); // 1
console.log(bb); // 2

改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。

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

方法一

1
2
3
4
5
for (let i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}

原理:利用 let 变量的特性 — 在每一次 for 循环的过程中,let 声明的变量会在当前的块级作用域里面(for 循环的 body 体,也即两个花括号之间的内容区域)创建一个文法环境(Lexical Environment),该环境里面包括了当前 for 循环过程中的 i

方法二

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

原理:利用 setTimeout 函数的第三个参数,会作为回调函数的第一个参数传入

Author: YangLeLe
Link: http://younglele.cn/front-end-interview/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.