前端面试题搜集自我解答

阿里钉钉

1

一面

1. 元素如何水平垂直居中

HTML中的元素可分为两种类型:块级元素和行级元素。这些元素的类型是通过文档类型定义(DTD)来指明。

  • 块级元素:显示在一块内,会自动换行,元素会从上到下垂直排列,各自占一行,如p,ul,form,div等标签元素。
  • 行内元素:元素在一行内水平排列,高度由元素的内容决定,height属性不起作用,如span,input等元素。

行内元素一般都是基于语义级(semantic)的基本元素,只能容纳文本或其他内联元素,常见内联元素 “a”。比如 SPAN 元素,IFRAME元素和元素样式的display : inline的都是行内元素。例如文字这类元素,各个字母 之间横向排列,到最右端自动折行。

inline元素的特点:

①和其他元素都在一行上;

②高,行高及外边距和内边距不可改变;

③宽度就是它的文字或图片的宽度,不可改变

④内联元素只能容纳文本或者其他内联元素

对行内元素,需要注意如下

设置宽度width 无效。
设置高度height 无效,可以通过line-height来设置。
设置margin 只有左右margin有效,上下无效。
设置padding只有左右padding有效,上下则无效。注意元素范围是增大了,但是对元素周围的内容是没影响的。

块级元素的特点:

①总是在新行上开始;

②高度,行高以及外边距和内边距都可控制;

③宽度缺省是它的容器的100%,除非设定一个宽度。

④它可以容纳内联元素和其他块元素

解答:

(1) 水平居中
  • 行内元素水平居中:只需给父元素设置text-align:center实现。
  • 块级元素水平居中
    • 定宽块级元素:给需要居中的块级元素设置margin: 0 auto,注意块级元素的宽度width一定要有值
      • 不定宽块级元素:
        1. 设置table 给需要居中的块级元素设置display: table, margin: 0 auto
        2. 设置inline-block 子元素设置display:inline-block,父元素设置text-align:center
        3. 设置flex布局 父元素设置diplay:flex, justify-content:center
        4. 利用position 设置父元素position:relative,设置子元素position:absolute;left:0;right:0;margin:auto
(2) 垂直居中
  • 单行文本垂直居中:paddingtop = paddingbottem;或者 line-height = height
  • 多行文本垂直居中:父元素 display:table,子元素(需要居中的元素)display:table-cell和vertical-align:middle
  • 块级元素垂直居中
    1. 设置flex布局 父元素设置display:flex,align-items:center,并且父元素必须设置height值
    2. 利用position 设置父元素position:relative,设置子元素position:absolute;top:0;bottom:0;margin:auto
    3. 利用position+transform 设置父元素position:relative,设置子元素position:absolute;top:50%;transform:translate(0, -50%) 该方法可用于未知元素大小的居中
(3)水平垂直居中
  • 绝对定位+margin:auto
    div{
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      top: 0;
      margin: auto;
    }
  • 绝对定位+transform
    div{
      position:absolute;
      left:50%;//父级的50%
      top:50%;
      transform: translate(-50%, 50%);//自己的50%
    }
  • flex布局
    .parent{
      display: flex;
      justify-content:center;
      align-items: center;
    }
  • table-cell实现
    .parent{
      display:table;
    }    
    .child{
      display:table-cell;
      vertical-align:middle;
      text-align:center;
    }

2. fixed relative absolute的区别

  • static(静态定位):默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)。
  • relative(相对定位):生成相对定位的元素,通过top,bottom,left,right的设置相对于其正常(原先本身)位置进行定位。可通过z-index进行层次分级。 特征:不会脱离正常流
  • absolute(绝对定位):生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。可通过z-index进行层次分级。 特征:脱离正常流
  • fixed(固定定位):生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。可通过z-index进行层次分级。 特征:脱离正常流

absolute与fixed
图片说明

absolute与relative

1、absolute参照的是父级元素的左上角;relative参照元素的原始点(比如,"left: 20" 会向元素的 left 位置添加 20 像素)

2、relative的z-index不能定义父子的上下关系,一定是子上父下;absolute多个层可以使用z-index属性改变层重叠顺序

3. 事件冒泡/捕获

详见JS学习系列17-事件的传播
图片说明

4. 跨域的方法

同源策略

同源策略是一种约定,它是浏览器最核心也最基本的安全功能。所谓同源是指"协议+域名+端口"三者相同。
图片说明

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了
    有三个标签是允许跨域加载资源:
    <img src=XXX>
    <link href=XXX>
    <script src=XXX>
    第一:如果是协议和端口造成的跨域问题“前台”是无能为力的。

第二:在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”。

有一个问题:请求跨域了,那么请求到底发出去没有?

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

浏览器出于安全的考虑,引入了同源策略。这种策略会对我们页面上执行的js访问资源的时候进行限制,比如我们不能直接通过js访问不同源之下的页面DOM结构,同时在对不同源发送请求时也无法获取到服务器响应内容(服务器会正常处理请求并返回响应内容,但是返回的内容被浏览器拦截掉了)。

同源策略对js访问一些敏感资源则进行了限制。比如js中无法访问不属于同个源的cookie、LocalStorage中存储的内容。具体来说,cookie和LocalStorage在控制哪些源可以访问的问题上还是细微的差别,父域在设置cookie的时候可以设定允许子域访问这段cookie,同时Cookie只和域名以及路径关联,如果是同个域名不同端口的源依然是共享同个域名下的Cookie的,而LocalStorage则是以源为单位进行管理,相互独立,不同源之间无法相互访问LocalStorage中的内容。

跨域方法

客户端与服务端通信

客户端与不同源的服务器的通信问题是平常开发过程中需要解决的很常见的问题:

  • CORS

基本思想就是引入一些自定义的HTTP Header来完成客户端与服务端的通信。后端是实现 CORS 通信的关键。

对于一些简单请求,浏览器在发送请求时会带上Origin请求头,指示当前的源,服务器端在处理请求时不会去检查当前请求来源是否合法,依然会正常处理请求并响应,最终浏览器在拿到响应之后会检查服务端响应的Access-Control-Allow-Origin列表中是否存在当前页面所在的源,如果不存在会直接block掉当前请求。

在浏览器看来,同时满足以下条件的请求都认为是简单请求:

请求方法为GET或者POST;
只包含Accept、Accept-Language、Content-Language或者Content-Type(取值为application/x-www-form-urlencoded,multipart/form-data, 或者text/plain),其余情况的Header则属于非简单Header;

对于非简单请求,浏览器会先向服务器发送一个Preflight请求,该请求使用Option方法,并包含以下Header:

(1) Origin

(2) Access-Control-Request-Method:询问服务器是否支持某方法;

(3) Access-Control-Request-Headers:询问服务器是否支持请求中包含的非简单Header;

其中后两个Header只会出现在Preflight请求中。然后浏览器收到包含以下Header的服务器响应:

(1)Access-Control-Allow-Origin

(2)Access-Control-Allow-Methods:对客户端回应服务器支持的请求方法列表;

(3)Access-Control-Allow-Headers:对客户端回应服务器支持的Header;

之后浏览器会检查当前请求发出的源是否在服务端响应的Access-Control-Allow-Origin列出的源的列表中
如果是才会发送真正的请求。
在实验过程中,浏览器并不一定要在服务器支持Preflight请求查询的请求方法和Header时才发送真正的请求
只要发出请求的源是合法的就会在Preflight请求之后把请求发出去。

对于客户端,我们还是正常使用xhr对象发送ajax请求。

唯一需要注意的是,我们需要设置我们的xhr属性withCredentials为true,不然的话,cookie是带不过去的哦,设置: xhr.withCredentials = true;

对于服务器端,需要在 response header中设置如下两个字段:Access-Control-Allow-Origin: http://www.yourhost.comAccess-Control-Allow-Credentials:true这样,我们就可以跨域请求接口了。

  • JSONP

原理:利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

流程:

(1)声明一个回调函数,其函数名(如fun)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。

(2)创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=fun)。

(3)服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串

(4)最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(fun),对返回的数据进行操作。

// 定义一个fun函数
function fun(fata) {
    console.log(data);
};
// 创建一个脚本,并且告诉后端回调函数名叫fun
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.type = 'text/javasctipt';
script.src = 'demo.js?callback=fun';
body.appendChild(script);
返回的js脚本,直接会执行。所以就执行了事先定义好的fun函数了,并且把数据传入了进来。

fun({"name": "name"})

实际情况下,我们需要动态创建这个fun函数,并且在数据返回的时候销毁它。

JSONP这种方式本身也是存在一定缺陷的,很明显它只能用于GET请求。另外,后端应用程序在处理过程可能会出现4xx、5xx错误或者遇到其他意外情况,导致无法返回正确的js函数调用格式的字符串的情况,所以还需要监听script标签的onerror事件来处理可能出现的意外情况。

  • Cookie跨域共享

跨页面通信

需要访问其他页面上的一些信息,或者将一些数据持久化,以供其他页面取用。

  • document.domain

通过这种方式跨域的两个源需要满足一定的条件的,即两个源的域名需要是父子域的关系或者是相同的域。

因为页面设置document.domain的值只能是当前域本身,或者是父域,而不能是其他不相关的域名。只有两个页面的document.domain都设置成相同的值,嵌入iframe的页面和iframe加载的页面才能相互获取到彼此的页面信息(包括DOM结构、window对象等)。

对于主域名相同,而子域名不同的情况,可以使用document.domain来跨域 这种方式非常适用于iframe跨域的情况,直接看例子吧 比如a页面地址为 http://a.yourhost.com b页面为 http://b.yourhost.com。 这样就可以通过分别给两个页面设置 document.domain = http://yourhost.com 来实现跨域。 之后,就可以通过 parent 或者 window[‘iframename’]等方式去拿到iframe的window对象了

在实践中也发现需要注意的两个问题:

(1)如果两个页面所在的源是一样的,可以直接通信,但是如果两个页面所在的域名相同但端口不同或者是其他情况,那么两个页面仍需要设置相同的document.domain,否则还是会被浏览器block掉。具体原因在MDN上也有提到:

浏览器单独保存端口号。任何的赋值操作,包括document.domain = document.domain都会导致端口号被重写为null。因此company.com:8080不能仅通过设置document.domain = "company.com"来与company.com通信。必须在他们双方中都进行赋值,以确保端口号都为null。

(2)需要在嵌入的iframe加载完成之后才能和其加载的子页面进行通信,否则拿到的值可能还是undefined。

  • window对象name属性

浏览器具有这样一个特性:同一个标签页或者同一个iframe框架加载过的页面共享相同的window.name属性值,意味着只要是在同一个标签页里面打开过的页面(不管是否同源),这些页面上window.name属性值都是相同的。利用这个特性,就可以将这个属性作为在不同页面之间传递数据的介质。

如果是通过iframe+window.name这种方式在完全没有父子域关系的两个源之间传递数据(假设源A要获取源B中的数据),源A页面上的iframe在加载源B的目标页面(源B页面把数据设置在window.name属性上)之后还需要再跳转到源A的某个页面上,以便于嵌入iframe的页面通过和在iframe中的页面将document.domain都设置为源A的方式来获取iframe中的数据。示例代码如下:

// www.a.com/getData.html
<script type="text/javascript">
function getData() {
  var frame = document.getElementsByTagName("iframe")[0];
  frame.onload = function () {
    var data = frame.contentWindow.name;
    // 此处获取数据
    alert(data);
  };
  frame.contentWindow.location = "./aaa.html";
  // 加载完www.b.com/data.html之后就加载www.a.com/下随便一个页面,获取数据
}
</script>
<iframe src="http://www.b.com/data.html" style="display: none;" onload="getData();"></iframe>
  • postMessage

window.postMessage是一个HTML5的api,允许两个窗口之间进行跨域发送消息。

参考网站什么是JS跨域访问?

5. 一个关键字搜索框,如何设计内部的数据结构以及前端如何做?trie树+防抖

  • trie树介绍

Trie 树,也叫“字典树”。顾名思义,它是一个树形结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。

此外 Trie 树也称前缀树(因为某节点的后代存在共同的前缀,比如pan是panda的前缀)

它的key都为字符串,能做到高效查询和插入,时间复杂度为O(k),k为字符串长度,缺点是如果大量字符串没有共同前缀时很耗内存。

它的核心思想就是通过最大限度地减少无谓的字符串比较,使得查询高效率,即「用空间换时间」,再利用共同前缀来提高查询效率。

假设有 5 个字符串,它们分别是:code,cook,five,file,fat,构建如下的结构会使查找更为迅速:
图片说明

三个特点:
根节点不包含字符,除根节点外每一个节点都只包含一个字符
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
每个节点的所有子节点包含的字符都不相同

插入

Trie树的插入操作很简单,其实就是将单词的每个字母逐一插入 Trie树。插入前先看字母对应的节点是否存在,存在则共享该节点,不存在则创建对应的节点。

查询

在 Trie 树中查找一个字符串的时候,比如查找字符串 code,可以将要查找的字符串分割成单个的字符 c,o,d,e,然后从 Trie 树的根节点开始匹配

如果沿路比较,发现不同的字符,则表示该字符串在集合中不存在。
如果所有的字符全部比较完并且全部相同,还需判断最后一个节点的标志位(标记该节点是否代表一个关键字)

删除

Trie树的删除操作与二叉树的删除操作有类似的地方,需要考虑删除的节点所处的位置,这里分三种情况进行分析:

1. 删除整个单词:
从根节点开始查找第一个字符h
找到h子节点后,继续查找h的下一个子节点i
i是单词hi的标志位,将该标志位去掉
i节点是hi的叶子节点,将其删除
删除后发现h节点为叶子节点,并且不是单词标志位,也将其删除
这样就完成了hi单词的删除操作
2. 删除前缀单词:查找到最后一位时,只需将其单词标志去掉即可。
3. 删除分支单词:
删除到 cook 的第一个 o 时,该节点为非叶子节点,停止删除,这样就完成cook字符串的删除操作。

Trie树可以对大量字符串按字典序进行排序,思路也很简单:遍历一次所有关键字,将它们全部插入trie树,树的每个结点的所有儿子很显然地按照字母表排序,然后先序遍历输出Trie树中所有关键字即可。

  • 防抖和节流

函数节流throttle:确保函数特定的时间内至多执行一次。
函数防抖debounce:函数在特定的时间内不被再调用后执行。

以“输入框输入文字触发ajax获取数据”为例,分别以防抖和节流的思想来优化,二者的区别:

函数节流是从用户开始输入就开始计时,而函数防抖是从用户停止输入开始计时。

// 节流
function throttle(fn, time) {
    // 设置初始时间
    let pre = 0;
    // 返回一个新的函数
    return () => {
        // 记录当前时间
        let now = Date.now();
        // 通过时间差来进行节流
        if (now - pre > time) {
            // 执行fn函数
            fn.apply(this, arguments);
            // 更新pre的时间
            pre = now;
        }
    }
}

// 防抖
function debounce(fn, time, isNow) {
    // 设置定时器变量
    let timer;
    return () => {
        // 默认首次是立即触发的,不应该一上来就延迟执行fn
        if (isNow) {
            fn.apply(this, arguments);
            isNow = false;
            return;
        }
        // 如果上一个定时器还在执行,就直接返回    
        if (timer) return;
        // 设置定时器
        timer = setTimeout(() => {
            fn.apply(this, arguments);
            // 清除定时器
            clearTimeout(timer);
            timer = null;
        }, time);
    }
}

结合实际举个例子:
<input onblur="checkUsername" placeholder="请输入用户名" id="username" />

var timer;
var count = 0;
var $username = document.getElementById("username");
$username.addEventListener("input", debounce, false);

function debounce() {
  console.log(++count);
  clearTimeout(timer);
  timer = setTimeout(function() {
    console.log("发请求到后台,检查用户是否已注册");
  }, 1000);
}

6. vue的双向绑定原理

详见Vue高频难点博文
图片说明

  1. 2个算法题:Leetcode165.比较版本号
思路转换成数组后补零对齐进行比较:
var compareVersion = function(version1, version2) {
    var arr1 = version1.split('.');
    var arr2 = version2.split('.');
    var flag = 0;
    if(arr1.length < arr2.length){
        var diff = arr2.length - arr1.length;
        while(diff){
            arr1.push(0);
            diff--;
        }
    }else{
        var diff = arr1.length - arr2.length;
        while(diff){
            arr2.push(0);
            diff--;
        }
    }
    var length = arr1.length;
    for(let i = 0; i < length; i++)
    {
        if(parseInt(arr1[i]) < parseInt(arr2[i]))
        {
            flag = -1;
            break;
        }else if(parseInt(arr1[i]) > parseInt(arr2[i]))
        {
            flag = 1;
            break;
        }
    }
    return flag;
};

这里还有个很不错的方法:https://leetcode-cn.com/problems/compare-version-numbers/solution/jsfen-ge-zi-fu-chuan-zi-fu-chuan-zhuan-huan-cheng-/

Leetcode25. K 个一组翻转链表

先实现一个反转链表——迭代法:
var reverseList = function(head) {
    let [prev, curr] = [null, head];
    while (curr) {
        let tmp = curr.next;    // 1. 临时存储当前指针后续内容
        curr.next = prev;       // 2. 反转链表
        prev = curr;            // 3. 接收反转结果
        curr = tmp;             // 4. 接回临时存储的后续内容
    }
    return prev;
};
反转链表——递归法:
var reverseList = function(head) {
    if(!head || !head.next) return head
    var next = head.next
    // 递归反转
    var reverseHead = reverseList(next)
    // 变更指针
    next.next = head
    head.next = null
    return reverseHead
};

再来看这道题(我写的太差了,拿题解的看看吧)
const myReverse = (head, tail) => {
    let prev = tail.next;
    let p = head;
    while (prev !== tail) {
        const nex = p.next;
        p.next = prev;
        prev = p;
        p = nex;
    }
    return [tail, head];
}
var reverseKGroup = function(head, k) {
    const hair = new ListNode(0);
    hair.next = head;
    let pre = hair;

    while (head) {
        let tail = pre;
        // 查看剩余部分长度是否大于等于 k
        for (let i = 0; i < k; ++i) {
            tail = tail.next;
            if (!tail) {
                return hair.next;
            }
        }
        const nex = tail.next;
        [head, tail] = myReverse(head, tail);
        // 把子链表重新接回原链表
        pre.next = head;
        tail.next = nex;
        pre = tail;
        head = tail.next;
    }
    return hair.next;
};

二面

1. 逻辑题:11223344排序后,使序列满足两个1之间有1个数,两个2之间有2个数,两个3之间有3个数,两个4之间有4个数。

41312432

2. js深拷贝

  • 浅拷贝: 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 深拷贝: 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

浅拷贝的实现方式:

(1)Object.assign() 方法: 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

(2)Array.prototype.slice():slice() 方法返回一个新的数组对象,这一对象是一个由 begin和end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。

(3)拓展运算符...:

let a = {
    name: "Jake",
    flag: {
        title: "better day by day",
        time: "2020-05-31"
    }
}
let b = {...a};

深拷贝的实现方式:

(1)JSON.sringify 和 JSON.parse 原理就是先将对象转换为字符串,再通过JSON.parse重新建立一个对象。 但是这种方法的局限也很多:不能复制function、正则、Symbol;循环引用报错;相同的引用会被重复复制

let obj = {         
    reg : /^asd$/,
    fun: function(){},
    syb:Symbol('foo'),
    asd:'asd'
}; 
let cp = JSON.parse(JSON.stringify(obj));
console.log(cp);

(2) 基础版(面试够用): 浅拷贝+递归 (只考虑了普通的 object和 array两种数据类型)

function cloneDeep(target,map = new WeakMap()) {
  if(typeOf taret ==='object'){
     let cloneTarget = Array.isArray(target) ? [] : {};//处理object和array

     if(map.get(target)) {
        return target;//重复引用直接返回
    }
     map.set(target, cloneTarget);
     for(const key in target){
        cloneTarget[key] = cloneDeep(target[key], map);
     }
     return cloneTarget
  }else{
       return target
  }
}

交叉面

让我介绍一下这个项目的背景。意识到STAR原则的重要性,心想之后要好好改一下自我介绍了。

介绍完毕后,大概和面试官聊了一下项目。问了具体的技术。然后聊了聊MVVM框架,又聊到了数据双向绑定的原理,虚拟DOM等等。

解释MVVM框架

数据双向绑定愿意

虚拟DOM及优缺点

一道逻辑题,八个小球,七个小球一样,另一个小球重量未知的题,我想了2~3分钟,想到了知道另一个小球重或轻的最少称法

取六个两边称  若平衡  取剩下两个钟的随机一个与六个中的随机一个称 若平衡 不一样的是剩下那个  若不平衡  不一样的是这个;
若不平衡 称不平衡中三个中的两个
    若平衡 剩下那个不一样
    若不平衡随机取其中一个与剩下一个称(若平衡 不一样的是另一个;若不平衡,不一样的是这个)

最多称3次

哔哩哔哩

1

一面

1. 为什么学前端

2. 为什么选Vue框架

3. 输入url会发生什么

见笔试题记录:浏览器会去浏览器缓存中寻找该url的ip;没有的话去系统缓存中找,还是没有的话去路由器缓存中寻找;再没有就去系统host文件中找,还是没有最后只能去请求dns服务器,然后dns给一个ip给浏览器;浏览器根据这个ip地址,将请求信息,请求说明和请求参数等封成一个tcp包,由传输层,到网络层,到数据链路层到物理层,传送给服务器,服务器解析这个tcp包将对应的页面文件返回。浏览器根据html文件生成dom树,根据css文件生成cssom树,然后合并这两棵树生成渲染树,然后渲染页面并且展示。要注意的是,当浏览器解析html文件时候如果遇到了内联或者外联的js代码,会暂停dom树的生成,等js代码执行完成之后,才能继续生成树并渲染。

  1. 跨域

5. 什么操作会导致重绘和回流,如何减少

  • 回流=重排:元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候),触发了重新布局,导致渲染树重新计算布局和渲染
    如添加或删除可见的DOM元素;
    元素的位置发生变化;
    元素的尺寸发生变化;
    内容发生变化(比如文本变化或图片被另一个不同尺寸的图片所替代);
    页面一开始渲染的时候(这个无法避免);
    因为回流是根据视口的大小来计算元素的位置和大小的,所以浏览器的窗口尺寸变化也会引发回流......
  • 重绘:元素样式的改变(但宽高、大小、位置等不变)
    如:outline, visibility, color, background-color......等
    重绘不是很消耗性能

回流很消耗性能(DOM元素的大小和位置信息都要重新计算一遍),而且一旦发生回流,重新计算完后,还需要重绘

回流一定会触发重绘,而重绘不一定会回流

  • 如何减少?
  1. 放弃传统操作 DOM 的时代,基于 vue/react 开始数据影响视图模式

利用mvvm / mvc / virtual dom / dom diff ......

vue / react 数据驱动思想 :

我们自己不操作DOM,我们只操作数据,让框架帮我们根据数据渲染视图(框架内部本身对于DOM的回流和重绘以及其它性能优化做的非常好)

  1. 分离读写操作(现代浏览器的渲染队列的机制)
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        .box {
            width: 100px;
            height: 100px;
            background: red;
        }
    </style>
</head>
<body>
    <div class="box" id="box"></div>
    <ul id="item">
        <!-- <li>我是第1个LI</li> -->
    </ul>
</body>
</html>

在老版本的浏览器中,我们分别改变了三次样式(都涉及了位置或者大小的改变),所以触发三次回流和重绘

现代浏览器中默认增加了“渲染队列的机制”,以此来减少DOM的回流和重绘=> 遇到一行修改样式的代码,先放到渲染队列中,继续看下面一行代码是否还为修改样式的,如果是继续增加到渲染队列中...直到下面的代码不再是修改样式的,而是获取样式的代码!此时不再向渲染队列中增加,把之前渲染队列中要修改的样式一次性渲染到页面中,引发一次DOM的回流和重绘

  1. 样式集中改变(不重要)
//=> 通过修改样式类:把样式实现写好,我们后期通过样式类修改样式
// 一次回流重绘
box.className = 'active';

//=> 把所有想写的样式,用cssText的方式添加
// 一次回流重绘
box.style.cssText = 'width:200px;height:200px;';
  1. 缓存布局信息(不重要)

把要操作的内容一次都拿到,然后用变量存储,想设置的时候直接拿变量值即可,不用在重新获取了,和分离读写的原理类似。

div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
=> 改为
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
  1. 元素批量修改(重要)

DOM的增加也会引起回流重绘

可以利用文档碎片:临时创建的一个存放文档的容器,我们可以把新创建的LI,存放到容器中,当所有的LI都存储完,我们统一把容器中的内容增加到页面中(只触发一次回流)

let frag = document.createDocumentFragment();
for (let i = 1; i <= 5; i++) {
    let liBox = document.createElement('li');
    liBox.innerText = `我是第${i}个LI`;
    frag.appendChild(liBox);
}
item.appendChild(frag); 

可以利用字符串拼接:项目中,有一个文档碎片类似的方式,也是把要创建的LI事先存储好,最后统一放到页面中渲染(字符串拼接)

let str = ``;
for (let i = 1; i <= 5; i++) {
    str += `<li>我是第${i}个LI</li>`;
}
item.innerHTML = str; 
  1. 除了transform之外还有什么会导致独立图层的生成
  2. ES6用过哪些

    8. 关于promise promise.all promise.race实现

    图片说明
    • promise实现:
  1. 调用 then 方法,将想要在 Promise 异步操作成功时执行的 onFulfilled 放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;

  2. 创建 Promise 实例时传入的函数会被赋予一个函数类型的参数,即 resolve,它接收一个参数 value,代表异步操作返回的结果,当异步操作执行成功后,会调用resolve方法,这时候其实真正执行的操作是将 callbacks 队列中的回调一一执行。

  3. then 方法应该能够链式调用。实现也简单,只需要在 then 中 return this 即可。

  4. 加入一些处理,保证在 resolve 执行之前,then 方法已经注册完所有的回调。在 resolve 中增加定时器,通过 setTimeout 机制,将 resolve 中执行回调的逻辑放置到JS任务队列末尾,以保证在 resolve 执行时,then方法的 onFulfilled 已经注册完成。

首先 new Promise 时,传给 Promise 的函数设置定时器模拟异步的场景,接着调用 Promise 对象的 then 方法注册异步操作完成后的 onFulfilled,最后当异步操作完成时,调用 resolve(value), 执行 then 方法注册的 onFulfilled。

//极简的实现+链式调用
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
        return this;//3
    }
 _resolve(value) {
        setTimeout(() => {//4
            this.callbacks.forEach(fn => fn(value));
        });
    }
}

//Promise应用
let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
}).then(tip => {
    console.log('then1', tip);
}).then(tip => {
    console.log('then2', tip);
});

加入状态后

//极简的实现+链式调用+延迟机制+状态
class Promise {
    callbacks = [];
    state = 'pending';//增加状态
    value = null;//保存结果
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        if (this.state === 'pending') {//在resolve之前,跟之前逻辑一样,添加到callbacks中
            this.callbacks.push(onFulfilled);
        } else {//在resolve之后,直接执行回调,返回结果了
            onFulfilled(this.value);
        }
        return this;
    }
    _resolve(value) {
        this.state = 'fulfilled';//改变状态
        this.value = value;//保存结果
        this.callbacks.forEach(fn => fn(value));
    }
}
  • promise.all()实现

Promise.all() 它接收一个promise对象组成的数组作为参数,并返回一个新的promise对象。

当数组中所有的对象都resolve时,新对象状态变为fulfilled,所有对象的resolve的value依次添加组成一个新的数组,并以新的数组作为新对象resolve的value。
当数组中有一个对象reject时,新对象状态变为rejected,并以当前对象reject的reason作为新对象reject的reason。

  Promise.all = function(promises){
          if(!Array.isArray(promises)){
            throw new TypeError('You must pass array')
          }

          return new Promise(function(resolve,reject){
            var result = [],
                count = promises.length;

            function resolver(value){
                resolveAll(value)
            }

            function rejecter(reason){
              reject(reason)
            }

            function resolveAll(value){
              result.push(value)

              if(--count ==0){
                resolve(result)
              }
            }

            for(var i=0;i<promises.length;i++){
              promises[i].then(resolver,rejecter)
            }
          })
        }

打开页面时有多个请求,如何用promise优雅的实现,(兼容型promise)
Vue双向数据绑定
Vue父子组件通信
事件流(先冒泡后捕获)
大量图片优化(懒加载)
base64压缩图片时,如果图片大小1k,没有优化效果
箭头函数特点
git使用 10人工作时,如何合并分支

2

一面

js数据类型,简单与复杂的数据类型区别
跨域实现方式 ,具体每种的实现方式
http2与http1区别,刚说到多路复用就让说一下具体实现,什么是多路复用
promise底层实现原理
除了cookie, webStorage还有哪些本地存储的方式
vue路由如何实现
vue子组件如何修改父组件的值
项目中实现了哪些公共组件

8

二面

1. 二分查找算法(比较递归和非递归算法的差异)

二分查找法是对一组有序的数字中进行查找,传递相应的数据,进行比较查找到与原数据相同的数据,查找到了返回对应的数组下标,没有找到返回-1;

想要应用二分查找法,这“一堆数”必须有一下特征:(1)存储在数组中 (2) 有序排列
所以如果是用链表存储的,就无法在其上应用二分查找法了。

二分查找法的递归JS实现如下:

function bsearch(array,low,high,target)
{
    if (low > high) return -1;
    var mid = Math.floor((low + high)/2);
    if (array[mid]> target){
        return  bsearch(array, low, mid -1, target);
    } else if (array[mid]< target){
        return  bsearch(array, mid+1, high, target);
    }ese{return mid;}

}

二分查找法也可以不用递归实现,而且它的非递归实现甚至可以不用栈

function bsearchWithoutRecursion(array,low,high,target)
{
    while(low <= high)
    {
        var mid = Math.floor((low + high)/2);// start + ((end - start) >> 1)
        if (array[mid] > target){
            high = mid - 1;
        }else if (array[mid] < target){
            low = mid + 1;
        }else{
            return mid;
        }
    }
    return -1;
}
对比差异:
递归的二分查找方***调用一系列函数,并且会涉及到返回中的参数传递和返回值的额外开销,在一些嵌套层数深的算法中,递归会力不从心,空间上会以内存崩溃而告终。
但是递归也是有优势的,递归的代码的可读性强,程序员也容易进行编程。

非递归
非递归的二分查找一般就需要使用循环了,循环一般情况下会难以理解,而且要编写出循环实现功能也更加困难,不利于代码的可读性和代码的编写。但是循环的效率高,执行次数只会根据循环次数的增加而增加,不会有额外的开销。

2. js 如何实现队列(push放,shift取)(unshift放,pop取)

3. css动画实现原理

JavaScript 中可以通过定时器 setTimeout 来实现,css3 可以使用 transition 和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的 API,即 requestAnimationFrame(rAF),顾名思义就是 “请求动画帧”

因此,当你对着电脑屏幕什么也不做的情况下,显示器也会以每秒60次的频率正在不断的更新屏幕上的图像。为什么你感觉不到这个变化? 那是因为人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了,这中间只间隔了16.7ms(1000/60≈16.7), 所以会让你误以为屏幕上的图像是静止不动的。我们看到图像正在以每秒 60 次的频率绘制,由于频率很高,所以你感觉不到它在绘制。而 动画本质就是要让人眼看到图像被绘制而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。

setTimeout 其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。但我们会发现,利用 seTimeout 实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因:

  • setTimeout 的执行时间并不是确定的。在JavaScript中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,所以 setTimeout 的实际执行时机一般要比其设定的时间晚一些。
  • 刷新频率受 屏幕分辨率 和 屏幕尺寸 的影响,不同设备的屏幕绘制频率可能会不同,而 setTimeout 只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致 setTimeout 的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象。

与 setTimeout 相比,rAF 最大的优势是 由系统来决定回调函数的执行时机。

具体一点讲就是,系统每次绘制之前会主动调用 rAF 中的回调函数。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

var progress = 0;

//回调函数
function render() {   
       progress += 1;//修改图像的位置     
       if (progress < 100) {           
               //在动画没有结束前,递归渲染           
               window.requestAnimationFrame(render);    
        }
} 
//第一帧渲染
window.requestAnimationFrame(render);

由于 rAF 目前还存在兼容性问题,而且不同的浏览器还需要带不同的前缀。因此需要通过优雅降级的方式对 rAF 进行封装,优先使用高级特性,然后再根据不同浏览器的情况进行回退,直止只能使用 setTimeout 的情况,因此可以这么写:

window.requestAnimFrame = (function(){  
          return  window.requestAnimationFrame 
                   || window.webkitRequestAnimationFrame 
                   || window.mozRequestAnimationFrame    
                   || function( callback ){            
                          window.setTimeout(callback, 1000 / 60);          
                       };
})();

4. 闭包作用

详见俺的博文:https://juejin.im/post/6854573208243273742#heading-31

github:https://github.com/YvetteLau/Step-By-Step/issues/24

js采用的是词法作用域,也就是函数可以访问的变量在函数定义时写在哪里就确定了和函数被调用的位置无关。闭包就是函数不在定义的词法作用域内被调用,但是仍然可以访问词法作用域中定义的变量。

闭包可以被用在三个地方:

  • 封装私有变量
    // name 只能通过getName接口来访问
    function Person(name) {
      this.getName = function() {
          return name;
      }
    }
  • 模拟模块(让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。)实现公有变量,比如说函数累加器
    function module() {
      let inner = 1;
      let increaseInner = function() {
          inner++;
      }
      let decreaseInner = function() {
          inner--;
      }
      let getInner = function() {
          return inner;
      }
      return {
          increaseInner,
          decreaseInner,
          getInner
      }
    }
    let api = module();
    console.log(api.getInner());
    api.increaseInner();
    console.log(api.getInner());
  • 用在块作用域上,做缓存
    for (var i = 0; i < 5; i++) {
      (function(i) {
          setTimeout(function() {
              console.log(i)
          }, 1000);
      })(i);
    }

5. 造成内存泄漏其他方式

内存泄漏可以定义为程序不再使用或不需要的一块内存,但是由于某种原因没有被释放仍然被不必要的占有。

内存生命周期:

  • 分配你所需要的内存、使用分配到的内存(读、写)、不需要时将其释放\归还

垃圾回收算法:

  • 垃圾回收机制通过定期的检查哪些先前分配的内存“程序的其他部分仍然可以访问到的内存”(√)。
  1. 引用计数

如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

var o = { 
    a: {
        b:2
    }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,
另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集

var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”的原始引用o被o2替换了

var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 最初的对象现在已经是零引用了
       // 他可以被垃圾回收了
       // 然而它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
       // 它可以被垃圾回收了

缺陷:在循环的情况下,引用计数算法存在很大的局限性

  1. 标记清除
    1. 垃圾回收器创建了一个“roots”列表。Roots通常是代码中全局变量的引用。JavaScript中,“window”对象是一个全局变量,被当作root。window对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);
    2. 所有的 roots被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从root开始的所有对象如果是可达的,它就不被当作垃圾。
    3. 所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了(本质上: 可达内存被标记,其余的被当作垃圾回收。)
  • 常见的四种内存泄漏:
  1. 全局变量:在非严格模式下当引用未声明的变量时,会在全局对象中创建一个新变量。在浏览器中,全局对象将是window

    function foo(arg){ 
     bar =“some text”; // bar将泄漏到全局.
    }

    全局变量是根据定义无法被垃圾回收机制收集.需要特别注意用于临时存储和处理大量信息的全局变量。如果必须使用全局变量来存储数据,请确保将其指定为null或在完成后重新分配它。 解决办法: 严格模式

  2. 被遗忘的定时器和回调函数::与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。

    var someResource = getData();
    setInterval(function() {
     var node = document.getElementById('Node');
     if(node) {
         node.innerHTML = JSON.stringify(someResource));
         // 定时器也没有清除
     }
     // node、someResource 存储了大量数据 无法回收
    }, 1000);

    解决方法: 在定时器完成工作的时候,手动清除定时器

  3. DOM引用:保留了DOM节点的引用,导致GC没有回收

    var refA = document.getElementById('refA');
    document.body.removeChild(refA); // dom删除了
    console.log(refA, "refA");  // 但是还存在引用

    解决办法:refA = null;

set get;set和get的缺点

6. http响应头??

7. 原型链作用(继承)

JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。
因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型.
图片说明
这图里可以再加一条person1.constructor = Person;(person1是构造函数Person的实例对象,但是person1自身没有constructor属性,该属性其实是读取原型链上面的Person.prototype.constructor属性。)

class Person {
  constructor(name) {
    this.name = name;
  }
  printName(){
    console.log(this.name);
  }
  common(){
    console.log("common method");
  }
}
class Student extends Person {
  constructor(name, score) {
    super(name);
    this.score = score;
  }
  printScore(){
    console.log(this.name);
  }
}

function Person(name){
  this.name = name;
  this.printName = function(){
    console.log(this.name);
  }
}
Person.prototype.common = function(){
  console.log("common method");
}
function Student(name, score){
  Person.call(this.name);
  this.score = score;
  this.printScore = function(){
    console.log(this.score);
  }
}
Student.prototype = Object.create(Person.prototype);
let person = new Person('小紫',80);
let stu = new Student('小红',100);
console.log(stu.printName===person.printName);//false
console.log(stu.commonMethods===person.commonMethods);//true

8. css选择器优先级,权重

优先级就是分配给指定的 CSS 声明的一个权重,它由 匹配的选择器中的 每一种选择器类型的 数值 决定。

而当优先级与多个 CSS 声明中任意一个声明的优先级相等的时候,CSS 中最后的那个声明将会被应用到元素上。

!important > 内联样式 > ID 选择器 > 类选择器 = 属性选择器 = 伪类选择器 > 标签选择器 = 伪元素选择器 > 通配符选择器 > 继承 > 浏览器默认属性

ID 选择器:如 #id{}
类选择器:如 .class{}
属性选择器:如 a[href="segmentfault.com"]{}
伪类选择器:如 :hover{}
标签选择器:如 span{}
伪元素选择器:如 ::before{}
通配符选择器:如 *{}

二面

1. split()、slice()、splice()、reduce()、join(),参数,区别

  • split()方法用于把一个字符串以特定字符分割成字符串数组并返回。第一个参数是指定的分割字符;字符存在于字符串中,且分割后不存在于字符串数组中。第二个参数是指定分割之后字符串数组的长度。(可选)
  • slice()方法从已有的数组中返回特定的元素。第一个参数是开始索引(从0开始)第二个参数是结束索引的下一个(可选,默认到最后一个)
  • splice()方法从数组中添加/删除元素,并返回被删除的元素。第一个参数代表开始删除的位置,第二个参数代表删除个数,第三个参数为添加的元素(元素添加在开始删除的位置之前)。
  • array.reduce(function(total, currentValue, currentIndex, arr), initialValue):total 必需。初始值, 或者计算结束后的返回值。currentValue 必需。当前元素currentIndex 可选。当前元素的索引arr 可选。当前元素所属的数组对象。initialValue 可选。传递给函数的初始值
  • join():join()方法用于把数组中的所有元素通过指定的分隔符分隔之后,再放入一个字符串
    一个居中的弹出框, 右上角有个溢出一半的关闭按钮,css怎么实现

2. 雪碧图的图片位置,css属性怎么取

主要用在小图标显示上。

 <div class="ps_demo_wrap">
        <div class="demo_icon weibo_icon"><div class="black_bg"></div></div>
        <div class="demo_icon qq_icon"><div class="black_bg"></div></div>
        <div class="demo_icon douban_icon"><div class="black_bg"></div></div>
        <div class="demo_icon renren_icon"><div class="black_bg"></div></div>
      </div>
 .ps_demo_wrap .demo_icon{
    position: relative;
    float:left;
    margin:13px 0px 0px 10px;
    cursor: pointer;
    width:54px;
    height:54px;
}
.ps_demo_wrap .weibo_icon{ background-image:url("../scss/images/icon_weibo.png"); }
.ps_demo_wrap .qq_icon{ background-image:url("../scss/images/icon_qq.png"); }
.ps_demo_wrap .douban_icon{ background-image:url("../scss/images/icon_douban.png"); }
.ps_demo_wrap .renren_icon{ background-image:url("../scss/images/icon_renren.png"); }

3. 性能优化平时怎么做

https://juejin.im/post/6844904055790125064

vue项目的目录

4. 状态码:200 500(服务器内部错误) 404(服务器无法回应且不知原因。通常是因为用户所访问的对应网页已被删除、移动或从未存在) 304(Not Modified 服务端已经执行了GET,但文件未变化。大家都知道服务器可以设置缓存机制,这个功能是为了提高网站的访问速度,当你发出一个GET请求的时候服务器会从缓存中调用你要访问的内容,这个时候服务器就可以判断这个页面是不是更新过了,如果未更新过那么他会给你返回一个304状态码。)

5. 缓存有关的http头

  • 浏览器中使用缓存的过程:

    浏览器发起请求
  1. 检查是否有缓存
  2. 有Pragma字段 no-cache 强制请求 新资源
  3. 有缓存并且没有Pragma,先判断缓存是否过期(Cache-Control 优先于 Expires),没有过期就使用缓存
  4. 缓存有效时间过期了,查看是否使用了Eatg 和 Last-Modified 头部
  5. 发送 If-none-Match 和 If-Modified-Since 去验证是否缓存还能继续使用(可能缓存到期了,但是服务端没有修改,而资源又比较大,6. 通过校验可以减少网络传输)
  6. 资源没有修改就使用缓存
  7. 资源修改了就返回新的资源
    缓存头及注意点:
    Expires
    

一般使用Cache-Control和Expires共存,主要是为了兼容http1.0
Expires返回的是服务器时间,需要考虑服务器与客户端的时间同步(时区等)
过期之后重新返回的响应中要加入新的Expires

Cache-Control

max-age 过期时间是一个时间段,从接受到这个响应之后开始生效,过期之后任然可以使用这个时间段
max-age 和 Expires 共存的时候使用max-age
no-cache 表示可以缓存,但是在使用缓存之前需要向服务器验证
no-store 不缓存

Last-Modified

用来标识服务端最后一次修改资源的时间
配合If-Modified-Since使用,检查缓存与服务端是否一致

Etag

服务端资源的唯一标识,看作一种数字签名
当资源没有发送变化的情况下,Etag计算值不发生变化
当有多个服务端的情况下,需要保证不同服务器上的Etag计算方式一致
需要额外的计算Etag的资源

全部评论

相关推荐

像好涩一样好学:这公司我也拿过 基本明确周六加班 工资还凑活 另外下次镜头往上点儿
点赞 评论 收藏
分享
点赞 评论 收藏
分享
点赞 1 评论
分享
牛客网
牛客企业服务