写点什么

校招前端必会面试题及答案

作者:loveX001
  • 2023-02-06
    浙江
  • 本文字数:9309 字

    阅读完需:约 31 分钟

代码输出结果

async function async1() {  console.log("async1 start");  await async2();  console.log("async1 end");}async function async2() {  console.log("async2");}async1();console.log('start')
复制代码


输出结果如下:


async1 startasync2startasync1 end
复制代码


代码的执行过程如下:


  1. 首先执行函数中的同步代码async1 start,之后遇到了await,它会阻塞async1后面代码的执行,因此会先去执行async2中的同步代码async2,然后跳出async1

  2. 跳出async1函数后,执行同步代码start

  3. 在一轮宏任务全部执行完之后,再来执行await后面的内容async1 end


这里可以理解为 await 后面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中。

如何防御 XSS 攻击?

可以看到 XSS 危害如此之大, 那么在开发网站时就要做好防御措施,具体措施如下:


  • 可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到 HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。

  • 使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。


  1. CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。

  2. 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式


  • 对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。

对 async/await 的理解

async/await 其实是Generator 的语法糖,它能实现的效果都能用 then 链来实现,它是为优化 then 链而开发出来的。从字面上来看,async 是“异步”的简写,await 则为等待,所以很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定 await 只能出现在 asnyc 函数中,先来看看 async 函数返回了什么:


async function testAsy(){   return 'hello world';}let result = testAsy(); console.log(result)
复制代码



所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda 表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。


async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:


async function testAsy(){   return 'hello world'}let result = testAsy() console.log(result)result.then(v=>{    console.log(v)   // hello world})
复制代码


那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)


联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。


注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

代码输出结果

const promise = Promise.resolve().then(() => {  return promise;})promise.catch(console.err)
复制代码


输出结果如下:


Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
复制代码


这里其实是一个坑,.then.catch 返回的值不能是 promise 本身,否则会造成死循环。

说一下数组如何去重,你有几种方法?

let arr = [1,1,"1","1",true,true,"true",{},{},"{}",null,null,undefined,undefined]
// 方法1let uniqueOne = Array.from(new Set(arr)) console.log(uniqueOne)
// 方法2let uniqueTwo = arr => { let map = new Map(); //或者用空对象 let obj = {} 利用对象属性不能重复得特性 let brr = [] arr.forEach( item => { if(!map.has(item)) { //如果是对象得话就判断 !obj[item] map.set(item,true) //如果是对象得话就obj[item] =true 其他一样 brr.push(item) } }) return brr}console.log(uniqueTwo(arr))
//方法3let uniqueThree = arr => { let brr = [] arr.forEach(item => { // 使用indexOf 返回数组是否包含某个值 没有就返回-1 有就返回下标 if(brr.indexOf(item) === -1) brr.push(item) // 或者使用includes 返回数组是否包含某个值 没有就返回false 有就返回true if(!brr.includes(item)) brr.push(item) }) return brr}console.log(uniqueThree(arr))
//方法4let uniqueFour = arr => { // 使用 filter 返回符合条件的集合 let brr = arr.filter((item,index) => { return arr.indexOf(item) === index }) return brr}console.log(uniqueFour(arr))
复制代码

代码输出问题

window.number = 2;var obj = { number: 3, db1: (function(){   console.log(this);   this.number *= 4;   return function(){     console.log(this);     this.number *= 5;   } })()}var db1 = obj.db1;db1();obj.db1();console.log(obj.number);     // 15console.log(window.number);  // 40
复制代码


这道题目看清起来有点乱,但是实际上是考察 this 指向的:


  1. 执行 db1()时,this 指向全局作用域,所以 window.number * 4 = 8,然后执行匿名函数, 所以 window.number * 5 = 40;

  2. 执行 obj.db1();时,this 指向 obj 对象,执行匿名函数,所以 obj.numer * 5 = 15。


参考 前端进阶面试题详细解答

二分查找--时间复杂度 log2(n)

题目描述:如何确定一个数在一个有序数组中的位置


实现代码如下:


function search(arr, target, start, end) {  let targetIndex = -1;
let mid = Math.floor((start + end) / 2);
if (arr[mid] === target) { targetIndex = mid; return targetIndex; }
if (start >= end) { return targetIndex; }
if (arr[mid] < target) { return search(arr, target, mid + 1, end); } else { return search(arr, target, start, mid - 1); }}// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];// const position = search(dataArr, 6, 0, dataArr.length - 1);// if (position !== -1) {// console.log(`目标元素在数组中的位置:${position}`);// } else {// console.log("目标元素不在数组中");// }
复制代码

哪些操作会造成内存泄漏?

  • 第一种情况是由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

  • 第二种情况是设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。

  • 第三种情况是获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。

  • 第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。

什么是原型什么是原型链?

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head><body>
</body><script> function Person () { } var person = new Person(); person.name = 'Kevin'; console.log(person.name) // Kevin
// prototype function Person () { } Person.prototype.name = 'Kevin'; var person1 = new Person(); var person2 = new Person(); console.log(person1.name)// Kevin console.log(person2.name)// Kevin
// __proto__ function Person () { } var person = new Person(); console.log(person.__proto__ === Person.prototype) // true
//constructor function Person() { } console.log(Person === Person.prototype.constructor) // true
//综上所述 function Person () { } var person = new Person() console.log(person.__proto__ == Person.prototype) // true console.log(Person.prototype.constructor == Person) // true //顺便学习一下ES5得方法,可以获得对象得原型 console.log(Object.getPrototypeOf(person) === Person.prototype) // true
//实例与原型 function Person () { } Person.prototype.name = 'Kevin'; var person = new Person(); person.name = 'Daisy'; console.log(person.name) // Daisy delete person.name; console.log(person.name) // Kevin
//原型得原型 var obj = new Object(); obj.name = 'Kevin', console.log(obj.name) //Kevin
//原型链 console.log(Object.prototype.__proto__ === null) //true // null 表示"没用对象" 即该处不应该有值
// 补充 function Person() { } var person = new Person() console.log(person.constructor === Person) // true //当获取person.constructor时,其实person中并没有constructor属性,当不能读取到constructor属性时,会从person的原型 //也就是Person.prototype中读取时,正好原型中有该属性,所以 person.constructor === Person.prototype.constructor
//__proto__ //其次是__proto__,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于Person.prototype中,实际上,它 // 是来自与Object.prototype,与其说是一个属性,不如说是一个getter/setter,当使用obj.__proto__时,可以理解成返回了 // Object.getPrototypeOf(obj) 总结: 1、当一个对象查找属性和方法时会从自身查找,如果查找不到则会通过__proto__指向被实例化的构造函数的prototype 2、隐式原型也是一个对象,是指向我们构造函数的原型 3、除了最顶层的Object对象没有__proto_,其他所有的对象都有__proto__,这是隐式原型 4、隐式原型__proto__的作用是让对象通过它来一直往上查找属性或方法,直到找到最顶层的Object的__proto__属性,它的值是null,这个查找的过程就是原型链


</script></html>
复制代码

进程与线程的概念

从本质上说,进程和线程都是 CPU 工作时间片的一个描述:


  • 进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。

  • 线程是进程中的更小单位,描述了执行一段指令所需的时间。


进程是资源分配的最小单位,线程是 CPU 调度的最小单位。


一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间。


如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存。


进程和线程之间的关系有以下四个特点:


(1)进程中的任意一线程执行出错,都会导致整个进程的崩溃。


(2)线程之间共享进程中的数据。


(3)当一个进程关闭之后,操作系统会回收进程所占用的内存, 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。


(4)进程之间的内容相互隔离。 进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。


Chrome 浏览器的架构图: 从图中可以看出,最新的 Chrome 浏览器包括:


  • 1 个浏览器主进程

  • 1 个 GPU 进程

  • 1 个网络进程

  • 多个渲染进程

  • 多个插件进程


这些进程的功能:


  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程:其实, GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。


所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。


虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:


  • 更高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。

  • 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。

节流

**节流(throttle)**:触发高频事件,且 N 秒内只执行一次。这就好比公交车,10 分钟一趟,10 分钟内有多少人在公交站等我不管,10 分钟一到我就要发车走人!类似 qq 飞车的复位按钮。


核心思想:使用时间戳或标志来实现,立即执行一次,然后每 N 秒执行一次。如果 N 秒内触发则直接返回。


应用:节流常应用于鼠标不断点击触发、监听滚动事件。


实现:


// 版本一:标志实现function throttle(fn, wait){    let flag = true;  // 设置一个标志    return function(...args){        if(!flag) return;        flag = false;        setTimeout(() => {            fn.call(this, ...args);            flag = true;        }, wait);    }}
// 版本二:时间戳实现function throttle(fn, wait) { let pre = 0; return function(...args) { let now = new Date(); if(now - pre < wait) return; pre = now; fn.call(this, ...args); }}
复制代码

说一说你用过的 css 布局

gird布局,layout布局,flex布局,双飞翼,圣杯布局等
复制代码

虚拟 DOM 转换成真实 DOM

描述:将如下 JSON 格式的虚拟 DOM 结构转换成真实 DOM 结构。


// vnode 结构{    tag: 'DIV',    attrs: {        id: "app"    },    children: [        {            tag: 'SPAN',            children: [                {                    tag: 'A',                    children: []                }            ]        }    ]}// 真实DOM 结构<div id="app">    <span>        <a></a>    </span></div>
复制代码


实现


function _render(vnode) {    // 如果是数字类型转化为字符串;    if(typeof vnode === "number") {        vnode = String(vnode);    }    // 字符串类型直接就是文本节点    if(typeof vnode === "string") {        return document.createTextNode(vnode);    }    // 普通 DOM    const dom = document.createElement(vnode.tag);    if(vnode.attrs) {        // 遍历属性        Object.keys(vnode.attrs).forEach((key) => {            dom.setAttribute(key, vnode.attrs[key]);        });    }    // 子数组进行递归操作    vnode.children.forEach((child) => dom.appendChild(_render(child)));    return dom;}
// 测试let vnode = { tag: "DIV", attrs: { id: "app", }, children: [ { tag: "SPAN", children: [ { tag: "A", children: [], }, ], }, ],};console.log(_render(vnode)); // <div id="app"><span><a></a></span></div>
复制代码

对浏览器的缓存机制的理解

浏览器缓存的全过程:


  • 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时对比使用;

  • 下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上一次返回 200 时的时间差,如果没有超过 cache-control 设置的 max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持 HTTP1.1,则使用 expires 头判断是否过期;

  • 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有 If-None-Match 和 If-Modified-Since 的请求;

  • 服务器收到请求后,优先根据 Etag 的值判断被请求的文件有没有做修改,Etag 值一致则没有修改,命中协商缓存,返回 304;如果不一致则有改动,直接返回新的资源文件带上新的 Etag 值并返回 200;

  • 如果服务器收到的请求没有 Etag 值,则将 If-Modified-Since 和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回 304;不一致则返回新的 last-modified 和文件并返回 200;

  • 很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 JS 或 CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的 JS 或 CSS 文件 ,以保证用户能够及时获得网站的最新更新。

说一下 vue3.0 你了解多少?

 <!-- 响应式原理的改变 Vue3.x 使用Proxy取代 Vue2.x 版本的Object.defineProperty --> <!-- 组件选项声明方式Vue3.x 使用Composition API setup 是Vue3.x新增的一个选项,他    是组件内使用Composition API 的入口 --> <!-- 模板语法变化slot具名插槽语法 自定义指令 v-model 升级 --> <!-- 其它方面的更改Suspense支持Fragment(多个根节点) 和Protal (在dom其他部分渲染组建内容)组件     针对一些特殊的场景做了处理。基于treeshaking优化,提供了更多的内置功能。 -->
复制代码

JS 整数是怎么表示的?

  • 通过 Number 类型来表示,遵循 IEEE754 标准,通过 64 位来表示一个数字,(1 + 11 + 52),最大安全数字是 Math.pow(2, 53) - 1,对于 16 位十进制。(符号位 + 指数位 + 小数部分有效位)

Promise.allSettled

描述:等到所有promise都返回结果,就返回一个promise实例。


实现


Promise.allSettled = function(promises) {    return new Promise((resolve, reject) => {        if(Array.isArray(promises)) {            if(promises.length === 0) return resolve(promises);            let result = [];            let count = 0;            promises.forEach((item, index) => {                Promise.resolve(item).then(                    value => {                        count++;                        result[index] = {                            status: 'fulfilled',                            value: value                        };                        if(count === promises.length) resolve(result);                    },                     reason => {                        count++;                        result[index] = {                            status: 'rejected'.                            reason: reason                        };                        if(count === promises.length) resolve(result);                    }                );            });        }        else return reject(new TypeError("Argument is not iterable"));    });}
复制代码

setInterval 模拟 setTimeout

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


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


实现


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
复制代码

Promise.resolve

Promise.resolve = function(value) {    // 1.如果 value 参数是一个 Promise 对象,则原封不动返回该对象    if(value instanceof Promise) return value;    // 2.如果 value 参数是一个具有 then 方法的对象,则将这个对象转为 Promise 对象,并立即执行它的then方法    if(typeof value === "object" && 'then' in value) {        return new Promise((resolve, reject) => {           value.then(resolve, reject);        });    }    // 3.否则返回一个新的 Promise 对象,状态为 fulfilled    return new Promise(resolve => resolve(value));}
复制代码

归并排序--时间复杂度 nlog(n)

题目描述:实现一个时间复杂度为 nlog(n)的排序算法


实现代码如下:


function merge(left, right) {  let res = [];  let i = 0;  let j = 0;  while (i < left.length && j < right.length) {    if (left[i] < right[j]) {      res.push(left[i]);      i++;    } else {      res.push(right[j]);      j++;    }  }  if (i < left.length) {    res.push(...left.slice(i));  } else {    res.push(...right.slice(j));  }  return res;}
function mergeSort(arr) { if (arr.length < 2) { return arr; } const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); return merge(left, right);}// console.log(mergeSort([3, 6, 2, 4, 1]));
复制代码


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
校招前端必会面试题及答案_JavaScript_loveX001_InfoQ写作社区