写点什么

前端工程师面试题自检篇(二)

作者:loveX001
  • 2022-10-12
  • 本文字数:9616 字

    阅读完需:约 32 分钟

请实现 DOM2JSON 一个函数,可以把一个 DOM 节点输出 JSON 的格式

题目描述:


<div>  <span>    <a></a>  </span>  <span>    <a></a>    <a></a>  </span></div>
把上诉dom结构转成下面的JSON格式
{ tag: 'DIV', children: [ { tag: 'SPAN', children: [ { tag: 'A', children: [] } ] }, { tag: 'SPAN', children: [ { tag: 'A', children: [] }, { tag: 'A', children: [] } ] } ]}
复制代码


实现代码如下:


function dom2Json(domtree) {  let obj = {};  obj.name = domtree.tagName;  obj.children = [];  domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));  return obj;}
复制代码


扩展思考:如果给定的不是一个 Dom 树结构 而是一段 html 字符串 该如何解析?


那么这个问题就类似 Vue 的模板编译原理 我们可以利用正则 匹配 html 字符串 遇到开始标签 结束标签和文本 解析完毕之后生成对应的 ast 并建立相应的父子关联 不断的 advance 截取剩余的字符串 直到 html 全部解析完毕

关于原型的继承我们借助寄生组合继承

function Person(obj) {    this.name = obj.name    this.age = obj.age}Person.prototype.add = function(value){    console.log(value)}var p1 = new Person({name:"番茄", age: 18})
function Person1(obj) { Person.call(this, obj) this.sex = obj.sex}// 这一步是继承的关键Person1.prototype = Object.create(Person.prototype)Person1.prototype.play = function(value){ console.log(value)}var p2 = new Person1({name:"鸡蛋", age: 118, sex: "男"})
复制代码

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

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

说一下类组件和函数组件的区别?

1. 语法上的区别:
函数式组件是一个纯函数,它是需要接受props参数并且返回一个React元素就可以了。类组件是需要继承React.Component的,而且class组件需要创建render并且返回React元素,语法上来讲更复杂。
2. 调用方式
函数式组件可以直接调用,返回一个新的React元素;类组件在调用时是需要创建一个实例的,然后通过调用实例里的render方法来返回一个React元素。
3. 状态管理
函数式组件没有状态管理,类组件有状态管理。
4. 使用场景
类组件没有具体的要求。函数式组件一般是用在大型项目中来分割大组件(函数式组件不用创建实例,所有更高效),一般情况下能用函数式组件就不用类组件,提升效率。
复制代码

说一下你对盒模型的理解?

CSS3中的盒模型有以下两种:标准盒模型、IE盒模型盒模型都是由四个部分组成的,分别是margin、border、padding和content标准盒模型和IE盒模型的区别在于设置width和height时, 所对应的范围不同1、标准盒模型的width和height属性的范围只包含了content2、IE盒模型的width和height属性的范围包含了border、padding和content可以通过修改元素的box-sizing属性来改变元素的盒模型;1、box-sizing:content-box表示标准盒模型(默认值)2、box-sizing:border-box表示IE盒模型(怪异盒模型)
复制代码

写版本号排序的方法

题目描述:有一组版本号如下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']


实现代码如下:


arr.sort((a, b) => {  let i = 0;  const arr1 = a.split(".");  const arr2 = b.split(".");
while (true) { const s1 = arr1[i]; const s2 = arr2[i]; i++; if (s1 === undefined || s2 === undefined) { return arr2.length - arr1.length; }
if (s1 === s2) continue;
return s2 - s1; }});console.log(arr);
复制代码

冒泡排序--时间复杂度 n^2

题目描述:实现一个冒泡排序


实现代码如下:


function bubbleSort(arr) {  // 缓存数组长度  const len = arr.length;  // 外层循环用于控制从头到尾的比较+交换到底有多少轮  for (let i = 0; i < len; i++) {    // 内层循环用于完成每一轮遍历过程中的重复比较+交换    for (let j = 0; j < len - 1; j++) {      // 若相邻元素前面的数比后面的大      if (arr[j] > arr[j + 1]) {        // 交换两者        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];      }    }  }  // 返回数组  return arr;}// console.log(bubbleSort([3, 6, 2, 4, 1]));
复制代码

DNS 如何工作的

DNS 的作用就是通过域名查询到具体的 IP。DNS 协议提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。是应用层协议,通常该协议运行在 UDP 协议之上,使用的是 53 端口号。


因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。


当你在浏览器中想访问 www.google.com 时,会通过进行以下操作:


  • 本地客户端向服务器发起请求查询 IP 地址

  • 查看浏览器有没有该域名的 IP 缓存

  • 查看操作系统有没有该域名的 IP 缓存

  • 查看 Host 文件有没有该域名的解析配置

  • 如果这时候还没得话,会通过直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器

  • 然后去该服务器查询 google.com 这个二级域名

  • 接下来查询 www.google.com 这个三级域名的地址

  • 返回给 DNS 客户端并缓存起来



我们通过一张图来看看它的查询过程吧 👇



这张图很生动的展示了 DNS 在本地 DNS 服务器是如何查询的,一般向本地 DNS 服务器发送请求是递归查询的


本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程👇



递归查询和迭代查询


  • 递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归 查询,用户只需要发出一次查询请求。

  • 迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出 多次的查询请求。


所以一般而言, 本地服务器查询是递归查询 ,而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程


DNS 缓存


缓存也很好理解,在一个请求中,当某个 DNS 服务器收到一个 DNS 回答后,它能够回答中的信息缓存在本地存储器中。返回的资源记录中的 TTL 代表了该条记录的缓存的时间。


DNS 实现负载平衡


它是如何实现负载均衡的呢?首先我们得清楚 DNS 是可以用于在冗余的服务器上实现负载平衡。


原因: 这是因为一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应 多个服务器地址。


举个例子来说👇


  • 当用户发起网站域名的 DNS 请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合

  • 在每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。

  • 以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。


DNS 为什么使用 UDP 协议作为传输层协议?


DNS 使用 UDP 协议作为传输层协议的主要原因是为了避免使用 TCP 协议时造成的连接时延


  • 为了得到一个域名的 IP 地址,往往会向多个域名服务器查询,如果使用 TCP 协议,那么每次请求都会存在连接时延,这样使 DNS 服务变得很慢。

  • 大多数的地址查询请求,都是浏览器请求页面时发出的,这样会造成网页的等待时间过长。


总结


  • DNS 域名系统,是应用层协议,运行 UDP 协议之上,使用端口 43。

  • 查询过程,本地查询是递归查询,依次通过浏览器缓存 —>> 本地 hosts 文件 —>> 本地 DNS 解析器 —>>本地 DNS 服务器 —>> 其他域名服务器请求。 接下来的过程就是迭代过程。

  • 递归查询一般而言,发送一次请求就够,迭代过程需要用户发送多次请求。

Loader 和 Plugin 有什么区别

Loader:直译为"加载器"。Webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到loader。 所以 Loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力。 Plugin:直译为"插件"。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。


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

DNS 完整的查询过程

DNS 服务器解析域名的过程:


  • 首先会在浏览器的缓存中查找对应的 IP 地址,如果查找到直接返回,若找不到继续下一步

  • 将请求发送给本地 DNS 服务器,在本地域名服务器缓存中查询,如果查找到,就直接将查找结果返回,若找不到继续下一步

  • 本地 DNS 服务器向根域名服务器发送请求,根域名服务器会返回一个所查询域的顶级域名服务器地址

  • 本地 DNS 服务器向顶级域名服务器发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的地址

  • 本地 DNS 服务器向权威域名服务器发送请求,域名服务器返回对应的结果

  • 本地 DNS 服务器将返回结果保存在缓存中,便于下次使用

  • 本地 DNS 服务器将返回结果返回给浏览器


比如要查询 IP 地址,首先会在浏览器的缓存中查找是否有该域名的缓存,如果不存在就将请求发送到本地的 DNS 服务器中,本地 DNS 服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com 的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。

DNS 同时使用 TCP 和 UDP 协议?

DNS 占用 53 号端口,同时使用 TCP 和 UDP 协议。 (1)在区域传输的时候使用 TCP 协议


  • 辅域名服务器会定时(一般 3 小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用 TCP 而不是 UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。

  • TCP 是一种可靠连接,保证了数据的准确性。


(2)在域名解析的时候使用 UDP 协议


  • 客户端向 DNS 服务器查询域名,一般返回的内容都不超过 512 字节,用 UDP 传输即可。不用经过三次握手,这样 DNS 服务器负载更低,响应更快。理论上说,客户端也可以指定向 DNS 服务器查询时用 TCP,但事实上,很多 DNS 服务器进行配置的时候,仅支持 UDP 查询包。

Ajax

它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。



面试手写(原生):


//1:创建Ajax对象var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本//2:配置 Ajax请求地址xhr.open('get','index.xml',true);//3:发送请求xhr.send(null); // 严谨写法//4:监听请求,接受响应xhr.onreadysatechange=function(){     if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 )          console.log(xhr.responsetXML)}
复制代码


jQuery 写法


$.ajax({  type:'post',  url:'',  async:ture,//async 异步  sync  同步  data:data,//针对post请求  dataType:'jsonp',  success:function (msg) {
}, error:function (error) {
}})
复制代码


promise 封装实现:


// promise 封装实现:
function getJSON(url) { // 创建一个 promise 对象 let promise = new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest();
// 新建一个 http 请求 xhr.open("GET", url, true);
// 设置状态的监听函数 xhr.onreadystatechange = function() { if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态 if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } };
// 设置错误监听函数 xhr.onerror = function() { reject(new Error(this.statusText)); };
// 设置响应的数据类型 xhr.responseType = "json";
// 设置请求头信息 xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求 xhr.send(null); });
return promise;}
复制代码

TCP 的流量控制机制

一般来说,流量控制就是为了让发送方发送数据的速度不要太快,要让接收方来得及接收。TCP 采用大小可变的滑动窗口进行流量控制,窗口大小的单位是字节。这里说的窗口大小其实就是每次传输的数据大小。


  • 当一个连接建立时,连接的每一端分配一个缓冲区来保存输入的数据,并将缓冲区的大小发送给另一端。

  • 当数据到达时,接收方发送确认,其中包含了自己剩余的缓冲区大小。(剩余的缓冲区空间的大小被称为窗口,指出窗口大小的通知称为窗口通告 。接收方在发送的每一确认中都含有一个窗口通告。)

  • 如果接收方应用程序读数据的速度能够与数据到达的速度一样快,接收方将在每一确认中发送一个正的窗口通告。

  • 如果发送方操作的速度快于接收方,接收到的数据最终将充满接收方的缓冲区,导致接收方通告一个零窗口 。发送方收到一个零窗口通告时,必须停止发送,直到接收方重新通告一个正的窗口。

说一下 JSON.stringify 有什么缺点?

1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式2.如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;3、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;6、如果对象中存在循环引用的情况也无法正确实现深拷贝;
复制代码

This

不同情况的调用,this指向分别如何。顺带可以提一下 es6 中箭头函数没有 this, arguments, super 等,这些只依赖包含箭头函数最接近的函数


我们先来看几个函数调用的场景


function foo() {  console.log(this.a)}var a = 1foo()
const obj = { a: 2, foo: foo}obj.foo()
const c = new foo()
复制代码


  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是window

  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象

  • 对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this


说完了以上几种情况,其实很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this


function a() {  return () => {    return () => {      console.log(this)    }  }}console.log(a()()())
复制代码


  • 首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 thiswindow。另外对箭头函数使用 bind这类函数是无效的。

  • 最后种情况也就是 bind 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window

  • 那么说到 bind,不知道大家是否考虑过,如果对一个函数进行多次 bind,那么上下文会是什么呢?


let a = {}let fn = function () { console.log(this) }fn.bind().bind(a)() // => ?
复制代码


如果你认为输出结果是 a,那么你就错了,其实我们可以把上述代码转换成另一种形式


// fn.bind().bind(a) 等于let fn2 = function fn1() {  return function() {    return fn.apply()  }.apply(a)}fn2()
复制代码


可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window


let a = { name: 'poetries' }function foo() {  console.log(this.name)}foo.bind(a)() // => 'poetries'
复制代码


以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。


首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。



函数执行改变 this


  • 由于 JS 的设计原理: 在函数中,可以引用运行环境中的变量。因此就需要一个机制来让我们可以在函数体内部获取当前的运行环境,这便是this


因此要明白 this 指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如


  • obj.fn(),便是 obj 调用了函数,既函数中的 this === obj

  • fn(),这里可以看成 window.fn(),因此 this === window


但这种机制并不完全能满足我们的业务需求,因此提供了三种方式可以手动修改 this 的指向:


  • call: fn.call(target, 1, 2)

  • apply: fn.apply(target, [1, 2])

  • bind: fn.bind(target)(1,2)

深浅拷贝

浅拷贝:只考虑对象类型。


function shallowCopy(obj) {    if (typeof obj !== 'object') return
let newObj = obj instanceof Array ? [] : {} for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key] } } return newObj}
复制代码


简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数。


function deepClone(obj) {    if (typeof obj !== 'object') return;    var newObj = obj instanceof Array ? [] : {};    for (var key in obj) {        if (obj.hasOwnProperty(key)) {            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];        }    }    return newObj;}
复制代码


复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。


const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) { if (map.get(target)) { return target; } // 获取当前值的构造函数:获取它的类型 let constructor = target.constructor; // 检测当前对象target是否与正则、日期格式对象匹配 if (/^(RegExp|Date)$/i.test(constructor.name)) { // 创建一个新的特殊对象(正则类/日期类)的实例 return new constructor(target); } if (isObject(target)) { map.set(target, true); // 为循环引用的对象做标记 const cloneTarget = Array.isArray(target) ? [] : {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop], map); } } return cloneTarget; } else { return target; }}
复制代码

清除浮动

  1. 在浮动元素后面添加 clear:both的空 div 元素


<div class="container">    <div class="left"></div>    <div class="right"></div>    <div style="clear:both"></div></div>
复制代码


  1. 给父元素添加 overflow:hidden 或者 auto 样式,触发BFC


<div class="container">    <div class="left"></div>    <div class="right"></div></div>
复制代码


.container{    width: 300px;    background-color: #aaa;    overflow:hidden;    zoom:1;   /*IE6*/}
复制代码


  1. 使用伪元素,也是在元素末尾添加一个点并带有 clear: both 属性的元素实现的。


<div class="container clearfix">    <div class="left"></div>    <div class="right"></div></div>
复制代码


.clearfix{    zoom: 1; /*IE6*/}.clearfix:after{    content: ".";    height: 0;    clear: both;    display: block;    visibility: hidden;}
复制代码


推荐使用第三种方法,不会在页面新增 div,文档结构更加清晰

节流

节流(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); }}
复制代码

函数柯里化

什么叫函数柯里化?其实就是将使用多个参数的函数转换成一系列使用一个参数的函数的技术。还不懂?来举个例子。


function add(a, b, c) {    return a + b + c}add(1, 2, 3)let addCurry = curry(add)addCurry(1)(2)(3)
复制代码


现在就是要实现 curry 这个函数,使函数从一次调用传入多个参数变成多次调用每次传一个参数。


function curry(fn) {    let judge = (...args) => {        if (args.length == fn.length) return fn(...args)        return (...arg) => judge(...args, ...arg)    }    return judge}
复制代码

迭代查询与递归查询

实际上,DNS 解析是一个包含迭代查询和递归查询的过程。


  • 递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归 查询,用户只需要发出一次查询请求。

  • 迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出 多次的查询请求。


一般我们向本地 DNS 服务器发送请求的方式就是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我 们最终的请求结果。而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次 查询的结果,下一级的查询由本地 DNS 服务器自己进行。

用户头像

loveX001

关注

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

还未添加个人简介

评论

发布
暂无评论
前端工程师面试题自检篇(二)_JavaScript_loveX001_InfoQ写作社区