常见进阶面试题
模块化
JS运行的环境 | 全局对象 | |
---|---|---|
浏览器 | window | 直接支持ES6Module,但是不支持CommonJS |
Node | global | 支持CommonJS,但是不支持ES6Module |
Webpack | Common JS&ES6Module都支持,支持相互之间的“混用 | |
Vite | 基于ES6Module规范,实现模块之间的相互引用 |
闭包
闭包:作用域的一种特殊应用,内层函数作用域用到了外层函数作用域的变量。由于被内层函数引用,导致外层函数执行完变量不会被释放,就形成了闭包。
闭包案例
function func(){
const val = 10
return function(){
console.log(val)
}
}
const val = 100
const fn = func()
fn()//10
function fn(){
const data = {}
return {
set: function(key,val){
data[key] = val
}
get:function(key){
return data[key]
}
}
}
const handle = fn()
handle.set('name','zhangsan')
handle.get('name')
const btns = document.querySelectorAll('.btn') //5个按钮
for(val i = 0;i < btns.length; i++){ //改用 let 形成闭包
btns[i].onclick = function(){
console.log(i) //输出结果都是 5
}
}
//==>
for(val i = 0;i < btns.length; i++){
(function(){
btns[i].onclick = function(){
console.log(i) //输出结果: 01234
}})(i)
}
应用
- 函数柯里化
- 变量私有化
弊端
过度使用闭包会造成内存占用过多,容易造成内存泄漏,手动清空可解决问题。
函数柯里化
数组求和
let arr = [1,2,3,4]
//方法一:
let sum = arr.reduce((pre,cur) => pre + cur,0)
//方法二:
eval(arr.join('+'))
实现 add(1,2)(3)
const add = function(...params){ // params:[1,2]
return function(...args){ //args:[3]
return params.concat(args).reduce((pre,cur) => cur+pre)
}
}
//简化
const add = (...params) => (...args) => params.concat(args).reduce((pre,cur) => pre + cur)
实现 add(1)(2)(3)
const add = function(a){
return function(b){
return function(c){
return a + b + c
}
}
}
//简化
const add = a => b => c => a + b + c
//柯里化:curring 函数
const curring = function(){
let params = []//利用闭包存储实参
const add = (...args) =>{
params = params.concat(args)
return add
}
//在对象转数字过程中,进行求和处理
add[Symbol.toPrimitive] = () => {
return params.reduce((pre,cur) => pre + cur)
}
return add
}
const add = curring()
let sum = add(1)(2)(3)(4)(5)(6)
alert(sum) //21
console.log(+sum)//21
compose
思想:利用闭包预先存储,供其夏季上下文后期使用,可解决函数嵌套导致的可读性差问题。
const add1 = x => x + 1
const mul3 = x => x * 3
const div2 = x => x / 2
//不使用柯里化:div2(mul3(add1(10)))
const compose = function(...funcs){
//考虑不传函数,返回operate传的实参
let len = funcs.length
if(len === 0) return x => x
if(len === 1) return funcs[0]
return function operate(x){
return funcs.reduceRight((x,func) => func(x),x)
}
}
//不传函数
const operate = compose()
operate(10) //10
//传一个
const operate = compose(add1)
operate(10) //11
// 传多个
const operate = compose(div2,mul3,add1,add1)
operate(10) //18
闭包惰性思想
function getCss(ele, attr){
//加载判断样式兼容问题,没有更换刷新浏览器每次都需要做多余的判断
if(window.getComputedStyle){
return window.getComputedStyle(ele)[attr]
}else{
return window.getCurrentStyle(ele)[attr]
}
}
//优化后
function getCss(ele, attr){
//第一次运行,根据兼容情况,重构 getCss
if(window.getComputedStyle){
getCss = function(ele, attr){
return window.getComputedStyle(ele)[attr]
}
}else{
getCss = function(ele, attr){
return window.getCurrentStyle(ele)[attr]
}
}
return getCss(ele, attr)
}
getCss(body, 'width')
getCss(body, 'padding')
深度优先和广度优先
//数据
let tree = {
id: '1',
title: '节点1',
children: [
{
id: '1-1',
title: '节点1-1'
},
{
id: '1-2',
title: '节点1-2',
children: [{
id: '2',
title: '节点2',
children: [
{
id: '2-1',
title: '节点2-1'
}
]
}]
}
]
}
深度优先遍历(DFS)
深度优先遍历(DFS)是一种递归或栈的思想来遍历所有节点的算法。在 JavaScript 中,可以使用递归或栈来实现 DFS。具体步骤如下:
- 访问根节点
- 对根节点的所有子节点,依次执行以下操作:
- 访问该子节点
- 对该子节点的所有子节点,依次执行以上两步操作
JavaScript 中 DFS 的时间复杂度为 O(n),其中 n 是节点数。空间复杂度为 O(h),其中 h 是树的高度。
//递归方式
let dfs = (node, nodes = []) => {
if (node) {
nodes.push(node.id)
node.children?.forEach(child => dfs(child, nodes) )
}
return nodes
}
//非递归方式
let dfs = (node) => {
let nodes = []
let stack = []
if(node){
stack.push(node)
while(stack.length){
let item = stack.shift()
let children = item.children
nodes.push(item.id)
children?.forEach(child => stack.push(child) )
}
}
return nodes
}
// 用非递归方式实现二叉树深度优先遍历
function dfs(node) {
let nodes = [];
if (node) {
nodes.push(node);
node.children?.forEach(child => nodes = nodes.concat(dfs(child)))
}
return nodes;
}
广度优先遍历(BFS)
广度优先遍历(BFS)是一种逐层扫描节点的算法。在 JavaScript 中,可以使用队列来实现 BFS。具体步骤如下:
- 将根节点入队
- 当队列非空时,执行以下操作:
- 将队头节点出队,并访问该节点
- 将该节点的所有子节点入队
JavaScript 中 BFS 的时间复杂度为 O(n),其中 n 是节点数。空间复杂度为 O(w),其中 w 是树的宽度。
function bfs(root) {
let nodes = []
const queue = [root];
while (queue.length > 0) {
const node = queue.shift();
nodes.push(node.id)
node.children?.forEach(child => {
queue.push(child);
});
}
return nodes
}
//简化后
function bfs(root){
let nodes = []
let node,queue = [root]
while(node = queue.shift()){
nodes.push(node.id)
node.children && queue.push(...node.children)
}
return nodes
}
function bfs(node) {
let nodes = []
if (node !== null) {
nodes.push(node)
let i = 0
while (node = nodes[i++]) {
node.children && nodes.push(...node.children)
}
}
return nodes
}
两者的区别
- 广度优先遍历需要使用队列逐层扫描节点,而深度优先遍历需要使用递归或栈来遍历节点。
- 广度优先遍历的空间复杂度比深度优先遍历高,因为广度优先遍历需要使用队列来存储节点,而深度优先遍历只需要使用递归或栈。
广度优先遍历的应用
广度优先遍历可以用于许多问题,例如:
- 查找最短路径:遍历距离起点最近的节点。
- 查找连通块:可以找到由相邻节点组成的连通块。
- 查找图的最小生成树
深度优先遍历的应用
- 查找连通块:可以找到由相邻节点组成的连通块。
- 查找拓扑排序:可以找到图的拓扑排序,即节点的一个线性序列,使得对于每个有向边 (u,v),都有 u 在序列中排在 v 的前面。
- 查找强联通分量:通过深度优先遍历,可以找到图的强联通分量,即任意两点之间都存在一条路径的最大子图。
loader和plugin区别
Loader
是用来处理单个模块的转换,将各种类型的文件(如 js、css、sass、less、图片等)转换为 Webpack 可以处理的模块;
执行顺序是从后往前依次执行,每个 Loader 都会对文件进行处理,直到最后一个 Loader 将文件转换为模块。
Plugin
是扩展 Webpack 功能的函数,可以在构建过程中执行一系列的任务,遍历所有的 Plugin,并执行它们的 apply 方法。
例如生成 HTML 文件(HtmlWebpackPlugin)、压缩代码(UglifyJsPlugin)、提取公共代码等。
Plugin 在 Loader 执行完成之后执行,可以访问 Webpack 的内部环境,并对其进行修改和优化。
前端模块化规范
前端模块化规范是为了解决前端代码复杂度、可维护性和可扩展性等问题而产生的。目前常用的前端模块化规范有CommonJS、AMD、CMD和ES6 Module等,它们之间的区别如下:
CommonJS规范:主要用于服务器端的JavaScript编程,Node.js采用了该规范。采用同步加载模块的方式,即在需要使用某个模块时,通过require函数同步加载该模块,然后才能执行后续代码。
ES6 Module:是ECMAScript 6标准中新增的模块化规范。 采用静态加载模块的方式,即在编译时就确定模块之间的依赖关系,然后再执行代码。
与其他模块化规范不同的是,ES6 Module不需要使用特定的函数或语法来定义和导入模块,而是使用import和export关键字来实现。