阿里前端常见面试题总结

2022-09-17 13:25:39 浏览数 (2)

LRU 算法

实现代码如下:

代码语言:javascript复制
//  一个Map对象在迭代时会根据对象中元素的插入顺序来进行
// 新添加的元素会被插入到map的末尾,整个栈倒序查看
class LRUCache {
  constructor(capacity) {
    this.secretKey = new Map();
    this.capacity = capacity;
  }
  get(key) {
    if (this.secretKey.has(key)) {
      let tempValue = this.secretKey.get(key);
      this.secretKey.delete(key);
      this.secretKey.set(key, tempValue);
      return tempValue;
    } else return -1;
  }
  put(key, value) {
    // key存在,仅修改值
    if (this.secretKey.has(key)) {
      this.secretKey.delete(key);
      this.secretKey.set(key, value);
    }
    // key不存在,cache未满
    else if (this.secretKey.size < this.capacity) {
      this.secretKey.set(key, value);
    }
    // 添加新key,删除旧key
    else {
      this.secretKey.set(key, value);
      // 删除map的第一个元素,即为最长未使用的
      this.secretKey.delete(this.secretKey.keys().next().value);
    }
  }
}
// let cache = new LRUCache(2);
// cache.put(1, 1);
// cache.put(2, 2);
// console.log("cache.get(1)", cache.get(1))// 返回  1
// cache.put(3, 3);// 该操作会使得密钥 2 作废
// console.log("cache.get(2)", cache.get(2))// 返回 -1 (未找到)
// cache.put(4, 4);// 该操作会使得密钥 1 作废
// console.log("cache.get(1)", cache.get(1))// 返回 -1 (未找到)
// console.log("cache.get(3)", cache.get(3))// 返回  3
// console.log("cache.get(4)", cache.get(4))// 返回  4

前端进阶面试题详细解答

setTimeout(fn, 0)多久才执行,Event Loop

setTimeout 按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定

vue-router

代码语言:text复制
vue-router是vuex.js官方的路由管理器,它和vue.js的核心深度集成,让构建但页面应用变得易如反掌

<router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址

<router-view> 组件是一个 functional 组件,渲染路径匹配到的视图组件。

<keep-alive> 组件是一个用来缓存组件

router.beforeEach

router.afterEach

to: Route: 即将要进入的目标 路由对象

from: Route: 当前导航正要离开的路由

next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

介绍了路由守卫及用法,在项目中路由守卫起到的作用等等

事件机制

涉及面试题:事件的触发过程是怎么样的?知道什么是事件代理嘛?

1. 简介

事件流是一个事件沿着特定数据结构传播的过程。冒泡和捕获是事件流在DOM中两种不同的传播方法

事件流有三个阶段

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

事件捕获

事件捕获(event capturing):通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件

事件冒泡

事件冒泡(dubbed bubbling):与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点

无论是事件捕获还是事件冒泡,它们都有一个共同的行为,就是事件传播

imgimg

2. 捕获和冒泡

代码语言:html复制
<div id="div1">
  <div id="div2"></div>
</div>

<script>
    let div1 = document.getElementById('div1');
    let div2 = document.getElementById('div2');

    div1.onClick = function(){
        alert('1')
    }

    div2.onClick = function(){
        alert('2');
    }

</script>

当点击 div2时,会弹出两个弹出框。在 ie8/9/10chrome浏览器,会先弹出”2”再弹出“1”,这就是事件冒泡:事件从最底层的节点向上冒泡传播。事件捕获则跟事件冒泡相反 W3C的标准是先捕获再冒泡, addEventListener的第三个参数决定把事件注册在捕获(true)还是冒泡(false)

3. 事件对象

imgimg

4. 事件流阻止

在一些情况下需要阻止事件流的传播,阻止默认动作的发生

  • event.preventDefault():取消事件对象的默认动作以及继续传播。
  • event.stopPropagation()/ event.cancelBubble = true:阻止事件冒泡。

事件的阻止在不同浏览器有不同处理

  • IE下使用 event.returnValue= false
  • 在非IE下则使用 event.preventDefault()进行阻止

preventDefault与stopPropagation的区别

  • preventDefault告诉浏览器不用执行与事件相关联的默认动作(如表单提交)
  • stopPropagation是停止事件继续冒泡,但是对IE9以下的浏览器无效

5. 事件注册

  • 通常我们使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 falseuseCapture 决定了注册的事件是捕获事件还是冒泡事件
  • 一般来说,我们只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件
代码语言:javascript复制
node.addEventListener('click',(event) =>{
    event.stopImmediatePropagation()
    console.log('冒泡')
},false);
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener('click',(event) => {
    console.log('捕获 ')
},true)

6. 事件委托

  • js中性能优化的其中一个主要思想是减少dom操作。
  • 节省内存
  • 不需要给子节点注销事件

假设有100li,每个li有相同的点击事件。如果为每个Li都添加事件,则会造成dom访问次数过多,引起浏览器重绘与重排的次数过多,性能则会降低。 使用事件委托则可以解决这样的问题

原理

实现事件委托是利用了事件的冒泡原理实现的。当我们为最外层的节点添加点击事件,那么里面的ullia的点击事件都会冒泡到最外层节点上,委托它代为执行事件

代码语言:html复制
<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>
<script>
  window.onload = function(){
    var ulEle = document.getElementById('ul');
    ul.onclick = function(ev){
        //兼容IE
        ev = ev || window.event;
        var target = ev.target || ev.srcElement;

        if(target.nodeName.toLowerCase() == 'li'){
            alert( target.innerHTML);
        }

    }
  }
</script>

对节流与防抖的理解

  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。

防抖函数的应用场景:

  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce

节流函数的适⽤场景:

  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

用过 TypeScript 吗?它的作用是什么?

为 JS 添加类型支持,以及提供最新版的 ES 语法的支持,是的利于团队协作和排错,开发大型项目

图片懒加载

实现getBoundClientRect 的实现方式,监听 scroll 事件(建议给监听事件添加节流),图片加载完会从 img 标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件。

代码语言:javascript复制
// scr 加载默认图片,data-src 保存实施懒加载后的图片
// <img src="./default.jpg" data-src="https://xxx.jpg" alt="" />
let imgs = [...document.querySelectorAll("img")];
const len = imgs.length;

let lazyLoad = function() {
    let count = 0;
    let deleteImgs = [];
    // 获取当前可视区的高度
    let viewHeight = document.documentElement.clientHeight;
    // 获取当前滚动条的位置(距离顶部的距离,等价于document.documentElement.scrollTop)
    let scrollTop = window.pageYOffset;
    imgs.forEach((img) => {
        // 获取元素的大小,及其相对于视口的位置,如 bottom 为元素底部到网页顶部的距离
        let bound = img.getBoundingClientRect();
        // 当前图片距离网页顶部的距离
        // let imgOffsetTop = img.offsetTop;

        // 判断图片是否在可视区内,如果在就加载(两种判断方式)
        // if(imgOffsetTop < scrollTop   viewHeight) 
        if (bound.top < viewHeight) {
            img.src = img.dataset.src;  // 替换待加载的图片 src
            count  ;
            deleteImgs.push(img);
            // 最后所有的图片加载完毕后需要解绑监听事件
            if(count === len) {
                document.removeEventListener("scroll", imgThrottle);
            }
        }
    });
    // 图片加载完会从 `img` 标签组成的 DOM 列表中删除
    imgs = imgs.filter((img) => !deleteImgs.includes(img));
}

window.onload = function () {
    lazyLoad();
};
// 使用 防抖/节流 优化一下滚动事件
let imgThrottle = debounce(lazyLoad, 1000);
// 监听 `scroll` 事件
window.addEventListener("scroll", imgThrottle);

点击刷新按钮或者按 F5、按 Ctrl F5 (强制刷新)、地址栏回车有什么区别?

  • 点击刷新按钮或者按 F5: 浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。
  • 用户按 Ctrl F5(强制刷新): 浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。
  • 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。

实现有并行限制的 Promise 调度器

题目描述:JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个

代码语言:javascript复制
 addTask(1000,"1");
 addTask(500,"2");
 addTask(300,"3");
 addTask(400,"4");
 的输出顺序是:2 3 1 4

 整个的完整执行流程:

一开始1、2两个任务开始执行
500ms时,2任务执行完毕,输出2,任务3开始执行
800ms时,3任务执行完毕,输出3,任务4开始执行
1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
1200ms时,4任务执行完毕,输出4

实现代码如下:

代码语言:javascript复制
class Scheduler {
  constructor(limit) {
    this.queue = [];
    this.maxCount = limit;
    this.runCounts = 0;
  }
  add(time, order) {
    const promiseCreator = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log(order);
          resolve();
        }, time);
      });
    };
    this.queue.push(promiseCreator);
  }
  taskStart() {
    for (let i = 0; i < this.maxCount; i  ) {
      this.request();
    }
  }
  request() {
    if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {
      return;
    }
    this.runCounts  ;
    this.queue
      .shift()()
      .then(() => {
        this.runCounts--;
        this.request();
      });
  }
}
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
  scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();

说一下SPA单页面有什么优缺点?

代码语言:javascript复制
优点:

1.体验好,不刷新,减少 请求  数据ajax异步获取 页面流程;

2.前后端分离

3.减轻服务端压力

4.共用一套后端程序代码,适配多端

缺点:

1.首屏加载过慢;

2.SEO 不利于搜索引擎抓取

setInterval 模拟 setTimeout

描述:使用setInterval模拟实现setTimeout的功能。

思路setTimeout的特性是在指定的时间内只执行一次,我们只要在setInterval内部执行 callback 之后,把定时器关掉即可。

实现

代码语言:javascript复制
const mySetTimeout = (fn, time) => {
    let timer = null;
    timer = setInterval(() => {
        // 关闭定时器,保证只执行一次fn,也就达到了setTimeout的效果了
        clearInterval(timer);
        fn();
    }, time);
    // 返回用于关闭定时器的方法
    return () => clearInterval(timer);
}

// 测试
const cancel = mySetTimeout(() => {
    console.log(1);
}, 1000);  
// 一秒后打印 1

说一下data为什么是一个函数而不是一个对象?

JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。而在Vue中,我们更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当我们每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。

代码输出结果

代码语言:javascript复制
const first = () => (new Promise((resolve, reject) => {
    console.log(3);
    let p = new Promise((resolve, reject) => {
        console.log(7);
        setTimeout(() => {
            console.log(5);
            resolve(6);
            console.log(p)
        }, 0)
        resolve(1);
    });
    resolve(2);
    p.then((arg) => {
        console.log(arg);
    });
}));
first().then((arg) => {
    console.log(arg);
});
console.log(4);

输出结果如下:

代码语言:javascript复制
3
7
4
1
2
5
Promise{<resolved>: 1}

代码的执行过程如下:

  1. 首先会进入Promise,打印出3,之后进入下面的Promise,打印出7;
  2. 遇到了定时器,将其加入宏任务队列;
  3. 执行Promise p中的resolve,状态变为resolved,返回值为1;
  4. 执行Promise first中的resolve,状态变为resolved,返回值为2;
  5. 遇到p.then,将其加入微任务队列,遇到first().then,将其加入任务队列;
  6. 执行外面的代码,打印出4;
  7. 这样第一轮宏任务就执行完了,开始执行微任务队列中的任务,先后打印出1和2;
  8. 这样微任务就执行完了,开始执行下一轮宏任务,宏任务队列中有一个定时器,执行它,打印出5,由于执行已经变为resolved状态,所以resolve(6)不会再执行;
  9. 最后console.log(p)打印出Promise{<resolved>: 1}

怎么解决白屏问题

代码语言:javascript复制
1、加loading
2、骨架屏

HTTP世界全览

  • 互联网上绝大部分资源都使用 HTTP 协议传输;
  • 浏览器是 HTTP 协议里的请求方,即 User Agent
  • 服务器是 HTTP 协议里的应答方,常用的有 ApacheNginx
  • CDN 位于浏览器和服务器之间,主要起到缓存加速的作用;
  • 爬虫是另一类 User Agent,是自动访问网络资源的程序。
  • TCP/IP 是网络世界最常用的协议,HTTP 通常运行在 TCP/IP 提供的可靠传输基础上
  • DNS 域名是 IP 地址的等价替代,需要用域名解析实现到 IP 地址的映射;
  • URI 是用来标记互联网上资源的一个名字,由“协议名 主机名 路径”构成,俗称 URL;
  • HTTPS 相当于“HTTP SSL/TLS TCP/IP”,为 HTTP 套了一个安全的外壳;
  • 代理是 HTTP 传输过程中的“中转站”,可以实现缓存加速、负载均衡等功能

vue-router

mode

  • hash
  • history

跳转

  • this.$router.push()
  • <router-link to=""></router-link>

占位

代码语言:javascript复制
<router-view></router-view>

vue-router源码实现

  • 作为一个插件存在:实现VueRouter类和install方法
  • 实现两个全局组件:router-view用于显示匹配组件内容,router-link用于跳转
  • 监控url变化:监听hashchangepopstate事件
  • 响应最新url:创建一个响应式的属性current,当它改变时获取对应组件并显示
代码语言:javascript复制
// 我们的插件:
// 1.实现一个Router类并挂载期实例
// 2.实现两个全局组件router-link和router-view
let Vue;

class VueRouter {
  // 核心任务:
  // 1.监听url变化
  constructor(options) {
    this.$options = options;

    // 缓存path和route映射关系
    // 这样找组件更快
    this.routeMap = {}
    this.$options.routes.forEach(route => {
      this.routeMap[route.path] = route
    })

    // 数据响应式
    // 定义一个响应式的current,则如果他变了,那么使用它的组件会rerender
    Vue.util.defineReactive(this, 'current', '')

    // 请确保onHashChange中this指向当前实例
    window.addEventListener('hashchange', this.onHashChange.bind(this))
    window.addEventListener('load', this.onHashChange.bind(this))
  }

  onHashChange() {
    // console.log(window.location.hash);
    this.current = window.location.hash.slice(1) || '/'
  }
}

// 插件需要实现install方法
// 接收一个参数,Vue构造函数,主要用于数据响应式
VueRouter.install = function (_Vue) {
  // 保存Vue构造函数在VueRouter中使用
  Vue = _Vue

  // 任务1:使用混入来做router挂载这件事情
  Vue.mixin({
    beforeCreate() {
      // 只有根实例才有router选项
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }

    }
  })

  // 任务2:实现两个全局组件
  // router-link: 生成一个a标签,在url后面添  // <router-link to="/about">aaa</router-link>
  Vue.component('router-link', {
    props: {
      to: {
        type: String,
        required: true
      },
    },
    render(h) {
      // h(tag, props, children)
      return h('a',
        { attrs: { href: '#'   this.to } },
        this.$slots.default
      )
      // 使用jsx
      // return <a href={'#' this.to}>{this.$slots.default}</a>
    }
  })
  Vue.component('router-view', {
    render(h) {
      // 根据current获取组件并render
      // current怎么获取?
      // console.log('render',this.$router.current);
      // 获取要渲染的组件
      let component = null
      const { routeMap, current } = this.$router
      if (routeMap[current]) {
        component = routeMap[current].component
      }
      return h(component)
    }
  })
}

export default VueRouter

watch 的理解

watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听

注意:Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

代码输出结果

代码语言:javascript复制
function runAsync (x) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
}

Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))

输出结果如下:

代码语言:javascript复制
1
2
3
[1, 2, 3]

首先,定义了一个Promise,来异步执行函数runAsync,该函数传入一个值x,然后间隔一秒后打印出这个x。

之后再使用Promise.all来执行这个函数,执行的时候,看到一秒之后输出了1,2,3,同时输出了数组1, 2, 3,三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。

Service Worker

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API

浏览器对 ServiceWorker 做了很多限制

  • ServiceWorker 中无法直接访问 DOM,但可以通过 postMessage 接口发送的消息来与其控制的页面进行通信
  • ServiceWorker 只能在本地环境下或 HTTPS 网站中使用
  • ServiceWorker 有作用域的限制,一个 ServiceWorker 脚本只能作用于当前路径及其子路径;

目前该技术通常用来做缓存文件,提高首屏速度

代码语言:javascript复制
// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register("sw.js")
    .then(function(registration) {
      console.log("service worker 注册成功");
    })
    .catch(function(err) {
      console.log("servcie worker 注册失败");
    });
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener("install", e => {
  e.waitUntil(
    caches.open("my-cache").then(function(cache) {
      return cache.addAll(["./index.html", "./index.js"]);
    })
  );
});

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener("fetch", e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response;
      }
      console.log("fetch source");
    })
  );
});

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了

在 Cache 中也可以发现我们所需的文件已被缓存

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

说一下前端登录的流程?

初次登录的时候,前端调后调的登录接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token,和一个用户信息的值,前端拿到token,将token储存到Vuex中,然后从Vuex中把token的值存入浏览器Cookies中。把用户信息存到Vuex然后再存储到LocalStroage中,然后跳转到下一个页面,根据后端接口的要求,只要不登录就不能访问的页面需要在前端每次跳转页面师判断Cookies中是否有token,没有就跳转到登录页,有就跳转到相应的页面,我们应该再每次发送post/get请求的时候应该加入token,常用方法再项目utils/service.js中添加全局拦截器,将token的值放入请求头中 后端判断请求头中有无token,有token,就拿到token并验证token是否过期,在这里过期会返回无效的token然后有个跳回登录页面重新登录并且清除本地用户的信息

0 人点赞