面向对象三大特征
封装:就是把客观事物封装成抽象的类。
继承:子类存在共性的东西,把它抽取出来放到父类里面。
多态:不同类的对象对同一消息作出不同的响应。
前端中的事件流
事件流:事件流描述的是从页面中接收事件的顺序。
- 事件捕获阶段:从window传到目标节点(外层传到底层)称为捕获阶段,捕获阶段不会响应任何事件;
- 处于目标阶段:在目标节点上触发,称为目标阶段
- 事件冒泡阶段:从目标节点传回window(从内层传回外层),称为冒泡阶段。
addEventListener参数:addEventListener(event, function, useCapture)
function触发时执行的函数;useCapture是冒泡(false)还是捕获(true)执行,默认冒泡。IE只支持事件冒泡。
事件冒泡、事件捕获、事件委托
基本概念:都会进行事件传播
事件捕获: 当鼠标点击或者触发dom事件时,浏览器会从根节点 =>事件源(由外到内)进行事件传播。
事件冒泡: 事件源 =>根节点(由内到外)进行事件传播。
dom标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
**阻止冒泡的方法:**使用event.stopPropagation()。判断event.target 和 event.currentTarget是否相等。
如何让事件先冒泡后捕获
对于同一个事件,监听捕获和冒泡,分别对应着不同的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获事件。
事件委托:不在事件的发生地(直接 dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素 DOM的类型,来做出不同的响应。
如果子元素阻止了事件冒泡,那么委托也就不成立。
举例:最经典的就是 ul和 li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在 li标签上直接添加,而是在 ul父元素上添加。
减少事件的绑定,也就减少了内存的占用,提高性能。
比较适合动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。
ES6新特性
新增块级作用域(let,const)
- 新增了变量的声明方式 let 和 const
- var没有块级作用域,存在变量提升的问题,
- let和const具有块级作用域,不存在变量提升。
- const声明必须赋值,声明的变量内存地址不可改变。
新增了箭头函数
新增了set和map数据结构;
新增了定义类的语法糖(class);
新增了一种基本数据类型(symbol);
新增了解构赋值;
新增了函数参数的默认值;
对象和数组新增了扩展运算符;
Promise;
新增了模块化(import,export);
新增了generator;
给数组新增了API
Array.of()将序列转成数组;Array.from()将数组,字符串,set,类数组对象转换为数组
map,filter,reduce,find()找出第一个符合条件的数组成员。
findIndex()返回第一个符合条件的数组成员的位置,fill()初始化数组。
includes返回一个布尔值,表示数组是否包含给定值。
copyWithin(index,start,end)将start到end下标间的元素复制到下标为index的位置
let、const、var的区别
(1)块级作用域: let和const具有块级作用域,var没有。块级作用域解决了ES5中的两个问题:
- 内层变量可能覆盖外层变量。
- 用来计数的循环变量泄露为全局变量。
(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否则会报错。
(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明: var可以重复声明,后声明的会覆盖前面声明的变量。const和let不允许重复声明变量。
(5)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
(6)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
变量提升
MDN解释:“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面。变量声明总是在任意代码执行之前处理的,在代码中的任意位置声明变量总是等效于在代码开头声明。这意味着变量可以在声明之前使用,这个行为叫做“hoisting”。“hoisting”就像是把所有的变量声明移动到函数或者全局代码的开头位置。
var的变量提升是:对于var定义的变量,JavaScript先对其进行声明,并不会进行定义。
函数的变量提升是:JavaScript 中的函数声明时也进行了函数定义。你可以在函数声明之前使用该函数。
对于let 或 const 声明的变量处于“暂时性死区”。当变量处于暂时性死区之中时,其尚未被初始化,访问变量将抛出 ReferenceError
执行上下文
执行上下文:当前代码的执行环境,每当js代码在运行的时候,它都是在执行上下文中运行。
**执行栈:**当js引擎第一次遇到要执行的代码的时,首先会创建一个全局的执行上下文并压入当前执行栈,每遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈顶,js引擎执行栈顶的函数,当该函数执行完毕,执行上下文从栈中弹出,控制流程到达下一个上下文。
全局执行上下文:js引擎解析js代码时最开始遇到的就是全局代码,初始化的时候会向执行栈中压入一个全局执行的上下文,只有当整个应用程序结束的时候才会清空执行栈,栈底永远都是全局执行上下文。全局执行上下文只有一个,全局执行环境是最外围的一个执行环境,在浏览器的全局对象是 window。
函数执行上下文:可以有无数个,函数被调用的时候才会被创建。每次调用函数都会创建一个新的执行上下文。
**eval执行上下文:**很少使用
**执行上下文的三个重要属性:**变量对象、作用域链、this。
变量对象:每个执行环境都有一个与之关联的变量对象,用来存储执行上下文中定义的所有变量和函数。在执行上下文中用活动对象表示变量对象,只有当进入一个执行环境时,这个执行上下文的变量对象才会被激活,此时成为活动对象,只有活动对象上的属性才能被访问。
作用域链:当代码在一个环境中执行时,会创建变量对象的一个作用域链。 作用域链的用途,是保证对执行环境有权访问的所有变量和函数进行有序访问。
**this:**其在js中指向当前执行代码所在的对象,全局执行上下文中,指向全局对象window,函数执行上下文中,取决于函数的调用方式。对于普通函数,this指向调用函数的对象,对于箭头函数,this继承外部的执行上下文。
作用域和作用域链
代码运行时,作用域决定了变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。
(1)全局作用域:拥有全局作用域的对象可以在代码的任何地方访问到。
- 最外层函数和最外层函数外面定义的变量拥有全局作用域。
- 所有未定义直接赋值的变量由于变量提升自动声明为全局作用域。
- window对象的所有属性拥有全局作用域。
全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
**(2)函数作用域:**定义在函数中的变量拥有函数作用域,并且函数在每次调用时都有一个不同的作用域。
**(3)块级作用域:**使用ES6中新增的let和const声明的变量拥有块级作用域。
作用域链: 在当前作用域中查找所需变量,在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。
作用域链的作用是**,保证对执行环境有权访问的所有变量和函数的有序访问**
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的开始始终都是当前执行上下文的变量对象。全局执行上下文的变量对象始终是作用域链的最后一个对象。
mouseover和 mouseenter的区别
mouseover:鼠标移入元素或其子元素都会触发事件,有一个重复触发冒泡的过程。移除事件是 mouseout
mouseenter:当鼠标移入元素本身会触发事件,不会冒泡,对应的移除事件是 mouseleave
JS的各种位置
clientHeight:表示的是可视区域的高度,包含padding,不包含 border、margin和水平滚动条。
offsetHeight:clientHeight + border + 滚动条
scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。
clientTop:表示边框 border的厚度,在未指定的情况下一般为 0。
offsetTop:元素距离父元素顶端的高度。
scrollTop:滚动条被隐藏部分的高度
BOM属性和方法
BOM 是指浏览器对象模型,是把浏览器当做一个对象,定义了与浏览器进行交互的方法和接口。
window对象 :表示浏览器窗口,是JS的顶层对象。
- alert:显示带有一个“确定”按钮的对话框。
- confirm:显示带有“确定”与“取消”两个按钮的对话框。
- prompt:显示带有输入区的对话框。
- open:打开一个新窗口。
- close:关闭用户打开的窗口。
location对象:浏览器当前的URL信息。
- location.href--返回或设置当前文档的 URL
- location.search --返回 URL中的查询字符串部分。
- location.hash --返回 URL#后面的内容,如果没有#,返回空
- location.host --返回 URL中的域名部分,例如 www.dreamdu.com
- location.hostname --返回 URL中的主域名部分,例如 dreamdu.com
- location.pathname --返回 URL的域名后的部分。
- location.port --返回 URL中的端口部分。
- location.protocol --返回 URL中的协议部分。
- location.assign --设置当前文档的 URL
- location.replace() --设置当前文档的 URL,并且在 history对象的地址列表中移除这个URL
- location.reload() --重载当前页面
navigator对象:浏览器本身信息,浏览器的名称、版本、用户代理等。
history对象:浏览器的浏览历史记录信息。
- history.go() --前进或后退指定的页面数 history.go(num);
- history.back() --后退一页
- history.forward() --前进一页
screen对象:浏览器的屏幕信息。
DOM属性和方法
DOM是指文档对象模型。将网页转为JS对象,是JS操作网页的接口,用脚本进行各种操作。
- 文档:文档表示的就是整个的HTML网页文档。
- 对象:对象表示将网页中的每一个部分都转换为了一个对象。
- 模型:使用模型来表示对象之间的关系。
node类型的属性
nodeType返回节点类型,常用节点类型如下
- 节点类型 值 对应常量
- 文档节点(document) 9 Node.DOCUMENT_NODE
- 元素节点(element) 1 Node.ELEMENT_NODE
- 属性节点(attr) 2 Node.ATTRIBUTE_NODE
- 文本节点(text) 3 Node.TEXT_NODE
- 文档类型节点(DocumentType) 10 Node.DOCUMENT_TYPE_NODE
- 注释节点(Comment) 8 Node.COMMENT_NODE
- 文档片断节点(DocumentFragment) 11 Node.DOCUMENT_FRAGMENT_NODE
nodeValue返回结点的文本值。
nodeName返回节点的名称。
textContent返回当前节点和它的所有后代节点的文本内容。
nextSibling返回紧跟在后面的第一个同级节点。如果当前节点后面没有同级节点,则返回null。
previousSibling返回前面的第一个同级节点。如果当前节点前面没有同级节点,则返回null。
parentNode返回父节点,父节点类型:元素节点、文档节点和文档片段节点。
parentElement返回父元素节点。如果没有父节点,或者父节点类型不是元素节点,则返回null。
firstChild和lastChild返回第一个子节点,如果没有子节点,则返回null。last返回最后一个子节点。
childNodes返回一个类似数组的对象(NodeList集合),成员包括当前节点的所有子节点。
node类型的方法
- appendChild(node)将node作为最后一个子节点,插入当前节点。返回值为插入的子节点node。
- insertBefore(node, 参照的node)将node插入到参照结点前面,返回值为插入的子节点。
- removeChild(node)从当前节点移除该子节点,返回值为移除的子节点。
- replaceChild(node, 被替换的node)用node替换,返回值为替换走的节点。
- cloneNode(deep)返回结点的clone,deep为true表示包括子节点的克隆,为false只克隆当前结点
DOM 操作
在 Web 开发中,DOM 操作通常发生在页面渲染完成后,也就是在页面加载完成后的 JavaScript 执行阶段。具体来说,DOM 操作可以分为以下几个阶段:
- 加载阶段: 在加载阶段,浏览器会解析 HTML、CSS 和 JavaScript 文件,并构建文档对象模型(DOM)、样式对象模型(CSSOM)和 JavaScript 解释执行环境。
- 解析和构建阶段: 在解析和构建阶段,浏览器会解析 HTML 标记并将其转换成 DOM 树,同时也会解析 CSS 样式并构建 CSSOM 树。这个阶段是页面加载的初始阶段,浏览器会根据 HTML 和 CSS 的内容构建出初始的 DOM 树和 CSSOM 树。
- 渲染阶段: 在渲染阶段,浏览器会根据 DOM 树和 CSSOM 树构建渲染树(Render Tree),并将其转换成屏幕上的可视化页面。这个阶段包括布局计算和绘制操作,浏览器会根据元素的位置、大小和样式等信息确定页面的布局,并将页面的内容绘制到屏幕上。
- JavaScript 执行阶段: 在 JavaScript 执行阶段,浏览器会执行页面中的 JavaScript 代码,包括事件处理函数、定时器、异步请求等。JavaScript 代码可以访问和操作 DOM 树和 CSSOM 树,从而改变页面的结构、样式和行为。
- DOM 操作阶段: 在 JavaScript 执行阶段的一部分,开发人员可以通过 JavaScript 来操作 DOM 树,包括创建、修改、删除 DOM 元素,添加事件监听器,修改元素的样式等操作。这些 DOM 操作可以在页面加载完成后的任何时候进行,但通常建议在页面加载完成后再进行,以避免影响页面的加载性能和用户体验。
总的来说,DOM 操作通常发生在页面加载完成后的 JavaScript 执行阶段,开发人员可以在这个阶段通过 JavaScript 来操作页面的 DOM 结构,实现页面的交互和动态效果。
虚拟DOM
对虚拟DOM的理解?
虚拟DOM是对DOM的抽象,是更轻量级的 DOM描述方式。
在代码渲染到页面之前,vue会将代码转换为虚拟DOM进行缓存。在每次数据发生变化时,都会重新构建一份虚拟DOM,然后在更新时,使用vue内部封装的diff算法进行新旧虚拟DOM的比较,最终将发生变化的部分渲染到真实DOM中。一次更新多次变化可减少页面渲染的次数和修改DOM引起的重绘重排次数,提供渲染性能。
为什么要使用虚拟DOM?
为了提高前端应用的性能和开发效率。如果一次操作中有 10 次更新 DOM 的动作,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的内容保存到本地一个 JS 对象中,最终将这个 JS 对象一次性 attch 到 DOM 树上,再进行后续操作,避免大量的计算,提高了浏览器渲染效率。
虚拟DOM的解析过程
- 首先对将真实DOM数结构进行分析,将其转换为js对象形式的虚拟DOM树
- 当页面的状态发生改变,根据变更的状态,重新构建一个虚拟DOM对象,然后将这棵新的虚拟DOM和旧的虚拟DOM进行比较,记录下两者的差异。
- 最后有差异的地方应用到真正的 DOM 树中去,完成视图更新。
虚拟 DOM 实现原理?
- 创建虚拟DOM树:用 JavaScript 对象模拟真实 DOM 树,当应用状态发生改变时前端生成新的虚拟DOM树,记录变化;
- diff 算法: 比较两棵虚拟 DOM 树的差异;
- pach 算法: 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
diff算法的原理
diff整体策略:深度优先,同层比较。比较的过程中,循环从两边向中间收拢。
在新老虚拟DOM对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点。
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
箭头函数和普通函数的区别
- 箭头函数比普通函数更加简洁
- 箭头函数没有自己的this,它的this指向外层的this
- 箭头函数继承来的this指向永远不会改变,call(),apply,bind也不会改变
- 箭头函数不能作为构造函数使用
- 箭头函数的参数不能使用arguments,需要使用rest运算符(扩展运算符)
- 箭头函数没有prototype。
- 箭头函数不能用作Generator函数(ES6引入的一种新的函数类型,可在执行过程中暂停并保存当前状态),不能使用yeild关键字。
js数据类型及其检测方法
基本数据类型:
基本数据类型保存在栈内存当中,保存的就是一个具体的值。
Number,String,Boolean,null,undefined,symbol(es6),bigint(es11)
引用数据类型:
保存在堆内存中,声明一个引用类型的变量,它保存的是引用类型数据的地址
object,function,Array
检测方法
- typeof()判断基本数据类型正常,判断数组、对象、null都为object,判断NaN为Number
用法:typeof 变量
- instanceof()只能判断引用数据类型,不能判断基本数据类型
用法:变量 instanceof Array
- constructor几乎都可以判断;如果声明了一个构造函数,并把它的原型指向Array,此时判断不了
用法:(变量).constructor === Array
- Object.prototype.toString.call()
用法:Object.prototype.toString.call(变量) 返回值[object 类型]
isNaN 和 Number.isNaN 函数的区别?
- 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
- 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
call apply bind区别
都可以改变函数内部的this指向。
- call 和 apply 会调用函数,并且改变函数内部this指向。bind 不会调用函数,可以改变函数内部this指向。
- call 和 apply 传递的参数不一样,call 传递参数序列形式; apply和bind 必须数组形式。
|| 和 && 操作符的返回值?
fn1 || fn2:fn1为true,不执行fn2;fn1为false,执行fn2
fn1 && fn2:fn1为true,执行fn2;fn1为false,不执行fn2
null 和 undefined 的区别
undefined代表未定义,就是应该有值,但是没有值,连null值都没有赋予;null代表空对象,空引用;
区别:(1)null转为数值是0,undefined转为数值时为NaN;(2)null通过type of判断类型时,结果是object类型,undefined的类型是undefined类型;(3)undefined == null 返回值为false
出现undefined的场景:
- 变量被声明了,但是没有赋值时,就等于undefined。
- 调用函数时,应该提供的参数没有提供,该参数等于undefined。
- 访问一个对象中没有的属性,则打印后可以看到为undefined。
- 函数没有返回值时,默认返回undefined;
出现null的场景:
- 作为对象原型链的终点出现。
- 当我们访问一个不存在的dom节点时。
- 手动释放内存obj=null
原型、原型链的理解
在js中,每个对象都有一个关联的原型对象,他包含属性和方法,其他对象可以通过原型对象来继承这些属性和方法。每个对象都有一个prototype属性,它指向了该对象的原型,并通过Objcet.getPrototypeOf()获取。
当访问一个对象的某个属性时,如果对象本身没有定义这个属性,那么就去它的__proto__隐式原型上查找,即它的构造函数的protoType,仍然没有找的话就接着在它构造函数的prototype的__proto__上查找,这样就形成了原型链。
原型链的终点是个null。
JS中的继承
原型链继承
实现方式:将子类的原型链指向父类的对象实例
原理:子类实例child的__proto__指向Child的原型链prototype,而Child.prototype指向Parent类的对象实例,该父类对象实例的__proto__指向Parent.prototype,所以Child可继承Parent的构造函数属性、方法和原型链属性、方法。
优点:子类继承父类构造函数的属性,父类原型的属性。 缺点:子类无法向父类构造函数传参,所有实例共享父类实例的属性。
构造函数继承
实现方式:在子类构造函数中使用call或者apply劫持父类构造函数方法,并传入参数
原理:使用call或者apply更改子类函数的作用域,使this执行父类构造函数,完成父类共有属性继承
优点:可解决原型链继承的缺点 缺点:不可继承父类的原型链方法
组合继承
综合使用构造函数继承和原型链继承
优点:可继承父类原型上的属性,且可传参;每个新实例引入的构造函数是私有的 缺点:会执行两次父类的构造函数,消耗较大内存,子类的构造函数会代替原型上的那个父类构造函数
原型式继承
原理:类似Object.create,用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象,结果是将子对象的__proto__指向父对象
缺点:共享引用类型
ES6的class类继承
new操作符做了哪些事情
- 创建一个空对象。
- 改变隐式指针的指向。将对象的__proto__属性指向构造函数的prototype属性。
- 执行构造函数,并将该对象绑定到构造函数的this
- 返回新的对象。
防抖和节流
防抖:时间触发后的一定时间内没有再次触发则执行
场景:频繁和服务端交互,输入框的自动保存事件,浏览器resize
// 获取页面中的一个输入框元素
let telInput = document.querySelector('input')
// 给输入框元素添加一个 input 事件的监听器,触发事件时调用防抖函数
// 参数 demo 是要执行的函数,2000 是等待的时间间隔(毫秒)
telInput.addEventListener("input", antiShake(demo, 2000))
// 封装防抖函数
function antiShake(fn, wait) {
let timeout = null // 定时器标识
// 返回一个函数作为事件处理函数
return function(...args) {
// 点击后取消上次定时器
clearTimeout(timeout)
// 设置一个新的定时器,在 wait 毫秒后执行传入的函数
timeout = setTimeout(() => {
fn.apply(this, args) // 执行传入的函数,args 是参数列表
}, wait)
}
}
// 定义要执行的函数,示例中为发起请求
function demo() {
console.log("发起请求")
}节流:每隔一段时间,只执行一次函数。
场景:搜索框,滚动加载,高频点击提交
// 定义一个函数,用于发起请求
function demo() {
console.log("发起请求");
}
// 获取页面中的一个类名为 'box' 的元素
let box = document.querySelector('.box');
// 添加事件监听器,当触发 touchmove 事件时调用节流函数
box.addEventListener("click", throttle(demo, 2000));
// 节流函数
function throttle(event, time) {
let timer = null; // 定时器标识
// 返回一个函数作为事件处理函数
return function() {
// 如果定时器不存在,则执行事件并设置定时器
if (!timer) {
event(); // 执行事件
timer = setTimeout(() => {
timer = null; // 定时器结束后重置
}, time);
}
}
}浅拷贝,深拷贝
- 浅拷贝和深拷贝都只针对于引用数据类型,
- 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;
- 但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改原对象;
深拷贝方法
使用 JSON 序列化和反序列化实现深拷贝
缺点: 1,忽略值为undefined和symbol的属性。
2,不会对函数,正则new RegExp()、newError('error')进行拷贝。
3,NaN、Infinity和-Infinity会变成null。
使用递归函数
js 延迟加载的方式有哪些
- 将 js 脚本放在文档的底部,来使 js 脚本在最后加载执行。
- 使用 setTimeout 延迟方法**:** 设置一个定时器来延迟加载js脚本文件。
- 给 js 脚本添加 async 属性,使脚本异步加载,不会阻塞页面的解析过程,当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。执行顺序是不可预测的。
- 给 js 脚本添加 defer 属性,会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行脚本文件,这样的话就能使页面的渲染不被阻塞。
- 动态创建 DOM 标签,对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 来引入 js 脚本。
setTimeout、setInerterval和requestAnimationFrame
最小执行时间
setTimeout最小执行时间是4ms;多久后执行;使用clearTimeout()清除。
setInterval最小执行时间是10ms;每隔多久执行;使用clearInterval()清除。
三者区别
- setTimeout/ setInterval 设定的时间并不精确,它们只是在设定的时间后将相应任务添加到任务队列中,而任务队列中如果还有前面的任务尚未执行完毕,那么后添加的任务就必须等待,这个等待的时间造成了原本设定的动画时间间隔不准。
- requestAnimationFrame不需要设置时间间隔,采用的是系统时间间隔(约16.7ms)
// requestAnimationFrame是一个JavaScript函数,它可以让浏览器在下一次重绘之前调用指定的函数。
// 这样可以让动画效果更加流畅,并且还可以节省系统资源。
// 在使用requestAnimationFrame时,浏览器会根据当前系统的性能来调整动画的刷新频率。
function animate(){requestAnimationFrame ( animate);}
requestAnimationFrame( animate);
// 请求动画帧后,浏览器会调用animate()函数,然后在动画完成后再次请求动画帧。
// 这样动画就能不断重复渲染,形成动画效果。如果窗口或标签不可见,浏览器可能会暂停动画。
// 对应的,还有一个cancelAnimationFrame来取消之前设定的动画帧请求。==和===、以及 Object.is的区别
- ==先检查两个操作数的数据类型,如果相同,就进行 === 的比较,如果不同,则进行一次number类型转换,转为相同类型后再进行比较。
" "==0 //true
"0"==0 //true
" "=="0" //false
123=="123" //true
null==undefined //true
"" == 0//true
NaN == NaN//false
+0 == -0 //true
null == false//false
true == null//true
undefined == 0//false- ===进行比较时,如果类型不同,直接就是false
如果类型不同,就一定不相等
如果两个都是数值,并且是同一个值,那么就相等。如果其中一个至少为NaN,那么就不相等(这里判断一个值是否是NaN,使用isNaN()来判断)
如果两个都是字符串,每个位置的字符都一样,那么相等,否则不相等
如果两个值都是true,或是false就相等
如果两个值都是引用同一个对象或是函数,那么相等,否则不相等
如果两个值都是null,或是undefined,那么相
- 等
- javascript
" "===0 //false " "==="0" //false 123==="123" //false null===undefined // false "" === 0//true NaN === NaN//false +0 === -0 //true - Object.js判断方式与===基本相同
- javascript
Object.is(NaN,NaN)//true Object.is(-0,+0)//false JS中关于this指向的问题
- 全局对象中的this指向指向的是window
- 全局作用域或者普通函数中的this指向全局window
- 在不是箭头函数的情况下,this永远指向最后调用它的那个对象
- new 关键词改变了this的指向
- apply,call,bind可以改变this指向,不能改变箭头函数this指向。
- 箭头函数中的this,它的指向在定义的时候就已经确定了,外层函数的this
- 匿名函数中的this指向了window
对this对象的理解
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
- 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
- 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
- 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
- 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
操作数组的方法有哪些?
- 数组和字符串转换方法:toString()、toLocalString()、join()。join() 方法可以指定转为字符串时的分隔符。
- 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
- 数组首部操作的方法 shift()删除 和 unshift()插入 。重排序的方法 reverse() 和 sort()
- 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
- 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
- 数组插入方法 splice(),影响原数组。查找特定项的索引的方法,indexOf() 和 lastIndexOf()。
- 数组迭代方法 every()对每个元素进行判断,都满足返回true,否则为false、some()与every方法对应,有满足条件的返回、filter()、map() 和 forEach() 方法。
- 数组归并方法 reduce() 和 reduceRight() 方。
- 数组删除和替换:splice(index, num, 要替换的新元素)从index下标开始。
- 数组拍平:flat()
push() pop() sort() splice() unshift() shift() reverse() concat() join() map() filter()ervery() some() reduce() isArray() findIndex() flat()
其中push() pop() unshift() shift() sort() reverse() splice()会改变原数组
类数组和数组
数组是一个特殊对象,与常规对象的区别:
- 当由新元素添加到列表中时,自动更新length属性
- 设置length属性,可以截断数组
- 从Array.protoype中继承了方法
- 属性为'Array'
类数组是一个拥有length属性,并且他属性为非负整数的普通对象,类数组不能直接调用数组方法。常见的类数组:函数的参数arguement, DOM方法的返回结果
- Array.from()
- Array.prototype.splice.call(arrayLike, 0);
- Array.prototype.concat.apply([], arrayLike);
- Array.prototype.slice.call(arrayLike);
ES6模块与CommonJS有什么异同?
- 引用形式:CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const;
- 加载时机:commonJS 模块是运行时加载,ES6 模块是在编译时确定依赖关系的;
- 引用写法:CommonJs导入的模块路径可以是一个表达式,因为它使用的是require()方法;ES6模块使用import,模块路径只能是字符串;
- 使用场景:浏览器环境使用ES Modules,nodejs使用CommonJS;
共同点:
- CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。
js 的几种模块规范?
js 中现在比较成熟的有四种模块加载方案:
第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。
第三种是 CMD 方案,CMD (Common Module Definition) 规范是为了实现按需加载模块,它是异步加载模块的。当模块被请求时,它将被启动并在完成后返回给调用者。这是通过异步的方式实现的,不会阻塞浏览器的主进程。
第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
AMD 和 CMD 规范的区别?
- 第一个方面是在模块定义时对依赖的处理不同。AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
- 第二个方面是对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,它们的区别在于模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD 在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句 的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。
import 和 require区别
import和require都是用来引入模块的方式,但是它们有以下区别:
- import是ES6中的语法,而require是CommonJS中的语法。
- import是静态引入,即在编译时就会执行,而require是动态引入,即在运行时才会执行。
- import只能引入ES6模块,而require可以引入CommonJS模块、AMD模块和ES6模块。
- import 只能在浏览器中使用模块化时使用,而在 Node.js 中,通常使用 require 来导入模块。
- import是基于Promise实现的,而require是基于回调函数实现的。
- import可以使用解构赋值,而require不支持解构赋值。
XMLHttpRequest、ajax、axios、fetch
XMLHttpRequest
XMLHttpRequest发起get无参请求(原生方式)
主要实现步骤: 1、创建异步对象 2、调用open函数 3、调用send函数 监听load事件
AJAX Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是一种用于创建异步网络请求的技术,基于 XMLHttpRequest 实现。它是一种在无需重新加载整个网页的情况下,实现网页的局部更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。
ajax发起网络请求的步骤
1.建立XMLHttpRequest对象;
2.设置回调函数;
3.配置请求信息,(如open,get方法),使用open方法与服务器建立链接;
4.向服务器发送数据;
5.在回调函数中针对不同的响应状态进行处理;
其缺点如下:
- 本身是针对MVC编程,不符合前端MVVM的浪潮
- 基于原生XHR开发,XHR本身的架构不清晰
- 不符合关注分离(Separation of Concerns)的原则
- 配置和调用方式非常混乱,而且基于事件的异步模型不友好。
Fetch 是在ES6出现的API,使用了ES6中的promise对象。Fetch是基于promise设计的,是XMLHttpRequest的替代品。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
fetch的优点:
- 语法简洁,更加语义化
- 基于标准 Promise 实现,支持 async/await
- 更加底层,提供的API丰富(request, response)
- 脱离了XHR,是ES规范里新的实现方式。
- 取消请求采用AbortController的abort方法
fetch的缺点:
- fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
- fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
- fetch没有办法原生监测请求的进度,而XHR可以
Axios Axios 是一种基于Promise封装的网络请求库,是基于XMLHttpRequest的二次封装,
特点:浏览器端发起XMLHttpRequests请求;node端发起http请求;支持Promise API;监听请求和返回;对请求和返回进行转化;取消请求使用CancelToken.source()工厂函数和使用CancelToken构造函数;自动转换json数据;客户端支持抵御XSRF攻击。
闭包
闭包的理解
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,**创建的函数可以访问到当前函数的局部变量,**因为内部函数作用域链中包括外部函数的作用域。
闭包的原理
外部函数调用时,会创建相应的作用域链,函数执行完毕,其作用域链销毁, 内部函数的作用域链仍然在引用这个活动对象,内部函数将外部函数的活动对象加到自己的作用链中,只有内部函数被销毁后,活动对象才会被销毁。
闭包的特点
- 让外部访问函数内部变量变成可能。
- 变量会常驻在内存中。
- 可以避免使用全局变量,防止全局变量污染。
闭包的好处和坏处
- 好处:可以读取其他函数内部的变量,并将其一直保存在内存中。
- 坏处:可能会造成内存泄漏或溢出。闭包会使得函数中的变量都被长久保存在内存中,增加内存使用量。滥用闭包可能导致内存泄漏(不再用到的内存,没有及时释放,就叫做内存泄漏)。
闭包有两个常用的用途
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以在外部调用闭包函数,从而访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
闭包实现一个函数让函数只执行一次
function once(fn) {
let executed = false;
return function(...args) {
if (!executed) {
executed = true;
return fn.apply(this, args);}}}谈谈为什么js是单线程
- JavaScript之所以是单线程,是因为它的设计初衷是在浏览器中执行脚本。在Web页面中,JS主要用于处理用户交互、更新DOM等操作,这些操作都需要对页面进行修改。如果JS是多线程的话,那么多个线程就可以同时修改同一个页面,这样会导致页面的状态会变得混乱不堪。
- 因此,JS被设计为单线程的,这样就可以保证页面的状态是一致的。当JS执行某个任务时,其他任务必须等待它执行完毕后才能执行。这种模型被称为“事件循环”,也就是说,JS不断地从消息队列中取出任务执行,然后再取下一个任务执行,直到队列中的任务全部执行完毕。这种模型虽然会导致JS的执行效率不高,但是却可以保证页面的稳定性和一致性。
事件循环
事件循环又叫事件队列,指的是js代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则。
同步任务:同步任务是阻塞的,代码按照从上到下的顺序依次执行,同步任务在主线程上排队执行,每个任务必须等待上一个任务执行完成后才能执行。
异步任务:代码执行时不会阻塞程序的执行。异步任务不在主线程,而是在任务队列中,只有主线程中的任务全部执行完毕,才会在任务队列中按顺序取出任务放入主线程再执行。 JS异步任务分为宏任务和微任务,
宏任务放在宏任务队列,微任务放在微任务队列,等待主线程中同步任务执行完毕再进行处理。
宏任务:script 脚本的执行、setTimeout、setInterval、事件监听器、UI渲染、网络请求的回调函数
微任务:Promise的处理过程,其回调函数、对 Dom 变化监听的 MutationObserver、node中的process.nextTick
为什么要分微宏任务:
- 微任务是线程之间的切换,速度快。不用进行上下文切换,可以快速的一次性做完所有的微任务。
- 宏任务是进程之间的切换,速度慢,且每次执行需要切换上下文。因此一个Eventloop中只执行一个宏任务。
- 而区分微任务和宏任务的根本原因是为了插队。由于微任务执行快,一次性可以执行很多个,在当前宏任务执行后立刻清空微任务可以达到伪同步的效果,这对视图渲染效果起到至关重要的作用。
整个事件循环过程
- 执行主线程上的同步任务
- 遇到同步代码,立即执行
- 遇到宏任务,放入到宏任务队列里
- 遇到微任务,放入到微任务队列里
- 执行完所有同步代码
- 执行微任务代码
- 微任务代码执行完毕,本次队列清空
反复直到清空所以宏任务,这种不断重复的执行机制,就叫做事件循环。
Event Loop 与 JS 的运行机制
当代码执行到setTimeout/setInterval时,其实是JS引擎线程通知定时触发器线程,间隔一个时间后,会触发回调事件, 而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中。
当代码执行到XHR/fetch时,其实是JS引擎线程通知异步http请求线程,发送一个网络请求,并制定请求完成后的回调事件, 而异步http请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。
当同步任务执行完,JS引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,若是有就会加入到执行栈中交给JS引擎线程执行。
内存分类
在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间。
代码空间:用来存放可执行代码
栈空间:一块连续的内存区域,容量较小,读取速度快,被设计成先进后出结构
堆空间:不连续的内存区域,容量较大,用于储存大数据,读取速度慢
- 栈内存(Stack Memory):
- 栈内存用于存储基本数据类型(例如数字、布尔值、字符串)和引用数据类型(例如对象、数组、函数)的地址引用。
- 栈内存中的数据具有固定的大小,并且在作用域结束时自动释放,因此称为自动分配和自动释放内存。
- JavaScript 中的函数调用和变量声明都会在栈内存中创建相应的执行上下文(execution context)和变量对象(variable object)。
- 栈内存中的数据具有固定的大小,并且在函数执行结束时自动释放,因此栈内存也被称为自动分配和自动释放内存。
- 堆内存(Heap Memory):
- 堆内存用于存储动态分配的对象,例如通过 new 关键字创建的对象、数组等引用类型的数据。
- 堆内存中的数据没有固定的大小,可以根据需要动态地分配和释放,需要手动管理内存的分配和释放。
- JavaScript 中的对象、数组等引用类型的数据通常存储在堆内存中,其生命周期由垃圾回收器(Garbage Collector)来管理。
宏任务和微任务举例
执行顺序: 先宏后微
在执行 宏任务 之前,要先把已入队的 微任务 全部执行完,再执行下一条 宏任务
宏任务(发起者:宿主(Node、浏览器)):script(整体代码)、setTimeout、setInterval、setImmediate(Node.js 环境)、UI事件、I/O(Node.js)
微任务(发起者: JS引擎):Promise、MutaionObserver、process.nextTick(Node.js)
宏任务(macrotask)和微任务(microtask)是在事件循环(Event Loop)中常见的概念,它们分别代表了不同类型的异步任务。下面是它们的一些典型例子:
宏任务(macrotask):
- 定时器任务:setTimeout、setInterval、setImmediate(Node.js 环境中)等。
- I/O 操作:文件读写、网络请求等。
- UI 渲染:浏览器在渲染页面时产生的任务,如重绘、重排等。
- 用户交互事件:鼠标点击、键盘输入等。
- 脚本执行:整体的 script 脚本任务。
微任务(microtask):
- Promise 回调函数:Promise 的 then、catch、finally 方法。
- MutationObserver 回调函数:监视 DOM 变化的回调函数。
- process.nextTick(Node.js 环境中):在事件循环的当前阶段结束之后立即执行的回调函数。
- Object.observe(已废弃):监视 JavaScript 对象变化的回调函数。
宏任务和微任务之间的执行顺序是微任务优先于宏任务,即在一个宏任务执行完毕后,会立即执行所有微任务,然后再执行下一个宏任务。这种顺序保证了微任务能够优先于宏任务执行,从而更快地更新状态和响应用户操作。
sleep 函数
在 JavaScript 中,通常情况下是没有提供类似于其他编程语言中的 sleep 函数的,因为 JavaScript 是单线程的,而且其运行环境(例如浏览器、Node.js)也不会提供类似于线程休眠的功能。
但是,你可以使用 setTimeout 函数来实现类似的效果。setTimeout 函数可以在指定的时间后执行一个函数。你可以创建一个函数,在其中使用 setTimeout 来延迟执行,从而达到类似于休眠的效果。
下面是一个示例代码,演示了如何使用 setTimeout 来实现一个简单的 sleep 函数:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用示例
console.log('开始执行');
sleep(2000).then(() => {
console.log('2 秒后执行');
});在这个示例中,sleep 函数接受一个毫秒数作为参数,并返回一个 Promise 对象。在 sleep 函数内部,使用 setTimeout 来延迟 resolve 函数的执行,从而实现了延迟执行的效果。在使用 sleep 函数时,可以通过 then 方法来指定延迟结束后要执行的操作。
异步请求
异步请求是指在发起请求后,程序不会立即等待请求完成,而是继续执行后续的代码,等待请求完成后再执行相应的回调函数或者处理结果的代码。在 JavaScript 中,常见的异步请求方式包括:
- XMLHttpRequest (XHR): XMLHttpRequest 是 JavaScript 中用于进行 HTTP 请求的原生对象。它可以实现异步请求,并提供了多种事件和方法来处理请求过程和结果。通常使用它来实现 AJAX 请求。
- Fetch API: Fetch API 是用于替代 XMLHttpRequest 的新一代网络请求 API,提供了更简洁、强大和灵活的接口来发起网络请求。它基于 Promise,支持异步请求和处理响应。
- Axios: Axios 是一个流行的第三方库,用于在浏览器和 Node.js 中发送 HTTP 请求。它提供了简洁的 API、更好的错误处理机制、拦截器等功能,支持 Promise API,可以方便地进行异步请求和处理响应。
- 使用框架提供的工具函数:许多前端框架(如 Vue.js、React 等)提供了自己的网络请求工具,例如 Vue.js 提供了 axios 插件,React 提供了 fetch 方法等,可以方便地进行异步请求操作。
无论使用哪种方式,异步请求都是在后台发起,不会阻塞 JavaScript 的执行,可以提高页面的响应速度和用户体验。通常,在异步请求的回调函数中处理请求的结果,例如更新页面内容、执行其他操作等。
异步编程的实现方式?
JavaScript中的异步机制可以分为以下几种:
- async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
- Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但有时会造成多个 then 的链式调用,使代码的语义不够明确。
- 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于维护。
- generator 的方式,它在遇到异步函数执行时,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。需要关注的问题是何时将函数的执行权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
对Promise的理解
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理,他的出现大大改善了异步编程的困境,避免了回调地狱。
(1)Promise的实例有三个状态:Pending(进行中)Resolved(已完成)Rejected(已拒绝)
当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、任务失败了变成Rejected。
(2)Promise的实例有两个过程:
- pending -> fulfilled : Resolved(已完成)
- pending -> rejected:Rejected(已拒绝)
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
Promise的特点:
- 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
- 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从pending变为fulfilled,从pending变为rejected。这时就称为resolved(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。
Promise的缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
总结: Promise 对象是异步编程的一种解决方案,是ES6提供的一个构造函数,可以使用Promise构造函数new一个实例。Promise构造函数接收一个函数作为参数,这个函数有两个参数,分别是两个函数 resolve和reject,resolve将Promise的状态由等待变为成功,将异步操作的结果作为参数传递过去;reject则将状态由等待转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去。实例创建完成后,可以使用then方法分别指定成功或失败的回调函数,也可以使用catch捕获失败,then和catch最终返回的也是一个Promise,所以可以链式调用。实例的状态只能由 pending 转变 fulfilled 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
注意: 在构造 Promise 的时候,构造函数内部的代码是立即执行的。
promise的方法
对象方法:
- finally方法无论是resolve还是reject,finally都会执行
- then方法,promise进行resolve时回调then方法,并执行resolve里面的方法
- catch方法,promise进行reject时回调catch方法,并执行reject里面的方法
类方法
- resolve和reject方法
- all方法,创建多个promise,参数为由promise对象组成的数组(不是promise强转为promise),并且按顺序执行每个promise,所有的promise都变成fulfilling时返回值为promise执行结果组成的数组;只要碰到一个promise为rejected,那么整个返回值为rejected,执行reject。(希望按顺序执行时使用)
- allSettled方法(ES10,2019),与all方法对应,无论是fulfilled还是rejected,都会有最终状态,并且这个promise的结果一定是fulfilled,返回值为对象数组。
- race方法(es6,2015),参数为promise组成的数组,谁先执行完返回谁。
- any方法(es12,2021),参数为promise组成的数组,只要有一个fulfilled整体就是fulfilled,所有的都是rejected,整体才是rejected
对async/await 的理解
async函数是generator 和 promise 实现的一个自动执行的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于声明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
async返回一个promise对象,其必须等到内部所有的 await 命令的 Promise 执行完,才会发生状态改变。async内部return值会成为then方法回调函数的参数,
当 async 函数中只要一个 await 出现 reject 状态,则后面的 await 都不会被执行。
解决办法:可以添加 try/catch
相较于 Generator,Async 函数的改进在 于下面四点:
- 内置执行器。Generator 函数的执行依靠执行器,而 Aysnc 自带执行器,调用方式跟普通函数的调用一样。
- 更好的语义。async 和 await 相较于 * 和 yield 更加语义化
- 更广的适用性。co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise对象。而 async 函数的 await 命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作)
- 返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用
async/await对比Promise的优势
- 代码读起来更加同步,Promise虽然摆脱了回调地狱,但有时会造成多个 then 的链式调用,使代码的语义不够明确。
- Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅。
- 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余。
- 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。
promise和 async await 区别
都是用来处理异步的
Promise
- es6,异步编程的一种解决方案,Promise好比容器,存放着异步事件执行完毕的结果,而这些结果一旦生成是无法改变的。
- promise是返回对象使用then,catch方法去处理和捕获异常,书写是链式,容易造成代码重叠,不好维护,async await 是通过try catch进行捕获异常
- 非阻塞
async await
- es7,异步编程的一种解决方案,基于Promise实现,遵循的是Generator 函数的语法糖,返回值是一个Promise对象。
- async await最大的优点就是能让代码看起来像同步一样,只要遇到await就要等待await执行结束后,再执行后边的部分,promise.then()的方式返回,会出现请求还没返回,就执行了后面的操作。
- 非阻塞
垃圾回收
垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
- Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续到页面卸载;而局部变量的生命周期从函数执行开始,直到函数执行结束,此时这些局部变量不再被使用,它们所占有的空间也会被释放。
- 当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
垃圾回收的方式
1)标记清除:****算法把“对象是否不再需要”简化为“对象是否可以获得”。算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始(在JS中就是全局对象)扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
此算法可以分为两个阶段,一个是标记阶段(mark),一个是清除阶段(sweep)。
标记阶段,垃圾回收器会从根对象开始遍历。每一个可以从根对象访问到的对象都会被添加一个标识
清除阶段,垃圾回收器会对堆内存从头到尾进行线性遍历,如果发现有对象没有标识,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作
通常来说,在使用标记清除算法时,未引用对象并不会被立即回收。垃圾对象将一直累计到内存耗尽为止。当内存耗尽时,程序将会被挂起,垃圾回收开始执行。
缺陷:无法从根对象查询到的对象都将被清除;可能会造成大量的内存碎片。
**2)引用计数:**把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
- 这种方式在对象被创建时初始化一个引用计数器,每当有新的引用指向该对象时,计数器加一,当引用失效时,计数器减一。当引用计数器归零时,代表该对象不再被引用,可以被回收。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
缺陷:循环引用的问题:例如: obj1和obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1和obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
Chrome V8 垃圾回收算法
Chrome 浏览器所使用的 V8 引擎就是采用的分代回收策略。目的是通过区分「临时」与「持久」对象;多回收「临时对象区」(新生代younggeneration),少回收「持久对象区」(老生代 tenured generation),减少每次需遍历的对象,从而减少每次垃圾回收的耗时。
减少垃圾回收
- 对数组进行优化: 在清空一个数组时,选择使用为数组长度赋0值,依次来清空数组。
- 对object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null。
- 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
内存泄漏
不再用到的内存,没有及时释放,就叫做内存泄漏
四种情况会造成内存的泄漏:
- 意外的全局变量: 使用未声明的变量,意外的创建了一个全局变量,这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 不规范的使用闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
设计模式有哪些
**设计模式(Design pattern)**是在软件开发中经常遇到的一些常见问题的解决方案。它们提供了一套通用的、可重用的解决方案
常用的设计模式
- 单体模式:单体是一个用来划分命名空间并将一批相关的属性和方法组织在一起的对象,如果他可以被实例化,那么他只能被实例化一次。
- 工厂模式:提供创建对象的接口,意思就是根据领导(调用者)的指示(参数),生产相应的产品(对象)
- 单例模式:单例模式定义了一个对象的创建过程,此对象只有一个单独的实例,并提供一个访问它的全局访问点。也可以说单例就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
- 观察者模式(发布订阅模式): 定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新,也被称为是发布订阅模式。
- 策略模式:策略模式指的是定义一些列的算法,把他们一个个封装起来,目的就是将算法的使用与算法的实现分离开来。说白了就是以前要很多判断的写法,现在把判断里面的内容抽离开来,变成一个个小的个体
- 模板模式:定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 通俗的讲,就是将一些公共方法封装到父类,子类可以继承这个父类,并且可以在子类中重写父类的方法,从而实现自己的业务逻辑
- 代理模式:代理模式的含义就是帮别人做事,javascript的解释为:把对一个对象的访问, 交给另一个代理对象来操作.
- 外观模式: 外观模式是很常见。其实它就是通过编写一个单独的函数,来简化对一个或多个更大型的,可能更为复杂的函数的访问。也就是说可以视外观模式为一种简化某些内容的手段
use strict是什么意思 ? 使用它区别是什么?
use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:
- 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;
- 消除代码运行的不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的 Javascript 做好铺垫。
区别:
- 禁止使用 with 语句。
- 禁止 this 关键字指向全局对象。
- 对象不能有重名的属性。
严格模式和非严格模式
创建变量:未使用关键字var等声明的变量会报ReferenceError。非严视为全局变量。
delete删除变量:delete变量出错,抛出SyntaxError。非严静默失败。
变量名:使用保留字命名变量会抛出SyntaxError。非严正常使用
对象属性重名:报错。非严不会报错,访问的属性为第二个属性
函数参数重名:报错。非严不会报错,通过参数名只能访问第二个参数,需要使用arguement访问第一个参数
函数体的argument:参数和argument独立。非严修改参数会反应到argument上
在if语句中声明函数:报错。非严正常。
eval中的局部变量:可以接受eval的结果,但不能在外面使用eval中的变量。非严均可以
this指向:指向全局的this为null。非严为全局对象。
强类型语言和弱类型语言的区别
强类型语言:强类型语言也称为强类型定义语言,是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。Java和C++等语言都是强制类型定义的,也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了(不能隐式转换)。例如你有一个整数,如果不显式地进行转换,你不能将其视为一个字符串。
弱类型语言:弱类型语言也称为弱类型定义语言,与强类型定义相反。简单理解就是一种变量类型可以被忽略的语言。JavaScript是弱类型语言,在JavaScript中就可以将字符串'12'和整数3进行连接得到字符串'123',在相加的时候会进行强制类型转换。
两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。
解释性语言和编译型语言的区别
解释型语言:使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。其特点总结如下
- 解释型语言每次运行都需要将源代码解释称机器码并执行,效率较低;
- 只要平台提供相应的解释器,就可以运行源代码,所以方便源程序移植;
- JavaScript、Python等属于解释型语言。
编译型语言:使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成机器码,并包装成该平台所能识别的可执行文件。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后要再运行时,直接使用编译结果即可。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:
- 一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;
- 与特定平台相关,一般无法移植到其他平台;
- C、C++等属于编译型语言。
两者主要区别在于: 编译型语言源程序编译后即可在该平台运行,解释型语言是在运行期间才编译。所以编译型语言的运行速度快,解释型语言的跨平台性好。
图片懒加载
主要原理:监听图片资源容器是否出现在视口区域内,来决定图片资源是否被加载。
技术核心:如何判断元素处于视口区域之内。
scroll时间+定时器+getBoundingClientRect()
- 给目标元素指定一张占位图,将真实的图片链接存储在自定义属性中;
- 监听与用户滚动行为相关的 scroll 事件(需要采用节流方式)(节流时使用requestAnimationFrame代替setTimeout可以增加流畅度);
- 在 scroll 事件处理程序中利用 Element.getBoundingClientRect() 方法判断目标元素与视口的交叉状态;getBoundingClientRect()返回元素大小和目标元素左上角坐标与网页左上角坐标的偏移值。再结合视口的高度和宽度,判断元素是否出现在视口区域。
- 当目标元素与视口的交叉状态大于0时,将真实的图片链接赋给目标元素 src属性。
IntersectionObserver 接口:它可以异步监听目标元素与其祖先或视窗的交叉状态。这个接口是异步的,它不随着目标元素的滚动同步触发,它并不会影响页面的滚动性能。IntersectionObserver 构造函数接收两个参数,回调函数以及配置项。
具体实现,创建一个IntersectionObserver实例,这个实例会传递一个包含 IntersectionObserverEntry 对象的数组,判断每个对象的intersectionRatio属性,大于0则在视口内,小于零则不在视口。observer.observe(oBox)添加监听元素。observer为IntersectionObserver实例
- intersectionRatio为intersectionRect 与 boundingClientRect 的比例值
- boundingClientRect:目标元素的矩形区域信息,
- intersectionRect:祖先元素与目标元素相交区域信息
- time:返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳;
- target:目标元素;
- rootBounds:祖先元素的矩形区域信息;
- isIntersecting:目标元素是否与祖先元素相交。
IntersectionObserver实例方法
- observe:开始监听一个目标元素;
- unobserve:停止监听特定的元素;
- disconnect:使 IntersectionObserver 对象停止监听工作;
- takeRecords:为所有监听目标返回一个 IntersectionObserverEntry 对象数组并且停止监听这些目标。
TypeScript和JavaScript的区别
1.静态类型系统
引入静态类型系统,允许开发者在编码过程种定义向量、函数参数和返回值的类型。使得在开发过程种捕获常见的错误,提高代码质量。
2.编译时类型检查
会在代码编译进行类型检查,发现潜在的类型错误并提供相应的错误信息。
3.增强的编辑器支持
4.更好的代码组织和可维护性
其引入了类型系统,使得代码更易于维护。
TypeScript的特性
- 静态类型系统: TypeScript 引入了静态类型系统,允许在声明变量、函数和对象时指定类型。这样可以在编译时发现类型错误,提高代码的可靠性和可维护性。
// 定义变量时指定类型
let message: string = "Hello, TypeScript!";
let age: number = 30;
// 使用静态类型,编译时会检查类型错误
// age = "thirty"; // Error: Type '"thirty"' is not assignable to type 'number'.- 类和接口: TypeScript 支持类和接口的概念,使得面向对象编程更加方便。开发者可以使用类来创建对象,并通过接口定义对象的属性和方法,提高了代码的组织和可读性。
// 定义接口
interface Person {
name: string;
age: number;
}
// 实现接口
class Student implements Person {
constructor(public name: string, public age: number) {}
}
let student = new Student("Alice", 20);
console.log(student.name); // Output: Alice- 泛型:TypeScript 具有泛型(Generic)支持,开发者可以编写具有通用性的代码,增加代码的灵活性和可复用性。
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("Hello, TypeScript");- 类型推断: TypeScript 能够
- 根据上下文自动推断变量和表达式的类型,减少了显式类型声明的需要,提高了代码的简洁性和可读性。
// TypeScript 根据上下文推断变量类型
let x = 10; // TypeScript 推断 x 是 number 类型
x = "hello"; // Error: Type '"hello"' is not assignable to type 'number'.- **枚举:**允许开发者定义一组命名的常量值。枚举提供了一种更好地描述代码意图的方式,增强了代码的可读性。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
let userDirection: Direction = Direction.Up;
console.log(userDirection) //UP- 命名空间:TypeScript 支持命名空间(Namespace),开发者可以将代码组织成逻辑上相关的模块,避免全局命名冲突。
namespace Geometry {
export interface Vector2D {
x: number;
y: number;
}
export function calculateLength(v: Vector2D) {
return Math.sqrt(v.x ** 2 + v.y ** 2);
}
}
let vector: Geometry.Vector2D = { x: 3, y: 4 };
let length = Geometry.calculateLength(vector);- 模快化:TypeScript 支持模块化编程,允许将代码分割成多个模块,提高了代码的组织性和可维护性。开发者可以使用 import 和 export 关键字来管理模块之间的依赖
- 关系。
// 定义模块
module Math {
export function add(x: number, y: number): number {
return x + y;
}
}
// 使用模块
import { add } from "./Math";
console.log(add(1, 2)); // Output: 3- 异步编程支持:TypeScript 对异步编程提供了良好的支持,包括异步函数、Promise、async/await 等语法。
async function fetchData() {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
}
fetchData().then(data => {
console.log(data);
});