(一)HTML
常见方法
- 查询节点方法:
- 根据id查询dom节点document.getElementById(‘#id’)
- 根据class查询dom节点document.getElementByClassName(‘.className’)
- 根据tagName查询dom节点document.getElementByTagName(‘tagName’)
- 查询所有节点document.querySelectorAll(‘*’)
- 创建节点方法
- 创建dom元素节点document.createElement(‘标签名’)
- 创建文本节点document.createTextNode(‘文本内容’)
- 追加节点方法
- 插入一个子节点node.appendChild(dom节点)
- 父节点下的某一个子节点前面插入一个节点parentNode.insertBefore(newNode, beforeNode)
- 替换节点parentNode.replaceChild(newNode, oldNode)
- 设置属性
- 设置属性dom.setAttribute(key, value)
DOM操作——DOM节点的创建、插⼊和删除
- 设置属性dom.setAttribute(key, value)
虚拟dom转化为真实dom
// vnode结构:
// {
// tag,
// attrs,
// children,
// }
//Virtual DOM => DOM
function render(vnode, container) {
container.appendChild(_render(vnode));
}
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 => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
})
}
// 子数组进行递归操作
vnode.children.forEach(child => render(child, dom));
return dom;
}
HTML5新增内容
- 语义化标签header、footer、nav、aside、article、section
- 多媒体标签video、audio和绘图的canvas
- webStorage储存机制sessionStorage、localStorage
- history API back()、forward()、go()、pushState()、replaceState()、popState()事件
- 表单元素标签升级,给表单元素加了placeholder属性,input标签type增加了number、tel、email、date、color、range、search值
onready和onload的区别
onready在html的dom树加载完成后触发,onload是整个页面及其所有依赖的资源如图片、CSS、JS文件完全加载完之后执行
怎么判断某个元素是否在可视区域
- 获取已滚动高度document.documentElement.scrollTop || document.body.scrollTop
- 获取视口高度document.documentElement.clientHeight
- 获取元素的偏移量dom.offsetTop
- 当offsetTop < ( scrollTop + clientHeight)的时候就表明了元素进入了可是区域
- 可以在window定义scroll事件去动态判断,注意加节流
web worker
WebWorker 实际上是运行在浏览器后台的一个单独的线程,因此可以执行一些耗时的操作而不会阻塞主线程
详解 WebWorker 的概念、使用场景、示例
(二)CSS
1.垂直水平居中
- 文字水平垂直居中:text-align: center, line-height = height
- margin水平居中: 子盒子margin: 0 auto(子元素需要有宽度)
- 弹性盒: 父盒子设置flex,justify-content: center, align-item: center
- 使position+transform:子元素设置absolute,top和left设置为50%,transform:translate(-50%,-50%)
- 网格布局Grip:place-items设置为center(是align-items:center和justify-items:center的简写)
浮动元素居中
- 浮动元素外层套一个div,宽度跟浮动元素宽度一样,然后给浮动元素外层的div设置居中
- 外层div也设置左浮动并且设置相对布局,设置left: 50%,子元素也设置相对布局,设置left: -50%
2.变量使用
css变量分为全局变量和局部变量,其中:root中定义的为全局变量,对应选择器下面定义的为局部变量,变量可以通过js语句修改document.documentElement.style.setProperty('--main-color', '#000000')
或者使用媒体查询给同一个变量设置不同的值
- 原生css:使用
--xxx: 30px
定义,font-size: var(--xxx)
进行使用 - less: 使用
@xxx: 30px
定义,font-size: @xxx
进行使用 - sass: 使用
$xxx: 30px
定义,font-size: $xxx
进行使用
@media (prefers-color-scheme: dark) {
:root {
--main-color: #2c3e50;
}
}
3.scoped
加了scoped之后样式仅在当前组件生效,并且默认不会穿透到子组件(但子组件的根标签会影响)scoped实际上是在标签中增加data-v-xxx属性,然后通过属性选择器进行限制。
4.样式穿透
原生: >>>
less和scss: /deep/
vue2中能使用>>>和/deep/,而在vue3中使用有可能回报错,建议使用::v-depp
5.sass和scss
sass是scss的前身,sass不适用大括号{}和分号;,而通过严格的缩进语句进行限制,scss更像是less,有{}和;,并且不要求缩进
块级元素与行内元素的区别
- 块级元素独占一行,可以设置宽度和高度,行内元素由内容撑开,不能设置宽度和高度
- 块级元素可以包含块级元素和行内元素,行内元素只能包含行内元素
外边距合并的原因及解决方案
- 两个上下相邻的块级元素都设置了外边距时,他们之间的外边距会等于两个块级元素中设置的较大值
- 解决方法
- 只给一个元素设置外边距
- 设置为行内块
- 给其中一个增加一个父元素并设置为overflow:hidden
- 使用浮动或定位,使其脱离文档流(不推荐)
父子元素外边距合并(塌陷)
- 父子元素垂直方向上都设置了外边距会进行合并,大小为两者中较大的值,父子元素之间不会由外边距,他们会紧贴在一起
- 解决方法
- 设置为行内块
- 父元素设置overflow: hidden 或 增加border值 或 padding值
- 父元素或子元素设置浮动或者定位使其脱离文档流
文字换行,省略号
换行:word-wrap: break-word
省略号:
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
切换主题色
采用CSS变量 + 属性选择器形式,采用属性选择器定义几套颜色的变量,给html加个data-theme属性,切换主题的时候去改变data-theme的值
自适应
固定1280分辨率,然后根据1280分辨率出原型图,使用px作为单位,后面通过postcss-px-to-viewport转为vw
css 之移动端适配方案vw、rem详解
CSS sprite是什么,谈谈这个技术的优缺点,什么是CSS Hack
- css sprite就是雪碧图,将多个小背景图整合到一个大图中,一次请求,减少了服务器的压力,然后通过background-positon设置偏移量
- 不同厂商的浏览器和某浏览器的不同版本对css的支持、解析不一样,导致在不同的浏览器会展示出不同的效果,为了在不同的浏览器获得一致的效果,我们就要针对这些特殊情况写上css
css3新增了什么
- 选择器增强,增加了属性选择器、伪类选择器新加了:hover、:first-child、伪元素选择器新加了::first-letter
- 盒子模型增强,增加了box-sizing、box-shadow、border-radius、border-image、背景渐变linear-gradient()
- 文字效果text-shadow、word-wrap、text-overflow
- 过渡transition、动画animation、关键帧@keyframes、转换transform
- 媒体查询@media、flex布局
css选择器为啥要从右向左匹配
对某一个节点计算样式时,当定义的样式有嵌套关系时,如果从左往右去匹配,那就要去逐层对比,如果从右往左匹配的话,当最后一个没匹配到那就可以不用比较了
为什么CSS选择器是从右往左解析
(三)JavaScript
闭包
闭包是指那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的,正常定义的变量在函数执行完之后内存就会回收,但是闭包中用到的变量不会被回收
闭包的优缺点
- 优点:闭包可以延长变量的生命周期,模仿私有变量,使得外部无法直接访问,只能通过接口去访问
- 缺点:闭包因为保持了对变量的引用,如果没有被及时释放,可能会导致内存泄漏
事件捕获和冒泡
- addEventListener三个参数,事件类型、处理函数、是否捕获阶段执行(默认为false)
- 先捕获在冒泡
- event.stopPropagation()阻止事件往后传播,event.preventDefault()阻止默认事件
字符串常用方法
- slice(start, end) :截取[start,end),两个参数可正可负,负数表示从右边开始截取
- substring(start, end) :截取[start,end),两个参数都为正数
- substr(start, length):start可正可负
- concat:对字符串进行合并
- indexOf(searchString,startIndex):返回子字符串第一次出现的下标,从startIndex开始查找,找不到返回-1
- lastIndexOf(searchString,startIndex)
- trim():去除字符串两边空格
- toLowerCase():转为小写
- toUpperCase():转为大写
- split(separator,limit):第一个参数是字符串或者正则,第二个指定数组的最大长度
- str.replace(rgExp/substr,replaceText)
数组的常用方法
- push
- pop
- shift
- unshift
- concat
- reverse
- indexOf
- includes
- foreach
- map
- reduce
Object常见方法
- is(a, b)
主要用来解决===
的NaN与NaN比较不相等、-0和+0比较相等的问题。is判断NaN与NaN比较相等,-0和+0比较不相等,===
比较结果相反,react用Object.is来做浅比较
你用过Object.is() 方法吗? - assign(target, source1, source2,…)
- 该方法会将所有source中的可枚举属性值复制到target对象中,并将合并后的target对象返回
- 当遇到同名的属性,后面合并的会覆盖掉之前的
- 如果target传入的是null或者undefined会报错,如果传入的不是对象,会先转换为对象
- 如果是source位置出现非对象,会先转化为对象,转不了的则跳过(null、undefined、数字、布尔不会报错,只会跳过),字符串abc会转为
{ 0: a, 1: b, 2: c}
Object.assign方法详解
- create(proto,propertiesObject?)
会将proto作为新创建对象的原型
Object.create()
类数组(伪数组)
- 有length属性、能for循环,但是不具备数组原型中的方法
- 有函数中的arguments,通过getElementByTagName等获取dom列表方法获取的节点列表
html 定义对象数组,javascript类数组的深入理解
Map和WeakMap的区别
- Map的键值可以是任意数据类型,WeakMap的键值只能是对象类型
- Map的对键值对的引用是强引用,只要map对象存在,那么键值对就会一直存在内存中,不会被垃圾回收掉,WeakMap对键的引用是弱引用,如果没有其他地方引用这个键对象,那么这个键值对就会被垃圾回收掉,可以以此来优化性能
- 由于WeakMap是弱引用,所以它的键值是不可枚举的,WeakMap没有keys、values和entries方法
JS中Map和WeakMap的不同
为啥要有Map
- 对象只能用字符串、数字和Symbol作为key,Map可以将任意类型作为key
- Map采用了类似哈希表的算法实现了快速查找和删除,在处理大数据量时效率高
let和const(变量提示、暂时性死区)
var定义的是全局变量(window、global),不存在块级作用域和暂时性死区,存在变量提升(可以先使用再声明),可以重复声明(后面声明的会覆盖前面的)
let和const是块级作用域,存在暂时性死区(声明变量之前不可用),不存在变量提示,不能重复声明
事件循环
事件循环又叫任务 循环,因为js是单线程的,但是有时又会处理一些异步的操作,如一些耗时的操作、交互相关的工作,触发事件等,js不可能一直等着这些事件执行,如果等着的话页面就会卡死,所以这些异步的操作要交给其他线程去处理,处理有了结果之后就放进任务队列里面去,js处理完当前的任务后再从消息队列里面去取出任务来执行,而消息队列并不是一个队列,而是多个队列,之前说是有宏任务队列和微任务队列嘛,但是随着浏览器越来越复杂,这样简单的划分已经不能够满足需要了,宏任务被更加细化了,w3c的解释就是浏览器必须有一个微任务队列,并且优先级是最高的,然后其他队列由浏览器去根据实际情况去考虑,比如谷歌的就至少包括3个队列,首先是微任务队列优先级最高,交互队列优先级次之,延时队列优先级再次之。在一次事件循环中,当前任务执行完后首先会把微任务队列给清空,然后渲染页面,然后浏览器根据实际情况从其他任务队列里面取出任务去执行,这样循环往复的就构成了事件循环。每一次事件循环的时候都会检查消息队列里有没有任务存在,如果没有任务存在则会进入休眠,当有新任务添加到任务队列的时候,如果主线程是休眠状态的,则会将其唤醒继续循环拿任务。而且最近我还了解到一个事,不管是在规范中还是现在浏览器的实现中都是没有宏任务这个叫法,有的只是task和microtask,也就是任务和微任务,而微任务也是es6规范中引入的,宏任务也只是我们相对于微任务的一种叫法而已。
宏任务
:整体代码、setTimeout/setInterval、I/O、UI交互、postMessage(h5新增,用于不同源窗口进行通信)、MessageChannel(在不同的浏览上下文建立通信管道)、setImmediate(Node环境api,事件循环末尾执行)
微任务
:Promise.then、Object.observe(es7提出,监听对象属性改变,只有chrome、opera浏览器支持)、MutationObserver(监听dom的改变)、process.nextTick(Node环境,下一个tick之前调用,即宏任务前)
宏任务——>微任务——>dom渲染——>宏任务
js事件循环
js中的宏任务与微任务
postMessage可太有用了
浅谈MessageChannel
Object.observe简介
MutationObserver详解
js中到底有没有宏任务这个概念?
this指向
- 在全局上下文中,this指向全局对象(浏览器是window对象,node是global对象)===非严格模式下,严格模式下,this为undefined。
- 在对象方法中,this指向调用该方法的对象。
- 在构造函数中,this指向新创建的实例对象。
- 在事件处理函数中,this通常指向触发事件的DOM元素。
- 箭头函数this用的是定义箭头函数时外层函数的this
注意
- es6中,var命令和function命名声明的全局变量是顶层对象(window)的属性,但let和const声明的全局对象不是顶层对象的属性
apply、call、bind的区别
call、apply改变this后会立即执行,bind不会立即执行,call和bind可接受多个参数,apply接受的是参数数组
注意:
多次bind,this由第一次bind决定,将null、undefined传入call、apply、bind后,this不会改变
[1]彻底弄懂bind,apply,call三者的区别
原型链
- 在js中我们创建的每一个对象,都会关联到另一个对象,这个就是我们对象的原型,这也是继承的一种形式,当然null除外,我们可以通过__proto__属性找到对象的原型,也可以通过对象原型的__proto__去找到对象原型的原型,这样一路下去就是原型链,null是我们原型链的终点,null是没有原型的。当然这其中还有构造函数,构造函数是用到创建我们对象实例,构造函数呢可以通过prototype来找到对象实例的原型
- 需要注意的是我们定义的构造函数其实Function创建的一个实例,所以Foo也能通过__proto__访问到Function.prototype,而Object也是一个构造函数,也是相当于通过Function创建的一个实例,所以Object.__proto__也能访问到Function.prototype
图解javascript原型链
JS原型与原型链图解
new命令做了什么
- 创建一个空对象
- 设置原型,将构造函数的原型指向空对象的 prototype 属性。
- 将 this 指向这个对象,通过apply执行构造函数。
- 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
类型判断的方式
- typeof:只能判断基本数据类型和函数,其中null和其他引用类型会判断为Object
- instanceof/isProtoTypeOf:只能判断引用类型,判断是否是某个类型的实例或者是否存在原型链上
obj instanceof Object
、Object.Prototype.isPrototypeOf(obj)
- constructor:获取构造函数,既能判断基本类型也能判断引用类型,null和undefined没有构造函数,不能检测,不能判断原型链上的构造函数
- Object.prototype.toString.call():返回一个对象字符串[object Type],可以检测包括null和undefined的所有类型
注:
- js的值是有
类型标签
+实际值
组成的,对象的类型标签是0,而null表示是0x00,所以typeof null
也就返回了object
typeof
操作符的优先级高于加法 (+)
等 二进制操作符, 因此, 必要时候记得用括号
js中四种类型判断
隐式类型转换
-
类型转换
- toString: 数组会相当于调用join方法通过’,'拼接起来,里面的null和undefined会转为空字符串拼接,普通对象相当于调用原型链中的tostring方法转为[object object]
- toNumber: null转0,undefined转NaN,数组和对象会先调用toPrimitive转为原始类型,再参照原始类型的规则转为number
- toBoolean: null、undefined、0、空字符串、false、NaN都是假值,其他都是真值
- toPrimitive:将对象转为原始类型,会先找有没有valueOf方法,如果valueOf返回结果为原始类型,则toPrimitive的结果就为这个,valueOf不存在或者返回结果不是原始类型,则调用toString方法作为返回结果,如果valueOf和toPrimitive都没有返回原始类型,则会报异常
从一道面试题说起—js隐式转换踩坑合集
-
宽松比较(==)
- 只有布尔值存在,则布尔值会转为数字类型
- 当数字与字符串比较时,字符串会转为数字类型(NaN与任何值不相等,包括他自己)
- 对象与原始类型比较时,对象会调用toPrimitive转为原始类型
- null和undefined宽松比较相等,但null、undefined与其他所有的值都不相等
-
‘11’ < ‘2’ 为true
- <、>、<=、=>等操作如果有一边是数字,则两边转为数字进行比较
- 如果两边都是字符串,则转为unicode编码进行比较,也就是调用charCodeAt()方法(该方法默认转换第一个字符)
输入url发生了什么
-
查找缓存
-
dns域名解析(浏览器缓存、本机host文件、本地dns服务器、根dns服务器、顶级dns服务器、权限域名服务器)
-
根据获取的ip建立tcp连接(三次握手)
-
服务端根据请求将html文件返回到客服端
-
客服端对html自上而下进行解析(遇到其他链接会进行下载)
-
会生成dom树和css规则树,然后合并成渲染树(遇到js会阻塞),将页面渲染出来
-
关闭tcp连接(四次挥手)
JavaScript解析过程
- js引擎在执行JavaScript代码的时候需要先解析再执行
- 解析会在堆内存创建变量环境VO(旧版本ECMA中叫VO,是个Object,最新版本的ECMA中叫VE,其中全局VO又叫GO,函数的VO叫AO),创建VO的时候会把定义的变量收集起来并赋值undefined,遇到函数的时候会为函数开辟一块单独的空间来存放改函数的父作用域和函数体内容,VO的函数名指向函数的这块空间,其中全局对象GO中会包含一些全局的方法和类,并且会有一个window属性指向GO本身
- 执行代码的时候会构建执行上下文并且压入执行上下文栈中,其中执行全局代码构建的是全局执行上下文GEC,执行函数代码构建的是函数执行上下文FEC,执行上下文中包含了变量对象VO的引用、作用域链、this指向,在执行代码的过程中遇到变量的赋值语句就会给VO进行赋值,当遇到函数调用时候会找到函数存储地址去解析并执行函数,也就是会创建函数变量环境和函数执行上下文,并把函数执行上下文压入栈中
- 当对应函数执行完后,函数执行上下文会出栈,对应VO没了引用后就会进行销毁
JavaScript的执行过程
JavaScript是解释性语言还是编译型语言
- 编译型语言和解释型语言都需要前将编译语言转换为机器语言,其中编译型语言的转换是在代码执行前,解释型语言的转换是在代码执行中
- 即时编译JIT:因为js是解释型的语言,在执行代码的时候需要逐行解释执行,当遇到一些执行频率高的语句效率会相对来说没这么高,所以就需要有一个工具来提升代码执行速度,也即是JIT。当执行代码的时候会追踪不同语句的命中次数,随着命中次数的增加该语句会被标记为warm、hot、very hot。随着原来越热,JIT就会将这个语句的编译结果存储起来并有可能做一些优化,下次执行同样的语句就直接从储存中取出来执行
- 抽象语法树AST:源代码经过词法分析和语法分析后产生的一种树状结构,是对源代码的一种抽象,转化为抽象语法树有利于程序的分析和处理。
- 用途:代码语法检查、代码风格检查、代码格式化、代码高亮、代码错误提示、代码自动补全
- 工具:JavaScript parser解析器,会把js源码解析成抽象语法树,再进一步转化为字节码或直接生成机器码
- 词法分析:把每一条语句拆分为一个个单独的token
- 语法分析:把一个个token组装成AST
JavaScript到底是解释型语言还是编译型语言?
你应该了解的抽象语法树AST
浏览器储存
- cookie一般由服务器生成,可以设置过期时间,每次请求都会携带在header中,存储大小4k;同域可共享,通过设置可以跨域共享,可以设置Domain和Path让指定一级域名的某个路径可以访问cookie
- sessionStorage是会话缓存,在页面关闭后清除,大小5m左右;不能多页面共享,但A页面打开B同源页面(window.open(‘同源页面’))会对sessionStorage复制一份,后面各自修改互不影响
- localstorage是本地缓存,一直有效,大小5m左右;同域即可共享
一文搞懂前端存储
忘记 localStorage 吧,indexedDB 才是前端存储新宠!
cookie跨域请求
- 服务端设置
- Access-Control-Allow-Credentials:true(设置可以携带cookie等凭证)
- Access-Control-Allow-Origin:请求域名(设置哪些域名可以共享)
- 客户端请求设置
- credentials: ‘include’(并不是直接设置在请求头上)
Cookie 能跨域吗?如何设置?
浏览器缓存(强缓存和协商缓存)
- 主要字段
- 强缓存:Expires、Cache-Control
- 协商缓存:Last-Modified/If-Modified-Sinse,ETag/If-None-Match
- 详细
- Cache-Control是http1.1提出的,客户端和服务端使用各有不同,客服端可以设置no-cache,服务端可以设置public、private、maxage、s-maxage、no-cache、no-storage
- 问题
- Expires依赖于客户端的时间,客户端时间的修改会影响到缓存的时效
- Last-Modifined的时间粒度是秒,一秒内多次修改不能识别到,并且当编辑了文件但未修改内容时也会更新修改时间
跨域及解决方案
- 跨域是浏览器的一种安全策略,为了防止跨站请求攻击,浏览器需要是确保是同源的才能正常返回,同源是指协议、ip、端口都一致
- 解决方案
- 开发环境采用proxy代理(实质是启动一个代理服务器)
- 生产环境用nginx去启动一个反向代理服务器
- 服务端去设置支持跨域规则,添加@CrossOrigin 注解,实质是给请求头添加access-control-allow-origin设置允许的源
- 使用script标签回调形式
正向代理和反向代理
- 正向代理代理的是客户端,客户端是知道服务端的ip、端口等的,服务端不知道是哪个客户端发起的请求。可以在局域网中访问外网。如vpn
- 反向代理代理的是服务端,客户端请求的是代理服务器,但具体请求到哪个ip客户端是不知道的。只要用作负载均衡、统一的访问权限控制
浏览器原理(html解析过程)
当我们输入url回车的时候,浏览器的主进程会让网络进程去获取HTML资源,这时候会进行dns域名解析和三次握手建立连接那些,这方面就不细说了,当浏览器主进程获取HTML资源后会简单处理下(会做下安全检查,防止恶意代码)然后通过renderhost接口传到渲染进程生成一个渲染任务,在事件循环机制的作用下,渲染主线程取出消息队列的渲染任务,开启渲染流程。整个渲染流程分为多个阶段,分别是HTML解析、CSS计算、布局、分层、绘制、分块、光栅化、合成,每个阶段都有明确的输入输出,上一个阶段的输出会作为下一阶段的输入。
- 渲染的第一步就是解析HTML嘛,解析的过程中遇到css就解析css,遇到js就执行js。为了提高解析的效率,浏览器在开始解析之前会启动一个预解析线程,率先下载HTML中外部的CSS和JS文件。当主线程解析到
link
标签的位置,如果CSS文件还没有下载解析好,主线程不会等待,而是继续往下解析HTML,下载和解析CSS都是在预解析线程进行的。当解析遇到script
标签的时候,会停止解析HTML,会等待JS下载并将全局代码解析执行完,然后再重新解析HTML。因为JS中可能会有一些处理dom的操作,所以必须等待JS解析处理完。经过这一步后会生成DOM树和CSS规则树,浏览器的默认样式、内部样式、外部样式、行内样式均包含在CSS规则树中。 - 下一步就是样式计算,主线程会遍历得到的DOM树,并为树中的每个节点计算它最终的样式,称之为computed style,在这过程中很多预设的值都会变成绝对值,比如color设置为red红色,这里会转为rgb,相对的单位会转为绝对的单位,比如rem、em会转换为px,这一步完成后会得到一颗带有样式的DOM树
- 在下一步就是布局了,布局阶段会去遍历DOM树的每一个节点,并计算每个节点的几何信息,例如节点的宽高,相对包含快的位置。大多数情况下,DOM树和布局树并不是一一对应的,例如
display:none
设置的节点并没有几何信息,并不会生成到布局树中,又比如说使用了伪元素选择器,DOM树中并没有这些节点,但又会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致DOM树和布局树没法一一对应。 - 布局完了后就是分层了,主线程会采用一套复杂的策略来对整个布局树进行分层(图层)。分层的好处在于当某一层改变了仅仅会对该层进行后续处理,从而提高了效率。滚动条、堆叠上下文、transform、opacity等样式或多或少都会影响到分层,也可以用will-change来更大程度的影响分层的结果。
- 再接下来就是绘制了,主线程会为每一层生成一个绘制指令集,用来描述这一层的内容如何画出来
- 绘制完成后就是分块,主线程会将绘制指令集交给合成线程,然后合成线程对每个图层进行分块(图块),将其划分为更小的区域,它会从线程池中拿取多个线程来完成分块的工作(分块后可以优先处理视口中的块,不分块的话处理整个页面图层会很慢)
- 分块之后就是光栅化了,会将图块转换为像素点信息,形成一块块的位图,合成线程在这过程中会用到GPU加速,会将块信息交给GPU进程,GPU进程会开启多个线程以极快的速度完成光栅化,并且优先处理靠近视口区域的块。
- 最后一步就是合成了,合成线程拿到每个层、每个块的位图后,生成一个个的指引(quad)信息,指引会标识出每个位图应该画在屏幕的哪个位置,以及会考虑到旋转、缩放等变形效果。变形发生在合成线程,与主线程无关,这就是
transform
效率高的本质原因。合成线程会把指引信息提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像。
浏览器渲染页面原理
浏览器原理
常见es6
- let和const、Symbol
- 解构赋值、扩展运算符
- 模板字符串
${a}
- Set和Map
- Promise
- for of、for in
- Generator和async await
- class
- Module,也就是ESM
setTimeout/setInterval和requestAnimationFrame的区别
setTimeout最小4毫秒,setInterval最小10毫秒,requestAnimationFrame跟屏幕刷新频率相同,setTimeout和requestAnimationFrame都是只执行一次
[1]setTimeout/setInterval与requestAnimationFrame的区别?
数组去重
const list = [1,2,3,4,5,6,1,2,3,4,5,6];
const uniqueList = list.reduce((pre, cur) => {
return !pre.includes(cur) ? pre.concat([cur]) : pre; // 这里不能使用push,因为push返回值是length
}, []);
const uniqueList = [...new Set(list)];
const uniqueList = list.filter((item, i) => {
return list.indexOf(item) === i;
});
axios拦截器的执行流程
请求和响应拦截器都放在同一个数组中,请求拦截器往数组前面加,所以先声明的后执行,响应拦截器往数组后面加,所以先声明的先执行
axios拦截器执行流程分析
原生JS如何使用esm
深拷贝和浅拷贝
浅拷贝
只是赋值原数据的内存地址,两个都是指向同一块内存地址,任一数据发生改变,都会影响到另一个
深拷贝
两个指向的不同的地址,数据发生变化时互不影响。要实现深拷贝可以采用JSON,数组的话可以使用concat()和slice()
实现深拷贝函数
入参a,判断a是数组、对象还是原始类型,原始类型直接返回,对象或数组直接创建一个新对象(数组),遍历值去调拷贝函数,将返回结果设置到新对象(数组)上,可以创建一个Map,将拷贝函数的返回结果设置上去,key为原对象的值(是对象的地址索引),每次调用拷贝函数前先查询map中有没有,避免做重复功
js全局函数
- 6个编码相关:escape()、unescape()、encodeURI()、decodeURI()、 encodeURIComponent()、decodeURIComponent()
- 2个数据处理:Number()、String()
- 4个数字相关:isFinite()、isNaN()、parseFloat()、parseInt()
- 1个特殊:eval()
- setTimeout是浏览器提供的,不是js内置的
encodeURI和encodeURIComponent区别
encodeURI只会对空格、%、中文进行编译,对http预留的字符不会处理
encodeURIComponent会对http预览的字符也会编译
nodeType
JavaScript中的所有节点类型都继承自Node类型,因此所有节点类型都共享相同的基本属性和方法。每个节点都有一个nodeType属性,用于表明节点的类型。elementNode.nodeType
的值是一个字符串,代表节点的类型,有以下12中取值,平时只用记住前三种就行
数组sort在v8中是如何实现的
v7.6前
v7.6后sort方法采用Torque语言编写,采用TimSort的方式去实现
隐藏类
当JavaScript引擎在执行代码时,会动态地创建对象的隐藏类。隐藏类会跟踪对象的属性和方法,并为它们分配固定的内存偏移量。每当对象的属性和方法发生变化时,隐藏类会根据变化的情况进行更新。使用隐藏类可以提高代码的执行速度,因为JavaScript引擎可以根据隐藏类的信息来直接定位和访问对象的属性和方法,而不需要进行动态查找或解析。这种优化技术可以减少对象访问的开销,提高代码的性能。
JavaScript 中, 隐藏类是什么概念?
隐藏类
defineProperty和proxy的区别
- defineProperty是对属性进行劫持,proxy是代理对象
- defineProperty无法监听新增和删除属性,proxy可以
- defineProperty不能监听数组下标值改变,proxy可以且不需要对数组方法进行重载
- proxy用到了Reflect,采用Reflect的原因将Object操作都变为函数行为和更好的处理错误,采用Reflect里的方法的话报错会返回false
[1]defineProperty的使用
Proxy的使用
Reflect
Reflect并不是一个函数对象,因此不可构造。Reflect对象提供了一些静态方法来操作对象,例如Reflect.get、Reflect.set、Reflect.deleteProperty等。这些方法可以用于读取、修改或删除对象的属性,并返回相应的结果。
- 为什么要有Reflect?
统一对象的操作,改为函数式调用的形式
js之Reflect 反射
TS的作用
ts是一个强类型的语言,提供静态检查,让我们在开发的时候明确数据类型,增加代码的可读性,能让我们开发的时候就给出编译错误,而js的话需要运行时才能发现
TypeScript和Flow
TypeScript基础
typescript史上最强学习入门文章(2w字)
Flow和TypeScript的区别
TS中type和interface的区别
- type可以描述基本数据类型和对象类型,可以使用联合类型和元组,interface只能描述对象类型
- interfacce可以多次声明,多次声明会合并,type多次声明会报重复定义的警告
- 都能实现继承,但采用的方式不一样,type采用是交叉类型的方式,interface采用的是extends
TS枚举类型的作用
枚举主要用在那些能确定值的变量上,比如性别,周一到周日这些,通过给这边变量设置枚举值,可以增强代码的可读性,也方便在swich和if esle中比较,因为枚举值通常是整数类型,因此处理起来会比字符串类型更加高效
TS中枚举的作用
TS泛型
- 作用:泛型可以使我们在定义函数、接口、对象的时候,不预先指定具体的类型,在使用的时候再确定类型,可以使得我们的代码更加灵活
- 泛型工具类:
- Partial:可以将T中的所有属性设置为可选
- Required:可以将T中的所有属性设置为必选
- Readonly:可以将T中的所有属性设置为只读
- Pick<T, keys>:可以从T中取出一组属性作为新的类型
- Exclude<T, keys>:可以从T属性集合中排除指定的属性
实现异常重传函数
入参fn、data、times,返回一个promise,发请求成功则resolve,失败则再调重传函数并且time减一传入,time小于0不重传
// 超时重传
const post = async () => {}
async function retryPost(url, data, times) {
let res = await post(url, data)
if (res.code === 504 && times > 0) {
res = await retryPost(url, data, times--)
}
return res
}
防抖和节流的区别和应用,实现
- 防抖:多次触发,只执行最后一次,规定时间内再次触发会重新计时,适用于短时间内的大量触发;窗口大小改变时resize事件、文本编辑器实时保存。
- 节流:规定时间内只执行第一次,适用于持续的触发;连续点击事件、监听滚动事件、百度搜索框提示
// 防抖
function debounce (fn, delay) {
let timer;
return function() {
const _this = this
const arg = arguments
if (timer) clearTimeout(timer)
timer = setTimeout(function () {
fn.call(_this, arg)
timer = null
}, delay)
}
}
// 节流
function throttle (fn, delay) {
let time = 0
return function () {
const now = Date.now()
const arg = arguments
if (now - time> delay) {
fn.call(this, arg)
time = now
}
}
}
Symbol的应用
- Symbol表示独一无二的值,可以用来做属性名,避免命名冲突,因为symbol不会出现在Object.keys中,所以可以用来模拟私有变量,并且JSON.toStringfy会忽略symbol属性名和属性值
- Symbol.for(des)函数创建的symbol会添加到全局的symbol注册中心去,并用des作为索引值,所以这种方式创建的symbol可能会相等
- ES6中对象会内置一个Symbol.iterator函数,有该函数的对象被称为可迭代对象,可以在对象使用for/of
JavaScript 原始数据类型 Symbol 有什么用?
迭代器Iterator和生成器Generator
- 实现了next()方法的对象就是迭代器对象,next方法必须返回的是对象并且包含value和done属性
- 正确实现了[Symbol.iterator]方法的对象也是可迭代对象,[Symbol.iterator]方法的返回值必须是迭代器对象,for…of的时候会用[Symbol.iterator]返回的迭代器进行遍历
- 生成器是一个函数,在函数名前面加个*标识这是生成器函数,生成器函数返回的是生成器对象,生成器对象是一个特殊的迭代器对象,所以也有next方法。执行生成器函数的时候并不会执行里面的代码,要调用next()的时候才会执行,执行遇到yield的时候停止,yield右边表达式的结果会作为value值返回,next()中传入的入参会赋值给yield左边变量
function* foo(num) { // 接受num参数5
const value1 = 100 * num
const n = yield value1 // 第一个停止点,n接受的是第二个next传递的参数 3
const value2 = 200 * n
const count = yield value2 // 第二个停止点,count接受的是第三个next传递的参数 6
const value3 = 300 * count
yield value3
return "123"
}
// 生成器上的next方法可以传递参数
const generator = foo(5) //第一个参数直接传递
// 第一个next只做启动,没有上一个yield,所以传的参数10接受不到
console.log(generator.next(10)) // { value: 500, done: false }
console.log(generator.next(3)) // { value: 600, done: false }
console.log(generator.next(6)) // { value: 1800, done: false }
console.log(generator.next(5)) // { value: 123, done: false }
深入解析 JavaScript 中的 Generator 生成器
函数柯里化&thunk&co
柯里化:接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数。柯里化其实本身是固定一个可以预期的参数,并返回一个特定的函数,处理批特定的需求。这增加了函数的适用性,但同时也降低了函数的适用范围。
thunk函数:将回调函数作为参数并且返回一个新的函数的函数
co函数:co函数是一个用于管理生成器的库,可以自行运行生成器并执行其中的异步操作
Thunk&&函数柯里化
async/await的原理
async/await是基于生成器的语法糖,使得异步代码编写更加简洁和可读。async是异步函数的声明方式,内部采用的是生成器来实现的。我们用async声明一个函数其实是相当于声明一个返回promise对象的函数,我们函数内部的执行逻辑会被封装成一个生成器对象,里面用到的await就相当于yield嘛,用来暂停等待异步操作的结果。在代码执行的时候回去自动执行生成器和处理异步操作,类似是用到co函数。这种自动执行生成器和处理异步操作的能力是已经内置到了JS语言中的
Async、Await的实现原理,你学会了吗?
如何按顺序请求
可以采用生成器,把要发送的请求放进一个数组,然后放进生成器里面去循环数组,每一个请求yield一下,生成器对象也循环调用next()并把结果存起来
// 模拟的一个异步网络请求
function asyncFn(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num + 1)
}, 1000)
})
}
const arr = [asyncFn(2), asyncFn(6), asyncFn(9)]
const res = []
function * generator (arr) {
let j = 0
for (let i of arr) {
j = yield i // j可以用来接受next的入参
}
}
async function test() {
const gen = generator(arr)
let param = 1
for (let i of arr) {
param = await gen.next(param).value
res.push(param)
}
console.log('===', res)
}
test()
如何终止一个请求
- 采用AbortController,创建AbortController实例并添加其signal的终止事件,回调函数为Promise的reject
function post(url, data, config) {
return new Promise((resolve,reject) => {
if(config.signal) {
config.signal.addEventListener('abort', reject)
}
...后续请求
}
}
const controller = new AbortController()
post('/api/hello', {message: '你好'}, { signal: controller.signal }).then(res => {}).catch(err => {})
controller.abort()
- axios已经实现了终止的功能
const source = axios.CancelToken.source();
axios.get('/api/data', {
cancelToken: source.token
}).then(response => {
console.log(response.data);
}).catch(error => {
if (axios.isCancel(error)) {
console.log('请求已被取消:', error.message);
} else {
console.log('请求出错:', error.message);
}
});
source.cancel('请求被用户取消');
认识 AbortController控制器对象 及其应用
axios 中断请求
JavaScript 中断请求几种方案详解
求和一个多维数组
function count(array) {
let sum = 0
function countArr (arr) {
arr.forEach(i => {
if (Array.isArray(i)) {
countArr(i)
} else {
sum =+ i
}
})
}
countArr(array)
return sum
}
首页如果需要加载9张图片,会有什么问题
- 会触发并发请求上限
- 解决
- 域名发散
页面上有很多按钮,会有什么问题,怎么解决?
- 性能会很差,采用事件委托
封装一个通用组件,你会考虑什么问题
- 扩展性:支持自定义插槽去拓展一些东西
- 通用性:尽可以与业务解耦,跟业务耦合越低,越通用
- 健壮性:组件本身不应该有致命的缺陷,要做好测试,多考虑一些边际情况
- 确认目的:封装一个组件,首先要考虑为了什么目的去封装的,是觉得这个页面太复杂,需要单独抽离一部分出去出来,降低我们当前页面的复杂度,这时候我们就不用考虑什么通用性的问题,只是单纯为这个页面服务,还是说很多地方都用到了同一个功能,我们希望把这个功能提取出来,如果是这样,我们就需要考虑一些通用性的问题。我们还要考虑通用是有多通用,是指在这一系列的功能中去通用,还是说在整个项目中去通用,还是说我们要跨项目去通用,我们这一个目的的不一样,或者说动机的不一样,那我们后续的设计肯定也会有所不同
- 分析边界:确认好目的之后我们就要去分析他的边界,也就是那些事情需要我们这个组件去管,哪些事情不用我们这个组件去管,而这个边界范围的大或者小完全都是取决于我们封装的目的。一般来说越通用的话,他的边界值越窄,实现的功能越小。而边界越窄,它就越灵活,越能在不同的地方去使用。但相对来说就没这个方便,这也就是为什么我们会对一些组件库的组件去进行二次封装,来针对我们的业务来提升一些便利性。其实在我们项目中封装组件的时候没有确认好边界值。然后封装好之后为了能在另一个地方用又去改代码,然后又为了在其他地方去用又改代码,改来改去就会越来越臃肿,充满了各种各样的判断,变得极难维护,屎山就是这样堆起来的,边界不清
- 设计接口:等我们确定好边界之后就可以设计接口了,就是确定我们组件的属性、事件、插槽这些,确定好我们对外交互的这些接口。如果是一些比较通用的组件的话,就还要去写我们组件的问题,像一些常用的组件库都是有对应的文档
- 代码实现:设计好之后就是进行编码实现和测试的这些东西了
vue通用组件设计原则
面试题
初级前端面试
八股文
2023,2022,2021年面试题合集
前端面试题汇总