前端面试题汇总2

289 阅读31分钟

请谈谈你对HTML5和CSS3新特性的理解。你在实际项目中如何运用这些新特性?

html5出现了很多语义化的标签和API,比如nav, header,section,main,canvas等,通过语义化标签可以提高代码的可阅读性和可维护性,有利于SEO搜索,同时也有利于一些阅读器进行语义化的分析;css3出现了圆角,阴影,动画,过渡,弹性布局等,有利于加快开发效率,实现复杂的样式更加简单化;

请解释一下JavaScript中的闭包(closure),并给出一个闭包的应用场景

闭包就是一个函数内部返回了另一个函数,并且另一个函数中使用了这个函数内部的变量;闭包的缺点其内部访问的变量不会被回收一直占用内存,因此会造成内存泄漏;闭包应用在柯里化函数中,防抖节流;

请简述原型和原型链的概念,以及它在JavaScript中的应用

原型:js中的原型主要用来实现继承的,js中每个对象都有自己的__ptoto__属性,这个属性指向它的构造函数的prototype对象,此对象就是原型;当访问一个对象上的属性的时候首先会从对象本身进行查找,如果没有找到就会从它的原型上查找。如果也没有找到就会从它的原型的__proto__指向的原型上查找,直到找到顶层的null为止,这个一层层网上查找的链就是原型链;可以通过原型或原型链实现一个对象公共方法或属性的定义;

在你的项目中,你是如何处理跨域请求的?

引起跨域的原因是因为浏览器同源策略的限制,必须协议、域名和端口都相同才是同源,对于非同源的请求就会出现跨域问题,在本地开发中通过本地服务器代理解决跨域,对于生产通过设置跨域资源共享,或者通过设置jsonp,但是jsonp只能发送get请求,也可以通过代理服务器进行转发;

请谈谈你对前端性能优化的理解,以及在项目中采取了哪些措施来提升性能?

前端提升性能有两个方面,一是文件体积大小方面(网络传输),二是渲染方面;

  • 体积大小方面:
  1. 压缩图片,压缩文件成zip格式;
  2. 合并小图标使用雪碧图,减少http请求
  3. 通过打包工具进行tree-shaking,去除没有使用的代码;
  4. 抽离公共的代码
  5. 单独抽离样式
  6. 对于较大的文件可以进行拆分成多个文件,可以使用webpack的一个插件工具进行打包分析,合理拆分;
  7. 使用CDN加载静态资源,开启http缓存;
  • 渲染方面:
  1. 使用服务端渲染
  2. 使用骨架屏
  3. 使用路由和组件懒加载的方式
  4. 使用keep-alive缓存组件状态;
  5. 采用预加载预渲染
  6. 样式放在顶部,js放在底部,防止js脚本阻塞页面的加载和渲染;
  7. 开发方面:减少频繁操作dom元素,使用文档碎片批量操作,使用class批量操作样式;

请解释一下Event Loop事件循环机制以及它在JavaScript中的作用

js中事件循环机制是用来管理异步操作的,由于js是单线程的,它只有一个执行栈,当遇到异步操作的时候比如网络请求,定时器等,就会把这些异步回调添加到消息队列中并且继续执行同步代码,当执行栈中的同步代码执行完毕之后,事件循环机制开始工作,消息队列又分为宏任务队列和微任务队列,执行栈中同步代码执行完毕之后首先执行微任务队列中的回调,当微任务队列执行完毕之后,就执行宏任务队列中的回调,如果此时又有微任务就会再次推入到微任务队列中,当这个宏任务执行完毕就再次执行微任务队列中的回调;这样的循环过程就是事件循环机制;

常见的浏览器内核

  1. trident: IE浏览器的内核,兼容性差,有很多bug和安全性问题;
  2. gecko: 火狐浏览器的内核,功能强大丰富,支持扩展,但是消耗资源,占用大量内存;
  3. webkit: safari浏览器的内核,性能上有提升,但是容错性不高;
  4. Blink: 谷歌浏览器内核,性能好,容错性比较高;

常见的请求和响应头?

请求头

  1. Accept: 浏览器能够处理的内容类型;
  2. Accept-charset: 浏览器能够显示的字符集;
  3. Accept-Encoding: 浏览器能够处理的压缩方式
  4. Accept-Language: 浏览器当前的语言
  5. host: 当前请求所在的域名
  6. referer: 当前请求所在的url

响应头

  1. cache-control: 缓存信息
  2. content-type: 响应的内容是什么类型
    • multipart/form-data: post请求,用于表单上传文件
    • application/json: 返回的内容是json字符串
  3. connection: 连接类型

DOCTYPE(⽂档类型) 的作⽤

DOCTYPE是html5中一种标准的文档类型声明。它的主要作用是告诉浏览器用什么样的类型进行解析当前文档。浏览器中有两种渲染模式,一种是标准模式,一种是怪异模式;

link和@import的区别?

  1. link是Xhtml的标签,除了可以加载css还可以加载其他类型;@import只能加载css
  2. link在页面加载的时候同步加载css,@import只能等到网页全部载入之后加载
  3. link可以通过js动态的创建,而@import不能

说一下vue跳转路由组件怎样销毁的

在vue中,当从一个路由跳转到另一个路由的时候在没有设置keep-alive的情况下,会销毁上个路由对应的组件和创建下个路由对应的组件;整个销毁过程,执行beforeDestory钩子,从父级上删除此组件,删除当前组件的依赖,删除其他数据对当前组件的依赖,清空当前组件的虚拟节点,移除响应式数据,执行destoryed钩子,关闭事件监听,销毁自身实例。所以在destoryed中可以使用事件和访问自身实例,但是不能访问响应式属性;

从一个路由页面跳转到另一个路由页面,我在前个路由页面有一个请求还没有结束,应该怎么办?

在请求前的钩子中存储正在请求的路径和cancelToken,在响应的钩子中根据对应的url从正在请求的数组中删除对应的数据,在离开当前路由的时候遍历正在请求的数组执行cancelToken取消对应的请求即可;

css文件在加载的过程中出现了阻塞会怎样?加载html文档10ms、加载css10s会出现什么现象?

css的加载不会阻塞js的加载和dom的解析,但是会阻塞页面的渲染,因为渲染需要Css样式表和dom树一起组合成render树,因此会出现白屏现象;js的加载和执行会阻塞dom解析;css的加载会阻塞js的执行会阻塞页面的渲染;DOMContentLoad的执行会等待css加载完毕,因为DOMContentLoad的执行会等待页面中所有引入的js加载执行完毕,而css的加载又会阻塞js的执行,所以DomContentLoad的执行需要等到Css加载完成;和此页面无关的css的引入,采用异步引入的方式;

说一下http1.1的keep-alive?

keep-alive是一种持久化链接,同一个tcp链接上进行多个请求。无需每次都重新创建和断开Tcp链接,这样节省了tcp创建的时间,减少请求时间;

http1.1中的keep-alive与http2.0中的多路复用有什么区别?

keep-alive: 长连接,在同一个tcp连接下可以进行多次请求,每个请求都是按照顺序执行的,具有对头阻塞问题,但是减少了tcp建立和关闭的开销;
多路复用:在同一个tcp链接下可以进行多个请求和响应,每条请求和响应都是由多个帧组成,每帧中都有一个标识表示属于哪条数据流,因此多路复用不需要等待之前的请求完成再进行下一条请求;解决了http1.1中队头阻塞问题;

webpack 如何实现动态加载?

在webpack中的动态加载是靠动态导入语法import和代码分割实现的,webpack会把动态导入的模块拆分成独立的块,每个独立的块都会被打包成一个单独的文件;这样就可以实现按需加载;

setTimeout和setInterval最小执行的时间是多少?

setTimeout : 最小执行时间是 4ms setInterval : 最小执行时间是 10ms

Promise手写题:控制绿黄信号灯循环

function green () {
    console.log('green')
}
function red () {
    console.log('red')
}
function yellow () {
    console.log('yellow')
}
// 通过promise去执行
function light (cb, timer) {
    return new Promise(resolve => {
        cb()
        setTimeout(() => {
            resolve()
        }, timer)
    })
}
function go () {
    Promise.resolve().then(() => {
        return light(green, 3000)
    }).then(() => {
        return light(yellow, 3000)
    }).then(() => {
        return light(red, 3000)
    }).finally(() => {
        return go()
    })
}
go()

实现数组扁平化+去重+排序

// 扁平化
const arr = [1,3,[3,2,[8,6]],[33,20]]
// 1. 通过正则
// 2. 通过递归
function flat (arr) {
    const newArr = []
    function fn (arr) {
        for (let i = 0; i < arr.length; i++) {
            if (Array.isArray(arr[i])) {
                fn(arr[i])
            } else {
                newArr.push(arr[i])
            }
        }
    }
    fn(arr)
    return newArr
}
// 去重
// 1. Set
const newArr = [...new Set(flat(arr))]

// 2. 遍历数组 通过indexOf
const arr2 = []
const result = flat(arr)
result.forEach(item => {
    if (arr2.indexOf(item) < 0) {
        arr2.push(item)
    }
})
// 3. 通过遍历数组 设置对象的key

// 排序
// 1. sort排序
arr2.sort((a,b) => a-b)
// 2. 冒泡
function bubbling(arr){
    if (!arr) {
        return arr
    }
    for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < arr.length - i; j++) {
            if (arr[j] > arr[j+1]) {
                const temp = arr[j]
                arr[j] = arr[j+1]
                arr[j+1] = temp
            }
        }
    }
    return arr
}
// 3. 快排
function quickSort(arr){
    if (!arr || !arr.length || arr.length <= 1) {
        return arr
    }
    // 去一个中间值
    const value = arr.splice(Math.floor(arr.length/2),1)[0]
    const leftArr = []
    const rightArr =[]
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] <= value) {
            leftArr.push(arr[i])
        } else {
            rightArr.push(arr[i])
        }
    }
    return [...quickSort(leftArr), value, ...quickSort(rightArr)]
}

组件二次封装考虑哪些东西

  1. 易用性
  2. 复用性
  3. 可扩展
  4. 易于维护

前端性能问题从哪几方面去解决

网络方面

  1. 减少请求数量和请求大小,可以通过合并文件或压缩资源的方式来减少请求和减少大小;
  2. 使用cdn加载静态资源,提高响应速度
  3. 预加载资源或预解析域名
  4. 使用多域名,因为浏览器在同一个域名下可以同步发送几条请求,多个域名可以同步发送更多的请求。
  5. 设置缓存资源
  6. 压缩文件

缓存方面

  1. pwa
  2. vue中可以设置keep-alive

代码方面

  1. 删除无用的代码;
  2. 提取公共逻辑;
  3. 提取公共样式;
  4. 单独抽离样式
  5. 减少操作dom,批量操作样式和dom;

页面渲染方面

  1. js脚本放在底部引入,可以设置defer或async异步加载;
  2. 本页面相关的Css样式文件放在head中;
  3. 动画相关的放在单独的图层,避免每次引入全部的回流;
  4. 压缩图片,使用webp格式;
  5. 非当前页展示的图片可以通过懒加载的方式延迟加载;
  6. 非当面页面的组件或者通过事件操作才展示的组件,可以通过异步加载的方式配合分包打包成单独的文件,减少包的大小;

git回退或git(reset 和 git revert)区别?

  1. git回退有两种方式,分别为git reset和git revert;
  2. git reset: 回滚到指定的提交版本,此提交之后的提交都会被删除,相当于回到这个位置;(回滚到指定版本)
  3. git revert: 创建一个新的版本,并且删除了某个指定的版本中的内容,某个版本之后提交的内容还是存在的;比如提交了四次代码,想要删除第二次提交的代码但是保留第三次和第四次的代码就可以使用该命令**(删除指定版本)**

align-content 和justify-content的区别

  1. justify-content: 主轴上子元素的排列方式;
  2. align-content: 次轴上多行子元素的排列方式,必须是多行子元素;一行子元素没有效果;align-items对一行子元素的排列有效;

把一个json转成字符串的形式

const r = {
  x: {
    x1: 'aa',
    x2: [1, 2],
    x3: {
      m: {
        n: 123
      }
    }
  },
  y: [{
    p: 1,
    q: 'aa'
  }],
  z: true
}

/*
const obj = {
  x: "object",
  x.x1: "string",
  x.x2: "array",
  x.x3: "object",
  x.x3.m: "object",
  x.x3.m.n: "number",
  y: "array",
  y.p: "number",
  y.q: "string",
  z: "boolean"
}
*/

function a(param) {
  const obj = {}
  const fn = (param, k) => {
    k = k ? k + '.' : k
    if (typeof param !== 'object' || param === null) {
      return
    }
    for (let key in param) {
      if (param.hasOwnProperty(key)) {
        const value = param[key]
        if (Array.isArray(value)) {
          obj[k + key] = 'array'
          value.forEach(item => {
            fn(item, k + key)
          })
        } else if (typeof value === 'object' && value !== null) {
          obj[k + key] = 'object'
          fn(value, k + key)
        } else {
          obj[k + key] = typeof value
        }
      }
    }
  }
  fn(param, '')
  return obj
}

实现一个函数 find(obj,str),满足:

// 如var obj = {a:{b:{c:1}}}
// find(obj,'a.b.c') //1
// find(obj, 'a.d.c') //undefned

function find (obj,str) {
    const arr = str.split('.')
    for (let i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            return
        }
        obj = obj[arr[i]]
    }
    return obj
}
var obj = {a:{b:{c:1}}}
find(obj,'a.b.c') // 1

岛屿的最大面积(力扣695)

给你一个大小为`m x n`二进制矩阵`grid`。
**岛屿** 是由一些相邻的 `1` (代表土地)构成的组合,
这里的「相邻」要求两个`1`必须在**水平或者竖直的四个
方向上**相邻。假设 `grid`的四个边缘都被`0`(代表水) ) 包围着。
岛屿的面积是岛屿的`1`数量。
计算并返回`grid`中最大的岛屿。如果没有岛屿,则返回面积为`0`

image.png

/*
 其实就是计算二维数组中,上下左右连续为1的数字,它们的个数;
 遍历二维数组,如果当前项为1,那么就递归判断它的上下左右是不是1,是1就相加,否则就加0
*/
function maxAreaOfIsland (grid) {
    let max= 0
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[0].length; j++) {
            // 如果是1就判断它的上下左右
            if (grid[i][j] === 1) {
                max = Math.max(max, isLand(grid, i, j))
            }
        }
    }
    function isLand (grid, i, j) {
        // 如果当前项为1 并且坐标在范围内就递归
        if (i>=0 && i<grid.length && j >=0 && j< grid[0].length && grid[i][j] === 1) {
        grid[i][j] = 0 // 记录过就标记为0,防止重复查找导致内存溢出
            return 1+isLand(grid, i+1,j)+isLand(grid, i,j+1)+isLand(grid, i-1,j)+isLand(grid, i,j-1)
        } else {
            return 0
        }
    }
}

flex布局,常用的属性有哪些?

用在父元素上

  1. flex-direction: 指定主轴的方向;row水平,colunm垂直;
  2. justify-content: 指定子元素在主轴上排列的方式;flex-start,flex-end,center,space-between;
  3. align-items: 指定子元素在主轴的垂直方向上的排列方式;stretch,center,flex-start,flex-end;
  4. align-content: 和align-items一样,只不过它用于多行的子元素;
  5. flex-wrap: 是否换行;

用在子元素上

  1. order: 子元素的排列顺序;值越大越靠后;
  2. flex-shrink: 子元素的按比例收缩;
  3. flex-grow: 子元素的按比例放大;
  4. align-self: 元素在交叉轴的排列方式,覆盖algin-items;

flex实现三列的布局,需要用到哪些属性?

flex: 1;指定中间元素沾满剩余空间;

flex:1 是什么意思?

flex:是flex-grow flex-shrink flex-basis三个属性的组合; flex:1表示flex-grow为1,flex-shrink为1,flex-basis为数字加单位;表示平均分配剩余空间;

将对象数组转换为树形结构

// list: 数组对象
// id: 每条数据的id
// pid: 每条数据的父节点对应字段
// pid:null 没有父节点的数据
var list = [
  { id: '04', pid: '03' },
  { id: '01', pid: null },
  { id: '02', pid: null },
  { id: '03', pid: '01' },
  { id: '05', pid: '01' },
  { id: '06', pid: '03' },
  { id: '07', pid: '02' },
  { id: '09', pid: '02' },
  { id: '10', pid: '07' },
]
// 转成
[
  {
    "id": "01",
    "pid": null,
    "children": [
    {
      "id": "03",
      "pid": "01",
      "children": [
      {
        "id": "04",
        "pid": "03"
      }, {
        "id": "06",
        "pid": "03"
      }, {
        "id": "04",
        "pid": "03"
      }, {
        "id": "06",
        "pid": "03"
      }]
    }, {
      "id": "05",
      "pid": "01"
    }, {
      "id": "03",
      "pid": "01",
      "children": [{
        "id": "04",
        "pid": "03"
      }, {
        "id": "06",
        "pid": "03"
      }, {
        "id": "04",
        "pid": "03"
      }, {
        "id": "06",
        "pid": "03"
      }]
    }, {
      "id": "05",
      "pid": "01"
    }]
  }, {
    "id": "02",
    "pid": null,
    "children": [{
      "id": "07",
      "pid": "02",
      "children": [{
        "id": "10",
        "pid": "07"
      }, {
        "id": "10",
        "pid": "07"
      }]
    }, {
      "id": "09",
      "pid": "02"
    }, {
      "id": "07",
      "pid": "02",
      "children": [{
        "id": "10",
        "pid": "07"
      }, {
        "id": "10",
        "pid": "07"
      }]
    }, {
      "id": "09",
      "pid": "02"
    }]
  }
]

/**
 * 先通过每项的id映射到一个对象obj中,
 * 方便通过pid找到父级
 * 如果有pid表示有父级,那么直接在obj中
 * 找到父级,给父级添加children属性
 * 如果没有pid表示没有父级,直接添加到结果数组中
 * @returns 
 */
var fn = () => {
  const obj = {}
  const result = []
  // 把id映射到一个对象中
  list.forEach((item) => {
    obj[item.id] = item
  })
  list.forEach((item) => {
    // 有父级,就从obj中找到父级通过id,创建子级
    if (item.pid) {
      (obj[item.pid].children || (obj[item.pid].children = [])).push(item)
    } else{ // 没有就添加
      result.push(obj[item.id])
    }
  })
  return result
}
fn()

说说BEM规范

BEM规范属于css命名规范;B(block):块级的意思,表示整块元素;E(element):元素的意思,表示块级下的子元素;M(Modifier):修饰符;B和E之间使用__双下划线连接,E和M之间使用--连接;

<div class="header">
    <div class="header__body">
        <button class="header__button--primary"></button>
    </div>
</div>

localStorage和sessionStorage,同url的两个页面会共享localStorage和sessionStorage吗?

同域下多个页面是会共享localStorage的;但是对于sessionStorage不会共享,只会复制,也就是说在一个页面跳转到另一个页面的时候,会复制上个页面的sessionStorage的数据到下个页面,下个页面能够获取到上个页面的sessionStorage数据,但是不会共享,上个页面修改了sessionStorage的数据,下个页面获取的还是修改之间的数据;如果是通过手动输入地址的方式打开新页面是不会拿到其他页面的sessionStorage数据的;

前端安全相关有哪些,如何预防xss?

XSS(跨站脚本攻击)、CSRF(跨站请求伪造);

XSS

页面被注入了恶意代码,窃取用户信息或做一些恶意的操作;

类型
  1. 存储型
    恶意代码被存储到目标网站的数据库中;用户在访问目标网站的时候,从数据库中返回恶意代码,浏览器解析并且执行了恶意代码,攻击者可以通过恶意代码获取到用户的信息,从而通过用户信息调用目标网站的接口执行一些恶意操作;这种攻击通常出现在评论功能,留言,私信等;
  2. 反射型
    攻击者将恶意代码拼接在目标网站的链接后面,用户打开链接会访问服务器获取资源,服务端拿到恶意代码之后拼接到html中返回,浏览器解析并且执行了恶意代码,攻击者可以通过恶意代码获取到用户的信息,伪造用户做一些恶意操作;这种攻击通常出现在带有参数的链接中,并且诱惑用户打开,比如搜索,跳转;post也可能出现这种攻击不过比较困难;
  3. DOM型
    攻击者将恶意代码拼接在目标网站的链接后面,用户打开链接之后,页面中的js获取到链接中的恶意代码并且执行了;DOM型和反射型的区别在于,DOM型是代码中的js主动获取到恶意代码并且执行了恶意代码,而反射型是服务端拼在html中返回执行的,所以dom型属于纯前端的安全漏洞,而反射型和存储型是属于服务端的安全漏洞;
预防

XSS攻击有两大要素:攻击者提交了恶意代码;浏览器执行了恶意代码;针对这两种要素进行处理;

  1. 前后端对用户输入的内容进行转义过滤;
  2. 对标签的src和href的值进行校验是以http,或https开头,防止以javascript:开头注入恶意代码;
  3. 设置cookie为只读模式;
  4. 限制输入内容的格式,长度等;
  5. 开启验证等
  6. 开启SCP
  7. 主动检查

以上预防只是笼统的措施,具体要针对不同的输入和功能做具体的相应的转移和过滤;

XSS 防范是后端 RD 的责任,后端 RD 应该在所有用户提交数据的接口,对敏感字符进行转义,才能进行下一步操作。

这句话不完全对,因为XSS攻击中的DOM型是纯前端的漏洞,需要前端采取相应的措施防御;

所有要插入到页面上的数据,都要通过一个敏感字符过滤函数的转义,过滤掉通用的敏感字符后,就可以插入到页面中。

这句话也不完全对,过滤通用的敏感字符,只能防御部分攻击,不同的插入地方需要不同的转义,html的插入,还是dom属性的插入,还是href和src连接的插入,html的插入需要过滤调<script>标签等,href和src的插入需要校验是否是以https或http开头的合法连接;

版本号排序

const versions = ['1.0.0', '1.2.1', '1.2.0', '2.0.0', '1.1.0'];
排序之后 ['1.0.0', '1.1.0', '1.2.0', '1.2.1', '2.0.0']

// 版本号排序
/*
先转成数字,并且把数字和字符串映射到一个对象中,
再根据数字进行排序,最后根据数字从对象中获取到对应的字符串返回
*/
const versions = ['1.0.0', '1.2.1', '1.2.0', '2.0.0', '1.1.0'];
const sortVersions = (versions) => {
  const obj = {}
  const arr = versions.map(item => {
    const v = item.split('.').join('')
    obj[v] = item
    return v
  }).sort((a,b) => a-b)

  return arr.map(item => {
    return obj[item]
  })
}

websocket和http2解释一下,socket是什么?

  • websoket:是浏览器和服务端之间的全双工通信协议,是应用层协议,基于TCP,websocket连接的时候需要http或https协议进行连接,连接成功之后的数据传输不再需要http;可用在视频聊天的实时通信中;
  • http2: 是浏览器和服务端之间的半双工通信协议,是应用层协议,基于TCP;
  • socket: 创建socket的时候可以指定使用什么协议(TCP或UDP),socket是应用在传输层和应用层之间的一层抽象接口,它是对tcp或UDP的封装,提供一些操作tcp和udp的API;

实现一个如下功能的event类

const event = new Event();
// 添加事件监听器
const callback1 = () => {
  console.log('Event 1');
};
event.on('event1', callback1);

const callback2 = () => {
  console.log('Event 2');
};
event.on('event2', callback2);

// 触发事件
event.emit('event1'); // 输出: Event 1
event.emit('event2'); // 输出: Event 2

// 移除事件监听器
event.off('event1', callback1);

// 再次触发事件
event.emit('event1'); // 无输出,因为已移除事件监听器
event.emit('event2'); // 输出: Event 2

// 实现
class Event {
    constructor(){
      this.callback = {}
    }
    on (name, cb) {
      if (!this.callback[name]) {
        this.callback[name] = []
      }
      this.callback[name].push(cb)
    }
    emit(name,...args){
      if (this.callback[name]) {
        this.callback[name].forEach(element => {
          element(...args)
        });
      }
    }
    off (name, cb) {
      if (this.callback[name]) {
        this.callback[name] = this.callback[name].filter(fn => fn !== cb)
      }
    }
}

sort()是内部使用了什么算法?时间复杂度是多少?indexOf()的时间复杂度是多少?

不同js引擎实现的方式可能不同,一般是通过快排和归并排序,当数组的长度在一个范围内才是快速排序,否则使用归并排序;时间复杂度度为O(nlogn); indexOf的平均时间复杂度为O(n),如果查找的值为第一项那么它的复杂度为O(1);如果查找的值为最后一项,那么它的时间复杂度为O(n);

vue中父子组件的生命周期顺序

创建:父beforeCreate > 父created > 父beforeMount > 子beforeCreate > 子created > 子beforeMount > 子mounted > 父mounted;
更新:父beforeUpdate > 子beforeUpdate > 子updated > 父updated;
销毁:父beforeDestory > 子beforeDestory > 子destoryed > 父destoryed;

setTimeout、setlnterval与requestAnimationFrame

  • setTimeout: 在指定时间之后执行一次回调;
  • setInterval: 在固定的时间间隔循环执行回调;
  • requestAnimationFrame: 在浏览器重绘之前执行函数或代码,用于平滑的动画实现;
  • setTimeout和setInterval受事件循环机制的影响可能不会按时执行回调,会有偏差。

手写懒加载

// html
<img src="placeholder.jpg" data-src="image-to-lazy-load.jpg" 
alt="Lazy Loaded Image">

// js
// 获取到页面中所有的图片
const allImgs = document.querySelectorAll('img[data-src]')
// 监听页面滚动
window.addEventListener('scroll',lazyLoad )
function lazyLoad () {
  // 遍历所有的图片,判断每个图片的顶部和底部距离可是窗口的顶部的距离
  // 是否小于可是窗口的高度,小于就加载正确的图片
  allImgs.forEach(img => {
    const rect = img.getBoundingClientRect()
    if (rect.top > 0 && rect.bottom <= window.innerHeight ) {
      img.setAttribute('src', img.getAttribute('data-src'))
      // 显示之后删除属性,防止重复执行
      img.removeAttribute('data-src')
    }
  })
}
lazyLoad()

给页面注入50万个i怎么做提升性能?

  1. 使用虚拟列表的方式;
  2. 使用分页加载;
  3. 懒加载;

fileReader用过吗? base64编码原理?

  1. fileReader:用于异步读取文件内容,并且只能读取input元素上传的文件,无法读取指定路径的文件;
  • readAsText(file): 将文件内容读取为文本字符串;
  • readAsDataURL(file): 将文件内容读取为Base64编码的字符串;
  • readAsArrayBuffer(file):将文件内容读取为ArrayBuffer对象;
  • readAsBinaryString(file):将文件内容读取为二进制字符串;
  1. Base64是把二进制数据转成Ascll字符编码的方式;
  • 将字符转成对应的Ascll码,再把对应的Ascll码转成八位二进制数;
  • 将编码的二进制划分为每三个八位的bit(比特)为一组,如果最后一组不够三个字节,通过0补充;
  • 将每一组转成四个6位的bit为一组;每六位前面使用0补充为八位,转成十进制数,从Ascll表中找到对应的字符进行拼接;
  • 原始长度不是3的倍数,通过=进行补充;

手写快排

/*
    1. 随便从数组中选择一个值,注意需要把这个值从原数组中截取出去,
     否则递归就是死循环
    2. 遍历数组,比这个值小的放在左侧,比这个值大的放在右侧
    3. 递归
    4. 跳出递归的条件,当前数组不存在或者当前数组的长度小于等于0,
    否则死循环
*/
function qsort(arr){
      if (!arr || arr.length <= 1) {
        return arr
      }
      const m = arr.splice(Math.floor(arr.length/2), 1)[0]
      const left = []
      const right = []
      for (let i = 0; i < arr.length; i++) {
        if (arr[i] <= m) {
          left.push(arr[i])
        } else {
          right.push(arr[i])
        }
      }
      return [...qsort(left),m, ...qsort(right)]
    }

qsort([6,4,2,8,6,4,2,4,6,7,5,8,1,4]) // [1, 2, 2, 4, 4, 4, 4, 5, 6, 6, 6, 7, 8, 8]

前端卡顿,渲染时间超过多久会卡顿

  • 页面的渲染是每秒60帧,如果页面渲染超过了1000毫秒/60=16.67毫秒就会卡顿;
  • 优化:批量操作dom;批量操作Css,大量的js计算使用WebWork;压缩图片;合并网络请求;通过事件委托解决监听过多的事件;使用http缓存静态文件;异步加载脚本;使用cdn加载静态资源;动画使用requestAnimateFrame;

前端优化方法,判断元素是否在可视区域的方法

  • getBoundingClientRect():判断元素的上下是否小于可视区域的高度并且大于0,满足就出现在屏幕中;

观察者模式实现?

观察者模式是一种一对多的关系,观察者去观察一个对象(被观察者),当这个对象发生变化的时候会通知所有的观察者;

/*
观察者是我们注册的事件回调,被观察的对象为dom的click事件。
当点击dom的时候,被观察者会通过注册的事件回调通知观察者
*/
document.querySelector('#btn').addEventListener('click',
function () { 
    alert('You click this btn'); 
},false)

移动端高清方案如何解决?

  • 对于图片,使用响应式图片,img中的srcset设置多倍屏的图片路径;或者svg或字体图标;(svg在部分ios上会时不时不显示)
  • 对于1px像素问题,使用Css的transform的scale进行缩放;
  • 对于适配采用viewport+vw/vh+rem+flex布局;

vue是如何实现绑定事件的?

通过v-on指令进行绑定事件。可以简写成@。并且可以设置修饰符来添加阻止默认事件或冒泡事件等;

浏览器事件有哪些过程? 为什么一般在冒泡阶段而不是在捕获阶段注册监听?addEventListener 参数分别是什么?

  • 浏览器的事件有捕获阶段、目标阶段和冒泡阶段;
  • 使用捕获阶段的事件监听器相对较少,因为在实际开发中,大部分情况下冒泡阶段的监听器能够满足需求,而且冒泡阶段的监听器更符合通用的事件处理逻辑
  • 参数有三个,分别为对应的事件名,回调函数和一个对象,此对象可以设置Capture是否捕获,默认false不捕获但是冒泡;once是否执行一次;

移动端300ms延时的原因? 如何处理?

  • 是因为移动端双击缩放的原因,通过300ms来判断你是点击还是缩放。现在浏览器已经优化了这个延时,延时针对于旧版本的浏览器和ios的UIWebView;
  • 通过fastClick.js库或者使用touchStart进行点击;

优化seo的多种方式?

  • 使用语义化标签
  • 添加页面标题和meta描述
  • img图标添加关键词描述

常见的js类型错误

  • Type Error(类型错误): 比如一个非函数的变量,通过函数的形式调用;
  • Reference Error(引用错误): 访问一个不是对象上的属性的时候;
  • Syntax Error(语法错误): 缺少一个括号等语法错误;
  • Range Error(范围错误): 使用超出范围的值;
  • URLError(URL错误):使用encodeURL()进行编码或转码的时候传入不合法的URL就会报这个错;
  • Promise Rejection Errors(promise拒绝错误): 未处理Promise的错误;

DIFF算法为什么是0(n)复杂度而不是(n^3)

因为DIFF算法只遍历一次,所以它是O(n)

JSbridge原理js和native是如何通信的?

  • 原生会向网页的全局对象中注入一个对象,此对象上由原生提供一些调用原生的方法,这样就可以在js中调用原生的方法了并且可以传递一些参数;

treeshaking是什么?为什么可以实现

  • treeShaking是构建工具中的一种优化方式,可以抖动掉没有使用的代码;
  • treeShaking和es6 Module结合,es6 module的import和export可以进行静态导入导出;在编译阶段treeShaking根据静态分析,找出哪些导出的变量被使用,哪些没有被使用,把没有使用的标记为unused状态;从最终的构建结果中移除掉没有被使用的代码;

移动端如何实现下拉到底部,跟随移动结束后回弹的动画?

/*
1. 通过移动端的touchStart,touchMove和touchEnd三个事件实现
2. 在touchStart中记录当前开始的位置
3. 在touchMove中通过当前的位置减去开始的位置就是滑动的距离,判断这个
距离是否在一个阈值之内,在的话就把它设置为外层元素的transform的
translateY的值,这样就会有被拉起来的效果;
4. 在touchEnd中把transform的translateY的值设置为0,通过Css的
transition设置一个动画时间,就达到了回弹的效果
*/
// 样式
.container {
  height: 100vh; /* 设置容器高度占满屏幕 */
  overflow: hidden; /* 隐藏超出容器的内容 */
  transition: transform 0.3s ease; /* 添加平滑过渡效果 */
  background: #000;
}
// html
<div class="container">
    <!-- 页面内容 -->
</div>
//js
<script>
const container = document.querySelector('.container');
let startY = 0;
// 记录开始的位置
container.addEventListener('touchstart', (e) => {
  startY = e.touches[0].clientY;
});

container.addEventListener('touchmove', (e) => {
  const currentY = e.touches[0].clientY;
  // 获取到滑动的距离
  const distanceY = currentY - startY;
  console.log(container.scrollTop, distanceY)
  // 下拉滑动的距离只能小于100,上拉的距离只能大于-100
  if (container.scrollTop === 0 && (distanceY < 100 && distanceY > -100)) {
    e.preventDefault(); // 防止页面滚动
    // 设置transform的translateY的值为滑动的距离
    container.style.transform = `translateY(${distanceY}px)`;
  }
});
// 放开的时候设置为0
container.addEventListener('touchend', (e) => {
  container.style.transition = 'transform 0.3s ease';
  container.style.transform = 'translateY(0)';
});
</script>

js浮点数运算不精确如何解决

  • 不精确是因为js底层运算是把数字转成二进制,而有些小数的二进制是无穷长的,js底层对于超过53位的二进制会进行截取,因此就会造成这种问题;
  • 通过BigNumber这个库进行计算;或者先把小数乘以一个10的倍数转成正数进行计算,计算完成之后再除以这个倍数;

promise是如何做向下兼容处理的

  • 使用polyfill
  • 自己实现一个promise,使用setTimeout模拟异步执行

清空一个数组的方法,越多越好

  • 直接赋值为空数组
  • 直接设置数组的长度为0
  • 使用splice从第0个截取到最后一个

{} + [] 结果是什么

0,因为{}写在前面。js会认为它是一个独立的代码块而不是一个对象,因此它不会产生任何结果和值,+ []表示把[]转成数字,就是0;

[] + {}的结果是什么?

'[object Object]', 对象类型参加运算的时候会进行隐式转换,对象先调用valueOf转成基本类型,转不成基本类型就会调用toString转成字符串,因此[]最后调用toString转成'',{}转成字符串'[object Object]'

img加载会影响渲染吗

图片不会阻塞dom解析和页面的渲染,图片加载完成之后会进行回流和重绘;

讲一讲DNS怎么找的

  • 本地缓存中查询,先从本地缓存中查找,找到并且没有过期直接返回对应的ip;
  • 本地主机文件(hosts)中查找,如果没有从本地缓存中找到就会从本地主机文件hosts中查找,hosts中会存储常用的域名和ip的映射关系;
  • DNS递归查找:本地主机文件中也没有查找到,就会向本地网络中的DNS服务器发起递归查询;本地网络中的DNS服务器就是你的网络服务商提供的;
  • 查询根域名的ip: 本地DNS服务器向根域名服务器发起请求获取到对应的一级域名的ip地址;
  • 查询一级域名的ip:本地DNS服务器向一级域名的服务器发起请求获取到对应的二级域名的ip地址;
  • 查询二级域名的ip:本地DNS服务器向二级域名服务器发起请求获取到对应的目标域名的ip;
  • 返回结果到浏览器:本地DNS服务器把目标域名的ip返回给浏览器并且进行缓存;

TCP和UDP的区别,都会丢包吗。平时遇到过TCP丢包的情况吗

  • 连接性:TCP是面向连接的,需要双方建立连接的基础上进行传输数据;UDP是无连接协议,可以直接发送数据;
  • 可靠性: TCP传输数据是可靠的,它通过确认,重传和流量控制来确保数据的可靠性和完整性;UDP传输数据是不可可靠的,可能会丢包;
  • 数据量:TCP对数据量没有限制,而UDP对数据的大小限制为64kb以内;
  • 开销:TCP需要三次握手和四次挥手,因此需要一定的开销;而UDP不需要;
  • TCP可以保证在传输层不会丢包,因为TCP在没有收到确认的数据的时候会进行重传,直到双方收到完整的数据;而丢包可能在其他层;而UDP是会丢包,丢包也没有重传机制,所以对于实时性较高而完整性不高的应用可以使用UDP;

无序数组中找第k大的数

function findK(arr, k){
    arr.sort((a,b) => a-b)
    return arr[k-1]
}

const arr = [2,4,87,4,1,5,7,0,5,4,2]
findK(arr, 5) // 4

浏览器内存泄露的场景及如何解决?

  • 未销毁的定时器:在页面卸载之后没有手动清除定时器,定时器会一直占用内存,页面卸载之前清除定时器;
  • 未移除的监听事件:通过addEventListener监听的事件,在页面卸载的时候没有进行相应的移除,在页面卸载或元素移除的时候移除对应的事件;
  • 闭包:闭包会导致闭包中引用的变量一直不会被回收释放,使用完闭包之后手动清除引用;
  • 引用的DOM元素:使用weakMap存储对应的dom引用,或者手动清除引用;
  • 全局对象上绑定大量的变量属性

算法题,fn([['a,b],['n','m],['0',1]])=>['an0', 'am0', 'an1', 'am1','bn0', 'bm0', 'bn1', 'bm0']

/*
从第0项开始,遍历当前项,拼接当前每个字符和上个字符,通过递归处理下一
层的数据;跳出循环的条件是当前索引大于等于了数组的长度表示遍历完了
*/
function fn (arr) {
  if (arr.length <= 1) {
    return arr
  }
  function cb(index,lastStr){
    // 如果当前项超出了数组的长度直接返回
    if (index >= arr.length) {
      return [lastStr]
    }
    const val = []
    // 遍历当前层
    for (let i = 0; i < arr[index].length; i++) {
      // 拼接上一项的
      const str = lastStr + arr[index][i]
      // 递归处理下一层
      const result = cb(index+1,str)
      // 存储到数组中
      val.push(...result)
    }
    return val
  }
  return cb(0,'')
}

输出什么?

image.png

11 11 11 6;因为return fn()的fn始终都是window执行的;

e.target和e.currentTarget的区别

  • e.target:当前点击的元素
  • e.currentTarget:当前事件绑定的元素

事件代理是什么?

事件代理就是把相同的事件绑定到外层元素上,不用给每个子元素都绑定一个事件;通过冒泡机制点击目标元素会冒泡到外层元素上触发外层元素上的事件;通过e.target来区分目标元素;

写一个事件代理函数,需要判断child是parent的子节点

实现 function proxy(event,cb,parent,child){}

/*
Element.closest(选择器):从当前元素上向上查找指定选择器的元素包括它自身;最后返回这个元素,没有找到就是null
Element.contains(元素):判断当前元素下有没有指定的子孙元素,包括自身;返回布尔值;
*/
function proxy(event,cb,parent,child){
    parent.addEventListener(event, (e) => {
      // 从当前节点向上查找距离最近的指定的节点,包括自身
      const elm = e.target.closest(child)
      // 父元素中包含这个子元素就触发回调
      if (parent.contains(elm)) {
        cb && cb(e)
      }
    })
}
// 测试
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul> 
const ul = document.querySelector('ul')
proxy('click', (e) => {
  console.log(ul.contains(e.target)) // true
}, ul, 'li')

CDN原理

  • 浏览器输入url发起请求的时候,会从本地dns服务器获取到对应的ip,如果是CDN的地址话就会通过CDN专用的dns服务器获取到CDN的全局负载均衡的服务器的ip;
  • 浏览器向CDN全局负载均衡的服务器发起请求,CDN全局负载均衡会选择一台用户所在区域的CDN服务器;
  • 区域的CDN服务器会选择一台负载最小,距离最近并且有资源的CDN服务器的ip返回给CDN全局负载均衡服务器,服务器再返回给浏览器;
  • 浏览器向CDN缓存服务器发起请求获取资源;

链表,怎么判断链表有环无环

class ListNode {
  constructor (val) {
    this.val = val
    this.next = null
  }
}

function hasCycle (head) {
  if (!head || !head.next) {
    return false
  }
  let slow = head
  let fast = head
  while(fast && fast.next){
    slow = slow.next
    fast = fast.next.next
    if (slow === fast) {
      return true
    }
  }
  return false
}
const a1 = new ListNode(1)
const a2 = new ListNode(2)
const a3 = new ListNode(3)
a1.next = a2
a2.next = a3
a3.next = a1
hasCycle(a1) // true

说一下跳转路由组件怎样销毁

  • 如果有父组件,就先从父组件上删除子组件;
  • 删除当前组件依赖的其他watcher和其他组件依赖当前组件的watcher;
  • 移除响应式数据的引用
  • 清空虚拟节点
  • 执行destory钩子
  • 关闭事件监听
  • 删除当前实例
  • 注意:在destoryed钩子中依然可以访问实例和属性,方法,但是已经删除了虚拟节点和依赖,所以无法更新视图了;并且属性相当于不是响应式的了,依赖也不会更新;

import 和 require 导入的区别是什么?

  • import:是es6的模块语法;是静态加载的,所以在模板编译的时候就确定了导入和导出;因此必须放在模块文件的顶部,不能动态导入;import导入的是值的复制,其他文件中如果修改了这个值,原始的导出不会修改;
  • require:是CommonJS的模块语法;是动态加载的,导入和导出是发生在运行时;因此可以放在模块文件中的任何位置;require导入的是一个引用,在其他文件中修改这个导入的值,原始导出的值也会修改;

如何避免 css 全局污染

  • 编译时,通过模块化的css(css module),css-in-js,scoped css;
  • 运行时通过命名空间来区分,不同模块使用不同的类名,可以使用BEM规范定义类名;
  • scoped css:编译的时候会给具有样式的元素上添加data-XX属性,在css中也会给对应的样式添加属性选择器;
  • css module:编译的时候修改选择器名称为全局唯一的名称,并且放在:export对象下,在需要的地方引入这个样式,通过styles.名称的方式调用;

css modules 的原理

css module:编译的时候修改选择器名称为全局唯一的名称,并且放在:export对象下,在需要的地方引入这个样式,通过styles.名称的方式调用;

/* styles.module.css */
.button { background-color: blue; color: white; } 
/* 导出类名 */ 
:export { primaryButton: button; }

// app.js 
import styles from './styles.module.css'; 
// 使用类名 
const buttonElement = document.createElement('button'); buttonElement.classList.add(styles.primaryButton);

组件库如何做按需加载

  • 组件库中的组件按需加载需要使用babel-plugin-import插件配合完成,在.babelrc文件中配置对应的组件所在文件夹和样式文件夹。
  • (每个功能单独打包)按照功能进行划分,每个功能都单独放在一个文件夹中,并且每个功能都有一个入口文件,这样做的目的就是为了打包的时候给每个功能都配置一个入口文件,每个功能都能单独的打成一个bundle;按需引入对应的功能只引入对应的bundle;
  • 每个功能下的入口文件中,导入此功能模块,并且在功能模块中定义一个install方法,方法内部通过Vue.component定义当前功能组件,这样在使用的时候就可以通过use方法把功能组件和使用者的Vue关联起来;
// 以element-ui为例
// 目录结构
- components
    - button
        - pl-button.vue
        - index.js
    - ele
        - index.js
    - icon
        - pl-icon.vue
        - index.js
    - input
        - pl-input.vue
        - index.js
// button功能的入口文件
import Button from './pl-button'
Button.install = (Vue) => Vue.component(Button.name, Button)
export default Button

// 按需打包,那么多组件,无需一个一个手动给打包工具配置入口路径,通过js进行操作
/*build/utils.js*/
const fs = require('fs')
const path = require('path')
const join = path.join
const resolve = (dir) => path.join(__dirname, '../', dir)

function getComponentEntries(path) {
    let files = fs.readdirSync(resolve(path));
    const componentEntries = files.reduce((ret, item) => {
        const itemPath = join(path, item)
        const isDir = fs.statSync(itemPath).isDirectory();
        if (isDir) {
            ret[item] = resolve(join(itemPath, 'index.js'))
        } else {
        	const [name] = item.split('.')
            ret[name] = resolve(`${itemPath}`)
        }
        return ret
    }, {})
    console.dir(componentEntries)
    return componentEntries
}
 
// 总体打包
/*src/index.js*/
import Button from 'components/button'
import Icon from 'components/icon'
import Ele from 'components/ele'
const PlainApp = {
    install(Vue) {
        Vue.use(Button)
        Vue.use(Icon)
        Vue.use(Ele)
    }
}
export default PlainApp

// 打包入口
entry: {
    ...getComponentEntries('components'),
    index: resolve('src/index.js'),
},

写一个 promise 重试函数,可以设置时间间隔和次数。

function retryPromise(fn, max, time){
      return new Promise((resolve, reject) => {
        function f (n) {
          fn().then(resolve, (err) => {
            // 如果n等于0就停止
            if (n <= 0) {
              reject(err)
            } else {
            // 否则定时执行下一次
              setTimeout(() => {
                f(n-1)
              }, time)
            }
          })
        }
        f(max)
      })
    }
    function fetchData() {
      return new Promise((resolve, reject) => {
        // 模拟异步请求数据
        setTimeout(() => {
          const randomNumber = Math.random();
          if (randomNumber < 0.5) {
            resolve('Data successfully fetched!');
          } else {
            reject(new Error('Data fetching failed!'));
          }
        }, 1000);
      });
    }
    retryPromise(fetchData, 5, 2000).then(data => {
      console.log(data);
    }).catch(error => {
      console.error(error);
    });

设计一个input 组件需要哪些属性?

  • value:用于设置和获取输入框的值;
  • type:设置input的类型,是number还是Text还是password;
  • placeholder:设置为空的默认显示
  • disable: 是否禁用,禁用之后不能输入;
  • readOnly:是否只读,设置只读不能进行编辑,可以复制和选中;
  • maxLength:限制输入字符的最大长度;
  • minLength:限制输入字符的最小长度;
  • required:是否是必填项
  • onChange: 输入框中值改变的时候触发;
  • onFocus: 聚焦的时候触发;
  • onBlur: 失去焦点的时候触发;
  • autoFocus: 是否自动聚焦;
  • name: 输入框的name属性,提交表单的时候使用;
  • disabled和readOnly的区别:disabled会使当前输入框变灰,而readOnly没有变化,disabled针对所有表单元素有效,而readOnly只针对于输入类型的表单元素有效;使用disabled的表单元素在表单提交的时候不会被传递,而ReadOnly会被传递;

webpack 能动态加载 require 引入的模块吗?

不能,因为Webpack在打包的时候需要明确的知道模块依赖的其他模块;所以不能使用Require动态的引入模块,但是可以使用规定写法进行异步加载模块;

require引入的模块 webpack 能做 Tree Shaking吗?

不能,因为Require是CommonJS的模块化,而CommonJS的导入导出都是在运行时确认的,而Tree Shaing是在编译的时候进行静态分析;所以不能,如果想要进行tree Shaking可以使用es6的模块化,因为Es6的模块化是静态确认的;

webpack 如何实现动态加载?

Webpack可以使用动态导入来实现动态加载模块,在运行时根据需要动态加载模块;可以使用es6的import导入,因为import返回一个promise;也可以使用webpack提供的import导入;

Rem布局及其优缺点?

  • 优点:可以根据不同设备的屏幕大小,设置对应的根字体的大小即可;易于设置和维护;
  • 缺点:需要额外的进行计算,不过可以通过打包插件自动计算;有些老版本的浏览器不兼容;用户设置的手机字体比较大,会导致rem失效;

vw或vh与%的区别?

  • vw和vh是相当于可视区域的大小,可视区域分成一百分,1vw或1vh占一份;
  • 百分比是相对于父级元素的大小;

click点击穿透问题?

  • 点击穿透就是点击上层元素的touchstart和touchend事件,下层元素的click事件也会被触发;原理:因为js默认先触发touch事件在一定时间之后再次触发它的click事件,如果元素自身没有click事件就会触发视觉上它下层元素的click事件;
  • 可以在被覆盖的元素上设置pointer-event:none,被覆盖的元素就不会接收点击事件;
  • 上层元素直接使用click事件;
  • 上下层元素都使用touch事件;
  • 设置上层元素在300ms之后隐藏,因为从touch事件到触发click事件最慢需要300ms;

1px问题

通过媒体查询 + 设备像素比 + 伪元素 + 缩放

// html部分
<div class="a">1px像素</div>

// css部分
.a{
    position: relative;
    width: 100px;
    height: 100px;
}
// 1倍屏
.a:after{
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    transform-origin: left top;
    width: 100%;
    height: 100%;
    border: 1px solid red;
    box-sizing: border-box;
}
// 2倍屏
@meta (-webkit-min-device-pixel-ratio:2) {
    // 1倍屏
    .a:after{
        content: '';
        position: absolute;
        left: 0;
        top: 0;
        transform-origin: left top;
        width: 200%; // 宽高放大2倍
        height: 200%;
        border: 1px solid red;
        transform: scale(0.5);// 缩小一倍
        box-sizing: border-box;
    }
} 
// 3倍屏
@meta (-webkit-min-device-pixel-ratio:3) {
    // 1倍屏
    .a:after{
        content: '';
        position: absolute;
        left: 0;
        top: 0;
        transform-origin: left top;
        width: 303%; // 宽高放大3.3倍
        height: 303%;
        border: 1px solid red;
        transform: scale(0.33); // 缩小3.3倍
        box-sizing: border-box;
    }
}