前端面试题整理 - JavaScript 篇
1. 防抖与节流的区别
防抖(debounce): 当连续触发事件时,如果触发间隔小于设定的时间,那么只会执行最后一次触发时的事件处理函数,每次触发事件重新开始计时。
原理是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
节流(throttle): 当连续触发事件时,一定时间内只会执行一次函数,执行函数后重新开始计时。(当第一次触发事件时马上执行事件处理函数,最后一次触发事件后也还会执行一次事件处理函数)。
原理是通过时间戳,判断是否到达一定时间来触发函数。
2. V8 垃圾回收
v8 具有内存限制(为了减少对应用性能的影响),在64位系统下位1.4G。
2.1. v8 的垃圾回收策略
- v8 的内存结构主要分为新生代和老生代,大多数对象开始都会被分配在新生代,存活一段时间后会被转移到老生代。
- 新生代内存较小但垃圾回收频繁,采用以空间换时间的 Scavenge 算法。
- 老生代使用标记清除与标记整理算法进行垃圾回收。
3. 如何避免内存泄漏
- 尽量避免使用全局变量
- 手动清除定时器
- 手动清除事件监听器
- 手动清除 DOM 引用
- 少用闭包
- 使用弱引用(ES6的WeakMap和WeakSet)
WeakMap示例:
// 一个对象被多次引用时,例如作为另一对象的键、值或子元素时,将该对象引用设置为 null 时,该对象是不会被回收的,依然存在
let a = {};
let arr = [a];
a = null;
console.log(arr); // [{}] {}不会被回收
let a = {};
let map = new WeakMap();
map.set(a, '111')
map.get(a)
a = null;
4. 事件循环(Event Loop)
4.1. 浏览器环境
- js 引擎遇到异步事件时,将异步事件挂起,继续执行执行栈中的其他任务。
- 异步事件返回结果后,将异步事件加入到对应的事件队列中。
- 执行栈中的所有任务都执行完毕,主线程空闲时,将微任务事件队列中的事件取出,并将对应的回调依次放入执行栈中执行。
- 执行完毕微任务队列中的所有事件后,再依次取出宏任务队列中的事件放入执行栈中执行。
宏任务: DOM 渲染后触发,如 setTimeout,setInterval,Ajax,DOM 事件。
微任务: DOM 渲染前触发,如 Promise.then,queueMicrotask、MutationObserver(监听 DOM 节点变化)
4.2. node 环境
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
- 首先进入 poll 阶段,执行 poll queue 中的事件,直到所有回调执行完毕或超过 node 执行数限制。
- 检查执行 setImmediate() 的 callback,同时将到期的 timer 加入到 timer queue 中,并执行回调。(这两者的顺序是不固定的,受代码运行环境的影响)
- 当 I/O 事件返回,进入 I/O callback 阶段并立即执行这个事件的 callback。
setImmediate() 和 setTimeout() 的执行顺序是不固定的,但在一个 I/O 事件的回调中,setImmediate() 永远在 setTimeout() 前执行。
5. EventTarget.addEventListener()
将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时执行回调。事件目标可以是Element、Document 和 Window 或者其他任何支持事件的对象(XMLhttpRequest)。
// 接口协议
target.addEventListener(type, listener, options);
target.addEventListener(type, listener, useCapture);
type type = string; // 事件的类型(不带 on)
type listener = Function; // 监听到事件后的处理函数,接受 Event 对象作为参数
/**
* 默认值为 false
* 监听器在监听时有三个阶段,
* 捕获阶段(根节点到子节点) -> 目标阶段(目标本身) -> 冒泡阶段(目标本身到根节点),
* useCapture 设置为 true 时监听器将在捕获阶段处理事件,设置为 false,则在目标或冒泡阶段处理事件。
*/
type useCapture = boolean;
type options = {
capture: boolean; // 默认值为 false,规定事件在捕获阶段处理还是冒泡阶段处理,同上。
once: boolean; // 默认值为 false,表示监听器只会调用一次,之后自动移除
passive: boolean; // 默认值为 false,设置为true时,监听器永远不会调用 preventDefault()。
};
// 使用 passive 改善滚屏性能,在添加 touchmove 事件监听器时,设置 passive 为 true,防止监听器阻塞页面滚动
6. typeof
- 除 function 外的所有引用类型使用 typeof 得到的结果均为 ‘object’。
- null 使用 typeof 得到的结果为 ‘object’。
基本数据类型:number, string, boolean, undefined, symbol( es6 新增,typeof Symbol() === ‘symbol’ )。
引用数据类型:object, function。
7. instanceof
语法: object instanceof constructor
用来检测 constructor.prototype 是否在参数 object 的原型链上。
Object instanceof Function === true
Function instanceof Object === true
Object instanceof Object === true
Function instanceof Function === true
8. Object.prototype.toString.call()
由于引用类型通过 typeof 只能得到 ‘object’ 和 ‘function’ 两种结果,无法区分 object、array、date、math 等类型,因此当不确定变量类型时,可用 Object.prototype.toString.call() 方法来识别。
Object.prototype.toString() 方法会返回一个形如 ‘[object XXX]’ 的字符串。
Object.prototype.toString.call({}) === '[object Object]'
Object.prototype.toString.call([]) === '[object Array]'
Object.prototype.toString.call(null) === '[object Null]'
Object.prototype.toString.call(new Date()) === '[object Date]'
9. 原型链
每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
原型链最终指向 null(Object.prototype.proto === null),null 没有原型对象。
9.1. 实现继承的方式
寄生组合式继承
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype) // 或者 Child.prototype = new Parent();
Child.prototype.constructor = Child;
// Object.create(proto: 原型对象) 创建一个以传入的 proto 对象为原型的新对象
10. Promise
10.1. Promise 状态
Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)、拒绝态(Rejected)。一旦 Promise 被 resolve 或 reject,不能再迁移至其他任何状态。
10.2. Promise 基本执行过程
- 初始化 Promise 状态(Pending)
- 立即执行 Promise 中传入的 fn 函数(参数为 resolve, reject)
- 执行 then(…)注册回调
注意:then 方法传入的参数 onFulfilled,必须在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
promise 内的函数依旧是宏任务,只有 .then() 方法内注册的回调函数才是微任务。
resolve 或 reject 的值即为 .then() 和 .catch() 中的参数。
11. 作用域和执行上下文
11.1. 作用域
js 是词法作用域,变量的访问范围仅由 声明 时候的区域决定。
var count = 10;
function a() {
return count + 10;
}
function b() {
var count = 20;
return a();
}
console.log(b());
// 结果是20
// 因为js是词法作用域,a方法中的count向外查找,外层是全局,count为10,因此+10之后结果是20。
var foo = 3;
function func() {
var foo = foo || 5;
console.log(foo);
}
func();
// 结果是5
// var foo = foo || 5; 这一行可以拆分为两步:var foo; foo = foo || 5;
// 此时 foo 为 undefined,因此赋值后 foo 为 5,打印 5。
// 如果这一行去掉 var,即 foo = foo || 5,那么 foo 会向外查找,即打印 3。
11.2. 执行上下文
- 每个上下文都有一个关联的变量对象 VO(variable object),而这个上下文中定义的所有变量和函数都在这个对象上。
- 上下文中的代码在执行的时候,会创建变量对象的一个作用域链
- 如果上下文是函数,则其活动对象 AO 用作变量对象,活动对象最初只有一个定义变量:arguments。
12. 闭包
12.1. 概念
闭包是指那些引用了另一个函数作用域中变量的函数。
通常在嵌套函数中实现,原因是内部函数的作用域链包含外部函数的作用域链(即能够访问外部函数的活动对象)。
注意:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。
// 函数作为参数传递
function print(fn) {
const a = 200;
fn();
}
const a = 100;
function fn() {
console.log(a);
}
print(fn); // 100
// 函数作为返回值返回
function create() {
const a = 100;
return function () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 100
12.2. 使用场景
- 实现私有属性,getter 和 setter 方法。
- 迭代器(每次执行函数返回下一个值)。
- 缓存(复杂的计算操作,将对应入参的计算结果缓存下来,再次执行函数时,如果缓存有,就直接从缓存中取,如果没有,再进行计算)。
13. 函数
13.1. arguments对象
- 只有用 function 定义函数时才会存在,包含调用函数时传入的所有参数。
- arguments 对象是一个类数组,具有 Interator 接口。
13.2. this 指向
- 在标准函数中,this 指向函数 执行时 的上下文对象。
- 在箭头函数中,this 指向函数 定义时 的上下文对象。
14. new 操作符实现过程
- 创建一个对象(new Object())
- 将构造函数的 prototype 赋值给已创建对象的 proto 属性
- 执行构造函数,使用 apply() 方法将 this 指向已创建的对象,并传入构造函数的 arguments 对象
- 获取执行构造函数后的返回值,若返回值类型为’object’,则 new 操作符返回构造函数返回值,否则返回已创建的对象
15. 柯里化(Currying) 和 Compose 函数
15.1 Currying
把接收多个参数的函数变换成接受一个参数的函数,并返回新的函数来接收剩下的参数,新的函数返回结果。
add(1, 2, 3); // 普通函数
currying(1)(2)(3); // Currying 后
使用场景: 参数复用,提前确认,延迟运行(多次调用,第一步相同,第二步不同)
bind 方法的实现原理就是 Currying。
// 支持多参数传递
function progressCurrying(fn, args) {
const _this = this
const len = fn.length;
const args = args || [];
return function() {
const _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);
// 如果收集的参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args);
}
// 参数收集完毕,则执行fn
return fn.apply(this, _args);
}
}
15.2 compose 函数
compose 函数就是将层级嵌套函数扁平化。
fn3(fn2(fn1(1,2,3))) // 普通函数
compose(fn1, fn2, fn3)(1,2,3) // compose后
实现方式
const compose = function(...args) {
let init = args.pop()
return function(...arg) {
return args.reverse().reduce(function(sequence, func) {
return sequence.then(function(result) {
return func.call(null, result)
})
}, Promise.resolve(init.apply(null, arg)))
}
}
16. ES6 注意点
- 任何定义了迭代器(Iterator)接口的对象(比如 arguments),都可以使用 for…of 循环。
- WeakSet 和 WeakMap 的元素和键只能为对象,它们都是弱引用,垃圾回收机制不会将该引用考虑在内。
- Symbol 类型表示一个独一无二的值(let s = Symbol()),相同参数的 Symbol 函数返回值依然不相等。
- Array 方法:
- Array.flat(n) 扁平化
- Array.reduce((prev, cur) => any) 循环
- Array.fill(value, start = 0, end = array.length) 填充数据,会修改原数组