200、Js中的requestAnimationFrame
- requestAnimationFrame是一个 JavaScript 函数,用于优化动画效果的实现。它的调用频率通常为每秒60次,可以在浏览器每个绘制帧之前执行回调函数,以确保动画的流畅性和响应速度,并避免了使用 setTimeout 或 setInterval 时可能导致的性能问题。
- 与 setTimeout 和 setInterval 相比, requestAnimationFrame可以更精确地控制动画的播放时间,从而保证动画的流畅性和响应速度。
setInterval与setTimeout是在指定的时间间隔内重复执行一个操作,不会考虑浏览器的重绘,是按照指定的时间间隔执行回调函数,可能会被延迟执行,从而影响动画的流畅度。 - requestAnimationFrame 的回调函数只有在浏览器下一次绘制之前才执行,这意味着当页面处于非激活状态时,动画就会暂停,从而避免了不必要的 CPU 和 GPU 资源消耗。
- requestAnimationFrame 在绘制每一帧时,会自动根据屏幕刷新率进行同步,从而得到最佳的动画效果和性能表现。
技术详解
requestAnimationFrame 是一个 JavaScript 函数,用于优化动画效果的实现。它可以在浏览器每个绘制帧之前执行回调函数,以确保动画的流畅性和响应速度,并避免了使用 setTimeout 或 setInterval 时可能导致的性能问题。
requestAnimationFrame 的使用方法:
function animation() {
// 动画逻辑代码
requestAnimationFrame(animation);
}
requestAnimationFrame(animation);
在上述代码中,animation 函数包含动画逻辑,通过 requestAnimationFrame 不断地更新动画状态实现动画的连贯播放。由于 requestAnimationFrame 方法会在浏览器下一次绘制前执行回调函数,因此可以确保动画效果平滑且符合用户的操作反应速度,同时也不会对浏览器的性能造成太大影响。
requestAnimationFrame 的优点:
-
与 setTimeout 和 setInterval 相比,requestAnimationFrame 可以更精确地控制动画的播放时间,从而保证动画的流畅性和响应速度。
-
requestAnimationFrame 的回调函数只有在浏览器下一次绘制之前才执行,这意味着当页面处于非激活状态时,动画就会暂停,从而避免了不必要的 CPU 和 GPU 资源消耗。
-
requestAnimationFrame 在绘制每一帧时,会自动根据屏幕刷新率进行同步,从而得到最佳的动画效果和性能表现。
需要注意的是,requestAnimationFrame 并不能完全解决所有的动画问题,有些复杂的动画仍然需要结合 CSS 过渡或者动画来实现。此外,使用 requestAnimationFrame 时也需要考虑兼容性问题,较老版本的浏览器可能不支持该方法,需要采取相应的兼容策略。
与setInterval和setTimeout对比
requestAnimationFrame的调用频率通常为每秒60次。这意味着我们可以在每次重绘之前更新动画的状态,并确保动画流畅运行,而不会对浏览器的性能造成影响。
setInterval与setTimeout它可以让我们在指定的时间间隔内重复执行一个操作,不会考虑浏览器的重绘,而是按照指定的时间间隔执行回调函数,可能会被延迟执行,从而影响动画的流畅度。
效果对比
我们设置了两个容器,分别用requestAnimationFrame()方法和setTimeout方法进行平移效果,Js 代码如下所示:
let distance = 0
let box = document.getElementById('box')
let box2 = document.getElementById('box2')
window.addEventListener('click', function () {
requestAnimationFrame(function move() {
box.style.transform = `translateX(${distance++}px)`
requestAnimationFrame(move)//递归
})
setTimeout(function change() {
box2.style.transform = `translateX(${distance++}px)`
setTimeout(change, 17)
}, 17)
})
效果图如下:
可能我们肉眼看得不是很清楚,但是确实下面的图形平移没有上面图形流畅,用setTimeout会有卡顿现象。
201、Js中的getBoundingClientRect
getBoundingClientRect() 是一个用于获取元素位置和尺寸信息的方法。它返回一个 DOMRect对象,其提供了元素的大小及其相对于视口的位置,其中包含了以下属性:
-
x:元素左边界相对于视口的 x 坐标。 -
y:元素上边界相对于视口的 y 坐标。 -
width:元素的宽度。 -
height:元素的高度。 -
top:元素上边界相对于视口顶部的距离。 -
right:元素右边界相对于视口左侧的距离。 -
bottom:元素下边界相对于视口顶部的距离。 -
left:元素左边界相对于视口左侧的距离。const box = document.getElementById('box'); const rect = box.getBoundingClientRect();
console.log(rect.x); // 元素左边界相对于视口的 x 坐标 console.log(rect.y); // 元素上边界相对于视口的 y 坐标 console.log(rect.width); // 元素的宽度 console.log(rect.height); // 元素的高度 console.log(rect.top); // 元素上边界相对于视口顶部的距离 console.log(rect.right); // 元素右边界相对于视口左侧的距离 console.log(rect.bottom); // 元素下边界相对于视口顶部的距离 console.log(rect.left); // 元素左边界相对于视口左侧的距离
为了更好地理解,我在页面上设置了一个容器,其对应属性看下图:
应用场景
这个方法通常用于需要获取元素在视口中的位置和尺寸信息的场景,比如实现拖拽、定位或响应式布局等,兼容性很好,一般用滚动事件比较多。
特殊场景会用上,比如你登录了淘宝的网页,当你下拉滑块的时候,下面的图片不会立即加载出来,有一个懒加载的效果。当上面一张图片没在可视区内时,就开始加载下面的图片。
下面代码就是判断一个容器是否出现在可视窗口内:
const box = document.getElementById('box')
window.onscroll = function () {//window.addEventListener('scroll',()=>{})
console.log(checkInView(box));
}
function checkInView(dom) {
const { top, left, bottom, right } = dom.getBoundingClientRect();
return top > 0 &&
left > 0 &&
bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
right <= (window.innerWidth || document.documentElement.clientWidth)
}
当容器在可视区域内就输出true,否则就是false。
202、Js中的intersectionObserver
IntersectionObserver 是一个构造函数,可以接收两个参数,第一个参数是一个回调函数,第二个参数是一个对象。这个方法用于观察元素相交情况,它可以异步地监听一个或多个目标元素与其祖先元素或视口之间的交叉状态。它提供了一种有效的方法来检测元素是否可见或进入视口。
用法
使用 IntersectionObserver 需要以下步骤:
创建一个 IntersectionObserver 实例,传入一个回调函数和可选的配置对象。
const observer = new IntersectionObserver(callback, options);
const callback = (entries, observer) => {
// 处理交叉状态变化的回调函数
};
const options = {
// 可选配置
};
将要观察的目标元素添加到观察者中。
const target = document.querySelector('#targetElement');
observer.observe(target);
在回调函数中处理交叉状态的变化。
const callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 元素进入视口
} else {
// 元素离开视口
}
});
};
entries 参数是一个包含每个目标元素交叉状态信息的数组。每个 entry 对象都有以下属性:
target:观察的目标元素。intersectionRatio:目标元素与视口的交叉比例,值在 0 到 1 之间。isIntersecting:目标元素是否与视口相交。intersectionRect:目标元素与视口的交叉区域的位置和尺寸信息。
options 对象是可选的配置,其中常用的配置选项包括:
root:指定观察器的根元素,默认为视口。rootMargin:设置根元素的外边距,用于扩大或缩小交叉区域。threshold:指定交叉比例的阈值,可以是单个数值或由多个数值组成的数组。
IntersectionObserver 适用于实现懒加载、无限滚动、广告展示和可视化统计等场景,同样可以判断元素是否在某一个容器内,不会引起回流。
203、Js中的createNodeIterator
createNodeIterator() 方法是 DOM API 中的一个方法,用于创建一个 NodeIterator 对象,可以用于遍历文档树中的一组 DOM 节点。
通俗一点来讲就是它可以遍历 DOM 结构,把 DOM 变成可遍历的。
这种方法算是一个比较偏的面试考点,面试官问你怎样实现遍历 DOM 结构?其实就可以用到这个方法。但是大多数程序员答不上来这个问题,因为我们在日常开发中这个方法用得极少。这个方法常在框架源码中体现。
应用场景
<body>
<div id="app">
<p>hello</p>
<div class="title">标题</div>
<div>
<div class="content">内容</div>
</div>
</div>
<script>
const body = document.getElementsByTagName('body')[0]
const item = document.createNodeIterator(body)//让body变成可遍历的
let root = item.nextNode() // 下一层
while (root) {
console.log(root);
if (root.nodeType !== 3) {
root.setAttribute('data-index', 123)//给每个节点添加一个属性
}
root = item.nextNode()
}
</script>
</body>
上面代码成功遍历到了各个 DOM 结构:并且在每个 DOM 节点上都添加了
data-index = "123"。
204、Js中的getComputedStyle
getComputedStyle()是一个可以获取当前元素所有最终使用的CSS属性值的方法。返回的是一个CSS样式声明对象。
这个方法有两个参数,第一个参数是你想要获取哪个元素的 CSS ,第二个参数是一个伪元素。
<style>
#box {
width: 200px;
height: 200px;
background-color: cornflowerblue;
position: relative;
}
#box::after {
content: "";
width: 50px;
height: 50px;
background: #000;
position: absolute;
top: 0;
left: 0;
}
</style>
const box = document.getElementById('box')
const style = window.getComputedStyle(box, 'after')
const height = style.getPropertyValue('height')
const width = style.getPropertyValue('width')
console.log(style);
console.log(width, height);
有一个 id 为 box 容器的 CSS 样式声明对象,以及伪元素的宽高。
205、Js中的执行上下文
执行上下文(Execution Context)是 JavaScript 中非常重要的概念之一,它定义了函数执行时的环境(包括变量、函数声明、this 等信息),是 JavaScript 实现作用域和作用域链的基础。每次执行函数时,就会生成一个新的执行上下文,并被加入到一个执行上下文栈(Execution Context Stack)中。
具体来说,一个执行上下文由以下三个部分组成:
-
变量对象(Variable Object,VO):包含了该上下文中定义的变量、函数声明以及函数的形参等信息。
-
作用域链(Scope Chain):由多个变量对象构成的链表结构,用于解析变量和函数的引用。
-
this 值:指向当前函数执行时所属的对象(或者全局对象)。
在函数执行过程中,JavaScript 引擎会首先创建一个空的执行上下文对象,然后依照以下顺序对其进行初始化:
-
确定 this 值。
-
创建变量对象。
-
填充变量对象:
-
给变量对象添加形参、函数声明和变量声明等。
-
如果当前函数是一个可识别的函数(FunctionDeclaration),则会在变量对象中直接创建一个该函数名的属性,值为该函数本身。
-
如果当前函数是一个函数表达式(FunctionExpression),那么该函数名只能在函数体内使用,不能在函数外部使用。
-
如果当前函数中使用了 let 或 const 声明变量,则会在变量对象中创建一个对应的暂时性死区(Temporal Dead Zone,TDZ),在该区域内访问该变量会抛出 ReferenceError 异常。
-
如果当前函数中使用了 var 声明变量,则会在变量对象中创建一个该变量名的属性,但此时该属性的值还是 undefined,随着代码执行的进行而逐渐变化。
-
-
确定当前作用域链。
总之,执行上下文是 JavaScript 中非常重要的概念,它可以帮助我们理解函数作用域和作用域链的实现原理,从而更好地编写 JavaScript 代码。
206、Js中执行上下文的类型
在JavaScript中,执行上下文(Execution Context)是一个抽象概念,用于描述代码在运行时的环境。可以根据上下文创建的方式以及上下文内部的代码特征将其分为以下3种类型:
- 全局执行上下文(Global Execution Context)
全局执行上下文是JavaScript代码在最外层的执行环境,是JS运行时的默认上下文。全局执行上下文在页面打开时自动创建,在页面关闭时被销毁。在全局执行上下文中声明的变量和函数都会作为全局对象(window或global)的属性被保存。
- 函数执行上下文(Function Execution Context)
每当一个函数被调用时,都会创建一个新的函数执行上下文。函数执行上下文会包含函数内部声明的所有变量、函数参数以及 this 对象的引用。每个函数都有自己的函数执行上下文,独立于其他函数执行上下文的。
- Eval函数执行上下文(Eval Function Execution Context)
Eval() 函数可以动态执行一段字符串作为JS代码,而为了执行这段字符串所代表的代码,需要创建一个特殊的执行上下文,即 Eval 函数执行上下文。
这些不同类型的执行上下文在JavaScript代码执行中扮演着非常重要的角色,因为它们定义了代码中变量和函数的作用域、可访问性和生命周期等方面的规则。理解这些执行上下文类型对于理解JavaScript运行时行为及其工作原理非常重要。
207、Js中执行上下文的三个阶段
在 JavaScript 中,每个执行上下文都由三个阶段构成,它们分别是:
-
创建阶段(Creation phase):在此阶段,JavaScript 引擎会创建一个执行上下文,并进行一些初始化工作,例如创建变量对象、建立作用域链、确定 this 值等。需要注意的是,在创建阶段时并不会进行实际代码的执行,而是会对代码进行预处理和解析,以便后面的执行阶段能够顺利运行。
-
执行阶段(Execution phase):在此阶段,JavaScript 引擎会按照代码的顺序逐行执行,并在执行过程中更新变量对象、调整作用域链、处理函数调用等。当代码执行到某个函数时,JavaScript 引擎会新建一个函数执行上下文,并将其压入执行上下文栈中,以便函数内部的代码能够正常执行。
-
回收阶段(Cleanup/Teardown phase):在此阶段,JavaScript 引擎会做一些清理工作,例如释放内存空间、销毁变量对象、断开作用域链等。需要注意的是,变量对象中的变量会在执行阶段和回收阶段之间共享,因此在回收阶段中仍然可以访问执行阶段中声明的变量。
以上是 JavaScript 执行上下文的三个阶段,了解其工作原理对于理解 JavaScript 变量作用域、函数调用机制以及 this 指向等相关概念非常有帮助。
208、Js中作用域的常见应用场景
作用域是 JavaScript 中非常重要的概念,其常见应用场景可以总结为以下几点:
-
封装变量:作用域可以将变量封装在函数或块级作用域中,避免变量名冲突和污染全局命名空间。这样可以保证代码的可读性和可维护性。
-
闭包:闭包是指一个函数能够访问并修改定义在其父函数作用域中的变量,即使父函数已经执行完毕的情况下仍然如此。闭包的最大特点就是能记住自己被创建时的上下文环境,使得函数内部能够访问外部作用域中的变量。
-
模块化开发:在 JavaScript 中,通过使用立即执行函数表达式(Immediately-Invoked Function Expression,IIFE)和闭包,可以实现模块化开发。将一个函数或对象封装在一个单独的作用域中,暴露出需要对外部使用的函数或属性,从而达到隔离和保护代码的目的。
-
函数作为参数传递:JavaScript 允许将函数作为参数传递给其他函数。由于作用域链的存在,内部函数能够访问外部函数中声明的变量和函数,从而实现了很多高级函数的功能。例如 Array 的 forEach() 和 map() 方法就使用了函数作为参数的特性。
-
避免变量提升(hoisting):JavaScript 中变量声明提升的特性可能会导致一些意外行为,使用 let 和 const 可以避免该问题发生。在块级作用域中声明变量,确保它们在程序流中的位置与代码中的位置相符。
209、Js中的作用域链
在 JavaScript 中,每个函数都有自己的作用域(Scope)。
作用域链(Scope Chain)指的是每个函数内部嵌套了一个作用域对象(Scope Object),同时该对象又引用了外部函数的作用域对象,依次形成了一个链式结构。
当函数内部需要访问一个变量时,JavaScript 引擎会先从当前函数的作用域对象中查找该变量,如果没有找到,则会继续到上层函数的作用域对象中查找,直至找到为止。这样一直到全局作用域对象都被查找完毕,如果还没有找到则会报错。
作用域链的顶端指向的是全局作用域对象,因此全局作用域中声明的变量可以在任意函数中被访问。
需要注意的是,当函数执行完毕后,其作用域对象将会被销毁,也就是说作用域链是动态的,而不是像静态作用域那样在编译时就确定了。
作用域链的建立是在函数创建时通过函数的[[Scope]]内部属性来实现的。[[Scope]]属性是一个数组,存储了创建该函数的作用域中所有的外部变量和函数的作用域对象,按照从内到外的顺序排列。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
总之,了解作用域链对于理解 JavaScript 中变量作用域、闭包等概念非常有帮助。
210、Js中如何延长作用域链
在 JavaScript 中,作用域链是一个由作用域对象组成的列表,它用于解析标识符的值。在访问一个变量时,JavaScript 引擎会先在当前作用域中查找该变量,如果没有找到则会继续在作用域链上一级的作用域中查找,直到找到该变量或者到达全局作用域。
我们可以通过以下几种方式延长作用域链:
函数作为参数传递:可以将一个函数作为参数传递给另一个函数,在内部函数中使用外部函数的变量,从而延长作用域链。这种方式被称为“闭包”。
function outer() {
var count = 0;
return function inner() {
count++;
console.log(count);
};
}
var increment = outer();
increment(); // 输出 1
increment(); // 输出 2
在这个例子中,函数 inner 延长了作用域链,可以访问外部函数 outer 中的变量 count。
with 语句:可以使用 with 语句将一个对象添加到作用域链的前端,从而可以更方便地访问对象的属性和方法。但是 with 语句由于性能和安全问题,已经被废弃,不建议使用。
try-catch 语句:可以使用 try-catch 语句来捕获异常,并在 catch 块中创建一个新的作用域。这种方式虽然不常用,但是也可以用来延长作用域链。
try {
throw new Error('Oops!');
} catch (e) {
var message = e.message;
console.log(message); // 输出 "Oops!"
}
在这个例子中,catch 块创建了一个新的作用域,使得可以在其中访问变量 e 和 message。
总之,在 JavaScript 中延长作用域链有多种方式,但是需要注意的是,过度使用闭包等技术可能会导致内存泄露。因此,在实际开发中需要谨慎使用,并注意优化性能和内存的使用。
211、什么是闭包?闭包会用在哪儿?
- 官方说法:闭包就是指有权访问另一个函数作用域中的变量的函数。
- MDN说法:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
- 深度回答:浏览器在加载页面会把代码放在栈内存( ECStack )中执行,函数进栈执行会产生一个私有上下文( EC ),此上下文能保护里面的使用变量( AO )不受外界干扰,并且如果当前执行上下文中的某些内容,被上下文以外的内容占用,当前上下文不会出栈释放,这样可以保存里面的变量和变量值,所以我认为闭包是一种保存和保护内部私有变量的机制。
闭包具有以下几个特点:
-
可以访问父级作用域中的变量:在函数内部通过闭包可以访问并修改父级作用域的变量,而且这些变量在函数执行完毕后依然存在。
-
可以将变量私有化:使用闭包可以将某些变量私有化,避免全局变量污染和变量冲突问题,同时也增加了代码的可维护性。
-
可以实现模块化:使用闭包可以将一些相关方法或属性封装在一个作用域内,形成一个独立的模块,从而提高代码的复用性和可读性。
闭包有两个常用的用途;
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
在实际的项目中,会基于闭包把自己编写的模块内容包裹起来,这样编写就可以保护自己的代码是私有的,防止和全局变量或者是其他的代码冲突,这一点是利用保护机制。
但是不建议过多的使用闭包,因为使用不被释放的上下文,是占用栈内存空间的,过多的使用会导致导致内存泄漏。
解决闭包带来的内存泄漏问题的方法是:使用完闭包函数后手动释放。
Js中闭包的使用场景:
return回一个函数- 函数作为参数
- IIFE(自执行函数)
- 循环赋值
- 使用回调函数就是在使用闭包
- 节流防抖
- 函数柯里化
-
私有变量和方法:使用闭包可以将某些变量或方法私有化,在外界无法直接访问或修改,从而保证数据的安全性。
-
延迟执行:使用闭包可以实现定时器的延迟执行,从而避免了异步回调带来的代码逻辑混乱问题。
-
实现模块化:使用闭包可以实现类似于模块化的功能,将一些相关方法或属性封装在一个作用域内,从而提高代码的复用性和可读性。
const myModule = (function() { let privateVariable = 0;
function privateMethod() { // ... }
return { publicMethod: function() { privateVariable++; privateMethod(); console.log(privateVariable); } }; })();
myModule.publicMethod(); // 1 myModule.publicMethod(); // 2
在上述代码中,myModule 是一个自执行函数,该函数返回了一个包含 publicMethod 方法的对象,公共方法可以访问并修改私有变量和方法。
Js中闭包的执行过程
-
形成私有上下文
-
进栈执行
-
一系列操作
(1). 初始化作用域链(两头)
(2). 初始化this
(3). 初始化arguments
(4). 赋值形参
(5). 变量提升
(6). 代码执行
- 遇到变量就先看是否是自己私有的,不是自己私有的按照作用域链上查找,如果不是上级的就继续线上查找,,直到 EC(G),变量的查找其实就是一个作用域链的拼接过程,拼接查询的链式就是作用域链。
-
正常情况下,代码执行完成之后,私有上下文出栈被回收。但是遇到特殊情况,如果当前私有上下文执行完成之后中的某个东西被执行上下文以外的东西占用,则当前私有上下文就不会出栈释放,也就是形成了不被销毁的上下文,闭包。
212、Js中创建对象有哪几种方式?
对象字面量,使用大括号 {} 来创建一个对象字面量,可以直接指定对象的属性和值。
构造函数,可以使用构造函数来创建一个对象。首先需要定义一个构造函数,然后使用 new 运算符来创建一个实例对象。
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
const person = new Person('Bob', 28, 'male');
Object.create(),使用 Object.create() 方法来创建一个新对象,并将其原型设置为另一个对象或 null。
const person1 = {
name: 'Alice',
age: 24,
gender: 'female'
};
const person2 = Object.create(person1);
person2.name = 'Bob';
person2.age = 28;
class,使用 ES6 中的 class 关键字来定义一个类,并使用 new 运算符来创建一个实例对象。
class Person {
constructor(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
}
const person = new Person('Alice', 24, 'female');
213、Js中的hasOwnProperty、instanceof方法
hasOwnProperty() 方法
hasOwnProperty() 是 JavaScript 中的一个对象方法,用于判断某个属性是否是该对象的自有属性(而不是继承来的)。
该方法接收一个参数,即要检测的属性名,如果该对象本身(非原型链)上存在这个属性,则返回 true,否则返回 false。与 in 运算符不同的是,hasOwnProperty() 方法只检查对象自身的属性,不会检查其原型链上的属性。
const obj = {name: 'Alice', age: 24};
console.log(obj.hasOwnProperty('name')); // true
console.log(obj.hasOwnProperty('toString')); // false
instanceof 运算符
instanceof 运算符用于检查一个对象是否是另一个对象的实例。它的语法是 obj instanceof Class,其中 obj 是要检查的对象,Class 是要检查的目标类(或函数)。
instanceof 运算符通过检查 obj 的原型链中是否存在 Class.prototype 来判断 obj 是否是 Class 的实例。如果是,返回 true,否则返回 false。
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
214、Js中深浅拷贝区别
在 JavaScript 中,深拷贝和浅拷贝是常用的对象复制方式。它们之间的区别主要体现在复制后的对象对原始对象的影响程度上。
浅拷贝是指创建一个新对象,然后将原始对象的属性值复制到新对象中。新对象和原始对象中的简单类型属性会互不影响,但对于引用类型属性,则会复制其引用而不是实际的值。这意味着在浅拷贝中,新对象和原始对象引用相同的内存地址。
深拷贝则是创建一个全新的对象,将原始对象的所有属性和嵌套对象的属性完全复制到新对象中。深拷贝会递归地复制嵌套对象,确保新对象与原始对象完全独立,不共享任何引用。这意味着在深拷贝中,修改新对象的属性不会影响原始对象。
以下是深拷贝和浅拷贝的常见实现方式:
浅拷贝的实现方式:
- 使用
Object.assign()方法:var obj2 = Object.assign({}, obj1); - 使用展开语法(Spread Syntax):
var obj2 = { ...obj1 }; - 使用数组的浅拷贝方法如
slice()和concat()。
深拷贝的实现方式:
- 使用递归和
Object.assign()方法:var obj2 = JSON.parse(JSON.stringify(obj1));
注意:该方法无法处理包含函数、正则表达式等特殊对象的深拷贝,且会忽略属性值为undefined的属性。 - 使用深拷贝库,如 Lodash 的
_.cloneDeep()方法。
需要注意的是,深拷贝可能会导致性能上的损耗,因为它需要递归遍历对象的所有属性。另外,在处理包含循环引用的对象时,深拷贝可能导致堆栈溢出的问题。
选择使用深拷贝还是浅拷贝取决于具体的需求。如果需要保留原始对象的结构并创建一个独立的副本,以便对副本进行修改而不影响原始对象,那么应该使用深拷贝。如果只需要对对象进行浅层次的复制,且对原始对象的修改不会对副本产生影响,那么浅拷贝就可以满足需求。
215、Js中如何避免一个对象的属性被修改
使用 Object.defineProperty() 或 Object.defineProperties() 方法:这两个方法可以用来定义或修改对象的属性,并且可以通过设置属性描述符的相关选项来控制属性的特性。通过将 writable 设置为 false,你可以将属性设置为不可修改。
const obj = {};
Object.defineProperty(obj, 'propertyName', {
value: 'propertyValue',
writable: false, // 将属性设置为不可修改
});
obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue
使用 Object.freeze() 方法:Object.freeze() 方法可以冻结一个对象,使其属性变为不可修改(包括值和属性的可配置性)。一旦对象被冻结,任何对其属性进行修改的尝试都将被忽略。
const obj = {
propertyName: 'propertyValue'
};
Object.freeze(obj); // 冻结对象
obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue
使用 ECMAScript 6 的类和 get 访问器:在类中定义属性时,可以使用 get 访问器而不提供 set 访问器。这会使该属性成为只读属性,不可修改。
class MyClass {
constructor() {
this._propertyName = 'propertyValue';
}
get propertyName() {
return this._propertyName;
}
}
const obj = new MyClass();
obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue
216、Js中类数组转换成数组的方法有哪些?如何遍历Js中的类数组
Array.prototype.slice.call(arrayLike);
Array.prototype.splice.call(arrayLike, 0);
Array.prototype.concat.apply([], arrayLike);
Array.from(arrayLike);
function foo(){
Array.prototype.forEach.call(arguments, a => console.log(a))
}
function foo(){
const arrArgs = Array.from(arguments)
arrArgs.forEach(a => console.log(a))
}
function foo(){
const arrArgs = [...arguments]
arrArgs.forEach(a => console.log(a))
}
217、Js中的forEach如何跳出循环
forEach是不能通过break或者return来实现跳出循环的,为什么呢?实现过forEach的同学应该都知道,forEach的的回调函数形成了一个作用域,在里面使用return并不会跳出,只会被当做continue
可以利用try catch
function getItemById(arr, id) {
var item = null;
try {
arr.forEach(function (curItem, i) {
if (curItem.id == id) {
item = curItem;
throw Error();
}
})
} catch (e) {
}
return item;
}
218、Js中typeof和instanceof的区别
typeof 和 instanceof 都是 JavaScript 中用于检测变量类型的操作符,
但它们之间存在一些区别:
-
typeof:主要用于检测基本类型,返回一个字符串。它可以判断基本数据类型(如 undefined、number、string、boolean 和 symbol)以及函数类型,但对于 null 类型来说,typeof 会误判为 object。同时,typeof 对于引用类型,只能粗略地判断其类型(如 array、object 和 function)。 -
instanceof:主要用于检测对象类型,返回一个布尔值。它可以准确地检测对象是否是某个类的实例(即复杂引用数据类型),但对于基本数据类型来说,instanceof 无法正确判断其类型。
typeof null 返回结果为 "object"。
typeof NaN 的结果是 "number"。
这个结果看似不合理,但实际上是 JavaScript 语言中一些历史遗留问题导致的。
219、Js中判断空对象
- 使用
Object.keys()方法:该方法返回一个对象中所有可枚举属性的字符串数组,如果返回的数组为空,则说明这个对象是空的。 - 使用
JSON.stringify()方法:该方法将一个 JavaScript 对象转换为一个 JSON 字符串。如果对象为空,则将返回一个空的 JSON 对象 "{}"。可以利用这个特性来判断一个对象是否为空。 - 使用
for...in循环:使用该循环可以遍历对象中的属性,如果对象没有任何属性,则循环体不会被执行。
220、谈谈Js中的事件冒泡和捕获
在JavaScript中,事件冒泡和事件捕获是两种事件传播机制。当一个事件(如点击、鼠标移动等)发生在一个元素上时,这个事件会沿着DOM树向上或向下传播。事件冒泡和事件捕获分别描述了这两种传播方向。
事件冒泡(Event Bubbling):
事件冒泡是指事件从触发元素开始,逐层向上(从子元素到父元素)传播,直到根元素(如<html>或<body>)。在事件冒泡过程中,你可以在父元素上设置事件监听器,从而在事件传播过程中捕获到子元素触发的事件。
例如,假设你有以下HTML结构:
<div id="parent">
<button id="child">点击我</button>
</div>
你可以在父元素上设置一个事件监听器来捕获子元素触发的点击事件:
document.getElementById('parent').addEventListener('click', function(event) {
console.log('事件冒泡:点击事件已捕获');
});
事件捕获(Event Capturing):
事件捕获是指事件从根元素开始,逐层向下(从父元素到子元素)传播,直到触发元素。与事件冒泡相反,事件捕获允许你在事件到达目标元素之前捕获它。
要在事件捕获阶段设置事件监听器,你需要在addEventListener方法中将第三个参数设置为true:
document.getElementById('parent').addEventListener('click', function(event) {
console.log('事件捕获:点击事件已捕获');
}, true);
注意:事件捕获在实际开发中使用较少,事件冒泡是更常见的事件传播机制。
总结:事件冒泡和事件捕获是JavaScript中两种不同的事件传播机制。事件冒泡是从触发元素向上传播,而事件捕获是从根元素向下传播。
221、Js中的浏览器事件机制
在 JavaScript 中,浏览器事件机制可以分为三个阶段:
- 捕获阶段 (Capture Phase):事件从最外层的元素逐级向目标元素传递过程中的任何一个阶段;
- 目标阶段 (Target Phase):事件到达目标元素后被触发的阶段;
- 冒泡阶段 (Bubble Phase):事件从目标元素逐级向最外层的元素传递过程中的任何一个阶段。
JavaScript 中使用 addEventListener() 方法添加事件监听器,该方法接收三个参数:
- 事件名称(例如
'click','keydown'等); - 事件处理函数;
- 是否在捕获阶段进行处理(默认是在冒泡阶段进行处理)。
在事件被触发时,会依照上述三个阶段依次执行事件监听器。例如:
document.body.addEventListener('click', () => {
console.log('body clicked!');
}, false);
在上述代码中,当用户在页面上点击时,首先会进入捕获阶段,然后进入目标阶段,在目标元素 body 上触发 click 事件,接着又进入冒泡阶段,直到事件穿过了整个文档树。
需要注意的是,由于现代浏览器已经优化了事件流程,多数情况下我们不需要去关心事件的捕获和冒泡阶段,只需要在目标阶段进行事件处理即可。如果我们不需要在捕获阶段执行代码,就可以将 addEventListener() 方法的第三个参数设置为 false,表示该事件监听器在冒泡阶段执行。
222、Js中的dispatchEvent的自定义事件,什么是Js中的事件委托/事件代理
在JavaScript中,dispatchEvent方法允许你创建并触发自定义事件。自定义事件可以让你在特定情况下通知其他代码,例如当某个操作完成或状态发生变化时。要使用dispatchEvent创建和触发自定义事件
JavaScript 中的事件代理(Event Delegation)是一种常用的绑定事件的技巧。
它将原本需要绑定在子元素的响应事件(如click、keydown等)委托给父元素,由父元素担任事件监听的职务。
事件代理的原理是基于 DOM 元素的事件冒泡机制,即事件从子元素开始逐级向上传播到父级元素,直到文档根节点。因此,对于一个父元素来说,可以通过 addEventListener() 方法监听子元素上触发的事件,并通过 event.target 属性获取实际被点击的子元素,即可实现对子元素事件的响应。
223、如何对监听scoll事件的优化
监听scroll事件的方式来触发可视区域中数据的更新,当滚动发生后,scroll事件会频繁触发,很多时候会造成重复计算的问题,从性能上来说无疑存在浪费的情况。
可以使用IntersectionObserver替换监听scroll事件。
IntersectionObserver可以监听目标元素是否出现在可视区域内,在监听的回调事件中执行可视区域数据的更新,并且IntersectionObserver的监听回调是异步触发,不随着目标元素的滚动而触发,性能消耗极低。
Intersection Observer API的功能包括:
- 异步观察目标元素与祖先元素或文档视口间的交集变化。
- 提供回调函数功能,当目标元素与祖先元素或文档视口的交集发生变化时,自动触发回调函数并返回IntersectionObserverEntry对象。
- 可以同时监控多个元素的交集变化,并在变化时依次执行回调函数。
- 可以通过传递options参数来定制Intersection Observer的根元素、margin、threshold等参数,从而更加灵活地监控目标元素与祖先元素的交集变化。
- Intersection Observer API可以有效提高性能,避免频繁监听scroll事件所带来的性能问题和代码复杂度。
224、Js的执行流程
-
解析:浏览器会将JavaScript代码解析成可执行的代码。这个过程包括词法分析、语法分析和代码生成。
-
创建全局执行上下文:在执行JavaScript代码之前,浏览器会创建一个全局执行上下文。全局执行上下文是一个特殊的执行上下文,它会在整个JavaScript代码执行期间存在。
-
执行代码:浏览器会按照代码的顺序执行JavaScript代码。在执行代码的过程中,浏览器会创建新的执行上下文,并将它们添加到执行上下文栈中。
-
变量提升:在执行代码之前,浏览器会将变量和函数声明提升到它们所在的作用域的顶部。这个过程被称为变量提升。
-
执行函数:当JavaScript代码中遇到函数调用时,浏览器会创建一个新的执行上下文,并将它添加到执行上下文栈中。函数执行完毕后,浏览器会将执行上下文从执行上下文栈中弹出。
-
执行异步代码:当JavaScript代码中遇到异步操作时,比如定时器或者Ajax请求,浏览器会将这些操作添加到任务队列中。当执行栈为空时,浏览器会从任务队列中取出一个任务,并将其添加到执行栈中执行。
-
执行完毕:当JavaScript代码执行完毕时,浏览器会将执行上下文栈中的所有执行上下文都弹出,最终只剩下全局执行上下文。
225、Js中数据在栈和堆中的存储方式
基本数据类型大小固定且操作简单,所以放入栈中存储
引用数据类型大小不确定,所以将它们放入堆内存中,让它们在申请内存的时候自己确定大小
这样分开存储可以使内存占用最小。栈的效率高于堆
栈内存中变量在执行环境结束后会立即进行垃圾回收,而堆内存中需要变量的所有引用都结束才会被回收
226、手写实现Promise及思路
-
创建一个 Promise 类,定义它的构造函数:Promise 构造函数接收一个函数作为参数,该函数在 Promise 实例化时会被自动执行,执行结束后根据异步操作结果调用 resolve 或 reject 方法。
-
定义 Promise 实例的状态和值:Promise 实例有三种状态,Pending(等待状态)、Fulfilled(成功状态)、Rejected(失败状态)。通过实例上的 status 和 value 属性来管理当前实例的状态和值。
-
定义 then 方法,实现链式调用:then 方法可以传入一个或两个函数参数,分别代表解决状态时的回调和拒绝状态时的回调,返回一个新的 Promise 对象,可以通过新的 Promise 实例进行链式调用。
-
实现 resolve 和 reject 方法,用于改变 Promise 对象的状态和值,并触发 then 方法中的回调函数。
-
实现 catch 方法和 finally 方法:catch 方法用于捕获错误信息,finally 方法用于在 Promise 对象被解析后运行一段代码块。
-
实现 all 和 race 方法:all 方法接收一个 Promise 数组作为参数,只有当所有的 Promise 都解析成功时,返回成功状态;反之,则返回失败状态。race 方法同样接收一个 Promise 数组作为参数,但是只要其中有一个 Promise 解析成功或者失败,它就会立即返回。
227、什么是Promise A+ 规范
Promise A+ 规范是一种关于 Promise 的标准规范,主要是为了解决不同实现之间的兼容性和互操作性问题。该规范定义了 Promise 对象的行为、状态、方法等内容,为 JavaScript 中的异步操作提供了一种标准化的解决方案。
Promise A+ 规范主要包含以下内容:
-
Promise 状态:规范定义了 Promise 的三种状态:Pending(等待状态)、Fulfilled(成功状态)和 Rejected(失败状态),并规定了每种状态下 Promise 对象的行为和状态转移。
-
Promise 方法:规范定义了 Promise 对象的 then、catch 和 finally 方法,明确了它们的参数和返回值,并规定了它们的执行流程和错误处理机制。
-
Promise 解决过程:规范定义了 Promise 对象的解决过程,即当 Promise 对象进入 Fulfilled 或者 Rejected 状态时,如何执行回调函数以及如何处理链式调用和异常情况等。
-
测试套件:规范提供了一个测试套件,用于检测 Promise 实现是否符合规范,并能够对 Promise 实现进行详细的测试和验证。
Promise A+ 规范通过明确的行为约定和规范化方法来保证 Promise 对象的可靠性和兼容性,使得开发者可以更加便捷地使用 Promise 来进行异步编程,同时也方便了各种异步库之间的协同使用。
228、Promise怎么进行超时控制
在 JavaScript 中,可以使用 Promise.race() 方法来实现 Promise 的超时控制。Promise.race() 方法接收一个 Promise 数组作为参数,返回一个新的 Promise 对象,该 Promise 对象将会在数组中的任意一个 Promise 对象状态发生改变时,立即采用该 Promise 对象的状态。
因此,我们可以将一个 Promise 对象和一个定时器 Promise 对象放在一个数组中,然后使用 Promise.race() 方法来实现超时控制。具体实现如下:
function timeoutPromise(promise, timeout) {
// 创建一个定时器 Promise 对象
const timeoutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Promise timed out after ' + timeout + ' ms'));
}, timeout);
});
// 使用 Promise.race() 方法实现超时控制
return Promise.race([promise, timeoutPromise]);
}
上述代码中,timeoutPromise() 函数接收一个 Promise 对象和一个超时时间作为参数,返回一个新的 Promise 对象。在该函数内部,我们创建了一个定时器 Promise 对象,该 Promise 对象会在超时时间到达后 reject 一个带有错误信息的 Promise 对象。然后,我们使用 Promise.race() 方法将原始的 Promise 对象和定时器 Promise 对象放在一个数组中,返回一个新的 Promise 对象,该 Promise 对象将会在原始的 Promise 对象状态发生改变或者超时时间到达时,立即采用该 Promise 对象的状态。
229、控制并发的Promise的调度器
想象一下,有一天你突然一次性发了10个请求,但是这样的话并发量是很大的,能不能控制一下, 就是一次只发2个请求,某一个请求完了,就让第3个补上,又请求完了,让第4个补上, 以此类推,让最高并发量变成可控的
实现一个控制并发的 Promise 调度器可以用来限制同时执行的 Promise 数量,从而避免因为并发量过大导致的性能问题。下面是一个简单的实现:
class PromiseScheduler {
constructor(concurrency) {
this.concurrency = concurrency; // 最大并发数
this.running = 0; // 当前正在运行的 Promise 数量
this.queue = []; // Promise 队列
}
add(promiseFn) {
return new Promise((resolve, reject) => {
// 将 Promise 函数和 resolve/reject 函数封装成一个任务
this.queue.push(() => promiseFn().then(resolve, reject));
// 执行任务队列
this.run();
});
}
run() {
// 如果正在运行的 Promise 数量已经达到最大并发数,或者任务队列为空,则直接返回
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
// 从任务队列中取出一个任务,并执行它
const task = this.queue.shift();
task().finally(() => {
// 任务执行完毕后,将正在运行的 Promise 数量减一,并递归执行任务队列
this.running--;
this.run();
});
// 将正在运行的 Promise 数量加一
this.running++;
}
}
上述代码中,我们定义了一个 PromiseScheduler 类,它接收一个最大并发数作为参数。在 add() 方法中,我们将 Promise 函数和 resolve/reject 函数封装成一个任务,并将任务添加到任务队列中。在 run() 方法中,我们从任务队列中取出一个任务,并执行它。当任务执行完毕后,我们将正在运行的 Promise 数量减一,并递归执行任务队列。这样,就可以限制同时执行的 Promise 数量,从而避免因为并发量过大导致的性能问题。
230、PromiseQueue
-
PromiseQueue是一个基于Promise的队列工具,它可以帮助我们管理异步操作,用来控制并发和顺序等情况。在 JavaScript 中,异步编程常常会导致代码结构复杂、难以维护,因此使用PromiseQueue可以更好地管理异步操作。 -
通过使用
PromiseQueue,我们可以将多个异步操作按照需求排成队列,保证它们按照一定的顺序依次执行。例如,在某些场景下,需要处理大批量的网络请求,但服务器同时处理的数量有限,如果同时发出太多请求,可能会导致服务器崩溃。这时,就可以使用PromiseQueue来控制异步请求的数量,避免同时发起过多的请求。
其中,PromiseQueue 提供了以下接口:
add(generator)方法:将一个生成Promise的函数添加到队列中,返回一个Promise,表示当前操作的状态;maxConcurrent和maxQueued选项:分别指定最大并发数和最大排队数;start()和pause()方法:分别用于启动和暂停队列。
总的来说,PromiseQueue 的作用就是将异步操作按照指定的顺序进行管理,避免出现混乱和冲突,提高代码的可维护性和可读性。
实现一个PromiseQueue
class PromiseQueue {
constructor({ maxConcurrent = 1, maxQueued = Infinity } = {}) {
this.maxConcurrent = maxConcurrent;
this.maxQueued = maxQueued;
this.activeCount = 0;
this.queue = [];
}
add(generator) {
return new Promise((resolve, reject) => {
this.queue.push({
generator,
resolve,
reject,
});
this._dequeue();
});
}
getQueueLength() {
return this.queue.length;
}
getPendingLength() {
return this.activeCount;
}
start() {
if (this.activeCount < this.maxConcurrent) {
this._dequeue();
}
}
pause() {
// no-op
}
async _dequeue() {
if (this.activeCount >= this.maxConcurrent) {
return;
}
const task = this.queue.shift();
if (!task) {
return;
}
this.activeCount++;
try {
const result = await task.generator();
task.resolve(result);
} catch (err) {
task.reject(err);
} finally {
this.activeCount--;
this._dequeue();
}
}
}
231、使用Promise异步加载一张图片
使用 Promise 异步加载一张图片可以通过创建一个 Promise 对象,在 Promise 对象的回调函数中进行图片的加载处理。具体代码如下:
function loadImage(url) {
return new Promise(function(resolve, reject) {
var img = new Image(); // 创建一个图片对象
img.onload = function() {
resolve(img); // 加载成功后返回图片对象
};
img.onerror = function() {
reject(new Error('Could not load image at ' + url)); // 加载失败时返回错误信息
};
img.src = url; // 设置图片地址,开始加载图片
});
}
// 调用方式如下:
loadImage('path/to/image.png')
.then(function(img) {
console.log('图片加载成功!');
document.body.appendChild(img);
})
.catch(function(error) {
console.log(error.message);
});
在上述代码中,使用了 Promise 的 resolve 和 reject 函数来返回异步操作的结果或错误信息。
当图片成功加载后,将会调用 resolve 函数,返回图片对象;
当图片加载失败时,将会调用 reject 函数,返回一个包含错误信息的 Error 对象。
注意:在使用 Promise 进行图片加载时,需要预先创建一个 Image 对象,并设置其 onload 和 onerror 事件监听。
同时,为了避免图片加载过程中出现异常,可以使用 try-catch 来捕获相关异常信息。
232、使用Promise异步加载一个在线js sdk
使用 Promise 异步加载一个在线 JS SDK 可以通过创建一个 Promise 对象,在 Promise 对象的回调函数中进行 SDK 的加载处理。具体代码如下:
function loadSDK(url) {
return new Promise(function(resolve, reject) {
var script = document.createElement('script'); // 创建 script 标签
script.src = url; // 设置标签 src 属性,加载 SDK 资源
script.onload = function() {
resolve(window.SDK); // 加载成功后返回 SDK 对象
};
script.onerror = function() {
reject(new Error('Could not load SDK at ' + url)); // 加载失败时返回错误信息
};
document.head.appendChild(script); // 将 script 标签添加到文档头部,开始加载资源
});
}
// 调用方式如下:
loadSDK('https://example.com/sdk.js')
.then(function(SDK) {
console.log('SDK 加载成功!');
SDK.init(); // 初始化 SDK
})
.catch(function(error) {
console.log(error.message);
});
在上述代码中,使用了 Promise 的 resolve 和 reject 函数来返回异步操作的结果或错误信息。
当 SDK 成功加载后,将会调用 resolve 函数,返回 SDK 对象;
当 SDK 加载失败时,将会调用 reject 函数,返回一个包含错误信息的 Error 对象。
注意:在使用 Promise 进行 SDK 加载时,需要预先创建一个 script 标签,并设置标签的 src 属性。
同时,为了避免 SDK 加载过程中出现异常,可以使用 try-catch 来捕获相关异常信息。在加载完成后,需要在 onload 事件回调函数中调用相关初始化函数。
233、手写实现Promise(简易版)
class MyPromise {
constructor(fn){
// 存储 reslove 回调函数列表
this.callbacks = []
const resolve = (value) => {
this.data = value // 返回值给后面的 .then
while(this.callbacks.length) {
let cb = this.callbacks.shift()
cb(value)
}
}
fn(resolve)
}
then(onResolvedCallback) {
return new MyPromise((resolve) => {
this.callbacks.push(() => {
const res = onResolvedCallback(this.data)
if (res instanceof MyPromise) {
res.then(resolve)
} else {
resolve(res)
}
})
})
}
}
// 这是测试案例
new MyPromise((resolve) => {
setTimeout(() => {
resolve(1)
}, 1000)
}).then((res) => {
console.log(res)
return new MyPromise((resolve) => {
setTimeout(() => {
resolve(2)
}, 1000)
})
}).then(res =>{console.log(res)})
234、手写实现Promise.all
要手写实现Promise.all,需要以下步骤:
- 创建一个新的
Promise对象,并返回它。 - 遍历传入的可迭代对象(通常是数组),对每个元素执行以下操作:
- 如果元素不是
Promise对象,则使用Promise.resolve将其转换为Promise对象。 - 对每个
Promise对象,等待其状态变为fulfilled,并收集解决的值。
- 如果元素不是
- 如果所有
Promise对象都成功解决,则使用resolve将所有收集到的解决值作为数组传递给新创建的Promise对象。 - 如果任何一个
Promise对象被拒绝,则使用reject将该拒绝原因传递给新创建的Promise对象。
下面是一个简单的示例代码,用于手动实现Promise.all:
function customPromiseAll(iterable) {
return new Promise((resolve, reject) => {
const promises = Array.from(iterable);
const results = [];
let completedCount = 0;
if (promises.length === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((result) => {
results[index] = result;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
})
.catch((error) => {
reject(error);
});
});
});
}
// 示例用法:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
customPromiseAll([promise1, promise2, promise3])
.then((results) => {
console.log(results); // 输出 [1, 2, 3]
})
.catch((error) => {
console.error(error);
});
235、手写实现Promise.race
传参和上面的 all 一模一样,传入一个 Promise 实例集合的数组,然后全部同时执行,谁先快先执行完就返回谁,只返回一个结果
MyPromise.race = function(promisesList) {
return new MyPromise((resolve, reject) => {
// 直接循环同时执行传进来的promise
for (const promise of promisesList) {
// 直接返回出去了,所以只有一个,就看哪个快
promise.then(resolve, reject)
}
})
}
236、async/await 实现原理
在 JavaScript 引擎中,async/await 函数的实现原理是基于 Promise 对象和生成器函数(Generator Function)的协作。
- 具体来说,async/await 函数内部会将其代码块转换为一个状态机,并使用生成器函数返回的迭代器来进行状态的管理和切换,从而实现异步的调用和处理。
- 当 async 函数被调用时,它会立即返回一个 Promise 对象,并且开始执行其中的代码。当遇到 await 表达式时,async 函数会暂停执行并将控制权转交给生成器函数返回的迭代器对象,该对象会执行一个 next() 方法来将获取到的 Promise 对象进一步传递。
- 在等待的过程中,async 函数会依次执行其代码块中下一个 await 表达式之前的所有同步操作。当等待的 Promise 对象状态变为 resolved 时,async 函数会再次被调用并继续执行,直到代码块执行结束或者抛出异常。
需要注意的是,async/await 函数的实现原理并不是原生的 JavaScript 语法规范所支持的,而是通过编译工具(如 Babel 等)将其代码转换为符合 JavaScript 语法规范的代码实现。
237、判断传入的函数是否标记了async
在 ES6 中,可以使用 async function 的语法来定义一个异步函数,但是如何判断一个函数是否是异步函数呢?可以通过以下方式:
使用 Object.getPrototypeOf 方法获取函数的原型对象,然后检查其 constructor 是否等于 AsyncFunction。如果是,则说明该函数是异步函数。
function isAsync(fn) {
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
return fn instanceof AsyncFunction;
}
// 示例用法
async function foo() {}
const bar = async function() {};
console.log(isAsync(foo)); // 输出 true
console.log(isAsync(bar)); // 输出 true
console.log(isAsync(function() {})); // 输出 false
利用异步函数会返回 Promise 对象的特性,检查函数返回值是否为 Promise 对象。如果是,则说明该函数是异步函数。
function isAsync(fn) {
return Promise.resolve(fn()) instanceof Promise;
}
// 示例用法
async function foo() {}
const bar = async function() {};
console.log(isAsync(foo)); // 输出 true
console.log(isAsync(bar)); // 输出 true
console.log(isAsync(function() {})); // 输出 false
需要注意的是,在使用以上方法判断一个函数是否为异步函数时,传入的函数必须是函数对象(Function 类型)。如果传入的是一个字符串、数字等其他类型的值,则会抛出错误。
238、如果在async()前面加上await会不会影响输出结果
在 JavaScript 中,await 关键字只能在 async 函数内部使用,用于暂停当前 async 函数的执行,等待一个 Promise 对象的状态变为 resolved 后再继续执行后续代码。
如果在 async 函数外部使用 await 关键字,将会导致语法错误。
因此,在 async() 前面加上 await 是无效的,并且会导致语法错误。如果你想要等待 async 函数执行完成并获取其返回值,可以在调用 async 函数的地方使用 await 关键字来获取结果。
下面是一个示例,展示了正确使用 await 获取 async 函数的结果:
async function myAsyncFunction() {
// 执行异步操作
return "Hello, World!";
}
async function main() {
const result = await myAsyncFunction();
console.log(result); // 输出 "Hello, World!"
}
main();
在上述示例中,main 函数中使用 await 关键字等待 myAsyncFunction 函数执行完成,然后获取其返回值并输出结果。通过合理使用 await,我们可以确保在处理 async 函数返回结果时遵循正确的执行顺序。
239、Promise和async/await有哪些相似性
Promise 和 async/await 都是用于处理 JavaScript 异步操作的机制,它们有一些相似性和共同点:
-
异步操作:Promise 和 async/await 都是为了处理异步操作而设计的。它们可以代替传统的回调函数方式,使得异步代码更易于编写和维护。
-
语法糖:async/await 是基于 Promise 的语法糖,它们实际上是对 Promise 进行封装和简化,使得异步代码更具可读性和可维护性。
-
链式调用:Promise 和 async/await 都支持链式调用,可以通过 then() 方法或 async 函数中的多个 await 语句来组织和控制异步操作的顺序。
-
错误处理:Promise 和 async/await 都提供了错误处理的机制。在 Promise 中,可以通过 catch() 方法或在链式调用中的 reject 回调来捕获和处理错误;在 async/await 中,可以使用 try/catch 块来捕获和处理异常。
尽管 Promise 和 async/await 有一些相似性,但它们之间也存在一些区别:
-
语法差异:Promise 是基于 then() 和 catch() 方法的链式调用,需要手动管理异步操作的状态和传递值;而 async/await 使用 async 函数和 await 关键字,可以以更直观、类似同步的方式编写异步代码。
-
错误处理方式:Promise 的错误处理主要通过链式调用中的 reject 回调或 catch() 方法进行,相对独立;而 async/await 使用 try/catch 块来捕获异常,更类似于同步代码的异常处理。
-
可读性和可维护性:由于 async/await 的语法糖特性,使得代码更易读、理解和维护,尤其是在处理多个异步操作时。而 Promise 的链式调用可能会导致代码嵌套层级过深,可读性稍差一些。
240、修改对象的迭代器,实现方法
在 JavaScript 中,每个对象都有一个预定义的 Symbol.iterator 方法,用于定义该对象的默认迭代器。我们可以通过修改对象的 Symbol.iterator 方法来改变其迭代行为。
为了让题目中的代码 var [a, b] = { a: 1, b: 2 }; 成立,我们可以将对象的 Symbol.iterator 方法修改为返回对象属性的值的迭代器。具体实现如下:
const obj = { a: 1, b: 2 };
obj[Symbol.iterator] = function() {
const keys = Object.keys(obj);
let index = 0;
return {
next: function() {
if (index < keys.length) {
return { value: obj[keys[index++]], done: false };
} else {
return { done: true };
}
}
};
};
var [a, b] = obj;
console.log(a); // 输出 1
console.log(b); // 输出 2
在上述代码中,我们首先获取对象的所有属性名,然后定义一个索引 index,用于记录当前迭代到的属性。接着,我们返回一个迭代器对象,其中包含一个 next() 方法,用于依次返回对象的属性值。当所有属性值都被迭代完毕时,我们返回 { done: true } 以表示迭代结束。
需要注意的是,在修改对象的 Symbol.iterator 方法时,我们只能使用函数表达式或方法定义,不能使用箭头函数。因为箭头函数没有自己的 this,无法正确访问对象的属性。
241、ES6中Map、WeakMap、Object 区别
三者都为键值对容器,但是在key的类型方面有一些区别:
- Object 的
key只能为string或者symbol类型; - Map 的
key接受任意类型; - WeakMap 的
key只能为object类型,并且该对象为弱引用,因此多用于解决引用层面,比如前文中提到的深拷贝中的循环引用问题。
除此之外,还存在一些 API 层面的不同。比如说 Map 可以通过size直接获得存储的个数,存储及获取值的方式也不同。
另外还有很重要的一点是:Object 的存储是无序的,但是 Map 的存储是有序的,在遍历过程中会根据值的插入顺序。
技术详解
ES6 中的 Map、WeakMap 和 Object 都是用于存储键值对的数据结构,但它们之间有些区别:
- Map
Map 对象是一组键值对的集合,其中的键可以是任意类型的值,包括对象和原始类型。相比于传统的 Object 对象,Map 对象支持更多的键类型,而且其迭代顺序与插入顺序一致。Map 对象提供了一些基本的方法,如 set、get、has、delete 等,在需要存储键值对并需要保留顺序的场景中非常实用。
- WeakMap
WeakMap 对象是一组弱键映射的集合,其中的键必须是对象类型,值可以是任意类型。WeakMap 对象的键是被弱引用的,这意味着当键所引用的对象在没有其它引用时,将会自动从 WeakMap 对象中删除。WeakMap 对象没有提供清除所有键值对的方法,因此无法遍历 WeakMap 对象中的所有键值对。
- Object
Object 是 JavaScript 中的基本数据类型之一,它可以用于存储简单的数据类型和复杂的对象类型。Object 对象提供了一些基本的方法,如 keys、values、entries 等,在处理对象时非常实用。但是,Object 对象并不支持以对象作为键值,这意味着它在某些场景下的使用受到了限制。
总之:
- Map 对象适用于需要存储键值对并需要保留顺序的场景,
- WeakMap 对象适用于需要轻松释放不再需要的键值对的场景,
- Object 对象则适用于存储简单的数据类型和复杂的对象类型,但不支持以对象作为键值。
242、Es6中 Class类 的本质是什么,原型继承 和 Class 继承
首先在 JS 中并不存在类,class只是语法糖,本质还是函数。
class Person {}
Person instanceof Function // true
组合继承
组合继承是最常用的继承方式
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承的方式核心是在子类的构造函数中通过Parent.call(this)继承父类的属性,然后改变子类的原型为new Parent()来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用class去实现继承,并且实现起来很简单
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
class实现继承的核心在于使用extends表明继承自哪个父类,并且在子类构造函数中必须调用super,因为这段代码可以看成Parent.call(this, value)。
当然了,之前也说了在 JS 中并不存在类,class的本质就是函数。
243、如何将 Class类 转换为 Function
Es6 里的 Class类就是构造函数的语法糖,因此 Es6 中的 Class 可以使用函数(构造函数)方式实现。
具体步骤如下:
定义一个函数(构造函数),用于初始化对象的属性。构造函数名通常以大写字母开头
function MyClass(name) {
this.name = name;
}
在构造函数的原型上定义类方法
MyClass.prototype.sayHello = function () {
console.log("Hello, " + this.name + "!");
};
在构造函数上定义类静态方法。静态方法不属于类实例,而是与类本身相关联的方法
MyClass.staticMethod = function () {
console.log("This is a static method.");
};
这样就可以使用 new 操作符创建类实例,并调用其成员方法,例如:
var obj = new MyClass("Tom");
obj.sayHello(); // Hello, Tom!
需要注意的是,ES6 的 Class 语法糖是在函数的基础上封装出来的语法,因此 Class 转换为函数也是合理的。
调用类的静态方法,使用类名直接调用即可
MyClass.staticMethod(); // This is a static method.
需要注意的是,ES5 的类实现不支持继承和多态的语法糖,需要手动使用原型链实现。子类继承父类的方式如下所示:
function MyChildClass(name, age) {
MyClass.call(this, name); // 调用父类构造函数,并传入 this 和参数
this.age = age;
}
// 子类继承父类的原型链
MyChildClass.prototype = Object.create(MyClass.prototype);
MyChildClass.prototype.constructor = MyChildClass; // 修复子类 constructor 指向
// 子类重写父类方法
MyChildClass.prototype.sayHello = function () {
console.log("Hello, I am " + this.name + ", and I am " + this.age + " years old.");
};
244、实现一个重试N次请求的方法
在 JavaScript 中,可以使用 Promise 和 async/await 来实现重试 N 次请求的功能。具体实现如下:
async function retryRequest(requestFn, maxRetries) {
let retries = 0;
while (retries < maxRetries) {
try {
const response = await requestFn();
return response;
} catch (error) {
retries++;
if (retries === maxRetries) {
throw error;
}
}
}
}
上述代码中,我们定义了一个 retryRequest() 函数,它接收两个参数:requestFn 和 maxRetries。requestFn 是一个返回 Promise 对象的函数,用来发起请求。maxRetries 是最大重试次数。在函数内部,我们使用 while 循环来重试请求,直到达到最大重试次数或者请求成功为止。在每次请求失败后,我们将重试次数加一,并判断是否达到最大重试次数,如果达到最大重试次数,则抛出错误。如果请求成功,则返回响应结果。
使用示例:
async function fetchData() {
const response = await retryRequest(() => fetch('https://example.com/data'), 3);
const data = await response.json();
console.log(data);
}
上述代码中,我们使用 retryRequest() 函数来重试请求,最大重试次数为 3。如果请求成功,则将响应结果转换为 JSON 格式,并输出到控制台。
245、迭代器(iterator)和生成器(generator)的关系
迭代器(Iterator)和生成器(Generator)是 JavaScript 中的两个重要概念,它们之间有着密切的关系。
迭代器是一种对象,它提供了一种方法来访问一个容器(如数组或对象)中的元素,而不需要暴露容器的内部实现。迭代器对象必须实现一个 next() 方法,该方法返回一个包含 value 和 done 两个属性的对象。value 属性表示当前迭代到的元素,done 属性表示迭代是否结束。
生成器是一种特殊的函数,它可以在执行过程中暂停并恢复。生成器函数使用 function* 关键字定义,它内部可以使用 yield 关键字来暂停执行并返回一个值。生成器函数返回的是一个迭代器对象,可以通过调用 next() 方法来依次访问生成器函数中 yield 返回的值。
因此,可以说生成器是一种特殊的迭代器,它可以通过 yield 关键字来暂停执行并返回值,而不需要手动实现 next() 方法。生成器函数返回的是一个迭代器对象,可以通过调用 next() 方法来依次访问生成器函数中 yield 返回的值。
下面是一个简单的示例,演示了迭代器和生成器的关系:
function* generateNumbers() {
let i = 0;
while (i < 5) {
yield i++;
}
}
const iterator = generateNumbers();
console.log(iterator.next()); // { value: 0, done: false }
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
上述代码中,我们定义了一个 generateNumbers() 生成器函数,它可以生成 0 到 4 的数字。我们通过调用 generateNumbers() 函数来获取一个迭代器对象,然后依次调用 next() 方法来访问生成器函数中 yield 返回的值。每次调用 next() 方法时,生成器函数会从上次暂停的位置继续执行,直到遇到下一个 yield 关键字或者函数结束。当生成器函数执行完毕后,迭代器对象的 done 属性为 true,value 属性为 undefined。
246、CommonJS和ES6 Module的区别是什么
CommonJS modules和ES6 modules都是用来组织JavaScript代码的模块标准。
它们的主要区别在于:
-
加载方式:CommonJS 模块使用 require() 函数同步加载模块,而ES6 模块使用 import 关键字异步加载模块,在需要时再进行解析和加载。
-
作用域:CommonJS 模块的导出(exports)和引入(require)都是在模块作用域中运行的,因此只有在运行时才能确定导出和引入的具体内容。而ES6 模块使用静态编译,在编译时就可以确定导出和引入的具体内容,因此具有更加严格的作用域。
-
默认导出:CommonJS 模块对默认导出的支持是通过module.exports实现的,而ES6 模块则是通过export default实现的。
CommonJS 模块可以这样导出一个函数:
module.exports = function () {
console.log('Hello World!');
}
而ES6 模块可以这样导出一个函数:
export default function () {
console.log('Hello World!');
}
在导入时,CommonJS 模块使用require()函数来引入默认导出的内容,而ES6 模块使用import关键字来引入默认导出的内容。
总的来说,CommonJS modules和ES6 modules都是非常有用的JavaScript模块标准,但它们有不同的加载方式、作用域和默认导出方式。在选择使用哪个标准时,需要根据具体场景来进行选择。
使用工具让CommonJS模块适用于浏览器环境
CommonJS模块最初是为了Node.js环境而设计的,并且在浏览器环境中并没有默认支持。但是,我们可以使用工具将CommonJS模块转换为可以在浏览器环境中运行的代码,例如Browserify和webpack。
webpack它支持多种模块标准,包括CommonJS模块。在webpack配置文件中,可以通过设置不同的loader来实现对不同模块标准的转换,比如使用babel-loader将ES6模块转换为CommonJS模块。
因此,虽然CommonJS最初是为Node.js环境而设计的,但是通过使用工具,我们可以将CommonJS模块转换为可在浏览器环境中工作的代码。
247、静态导入与动态导入的区别
-
静态导入:是指在 JavaScript 应用的编译期间静态地加载 ES6 模块。在代码中使用 import 关键字来引入模块,这种方式称为静态导入。
import { foo, bar } from './myModule.js'; -
动态导入:它允许在 JavaScript 代码运行时动态地加载 ES6 模块。与静态导入不同,动态导入使用 import() 函数来动态加载模块,而不是使用 import 关键字。
import("./myModule.js") .then((module) => { // 使用 module 中的方法 }) .catch((error) => { // 处理错误 });
248、Es6有哪些新语法
-
let 和 const:引入了块级作用域变量声明方式,
let声明的变量具有块级作用域,而const声明的变量是常量。 -
箭头函数:提供了更简洁的函数定义方式,可以减少代码量,并且自动绑定 this。
-
模板字符串:使用反引号 ` 来创建多行字符串和插值表达式,使字符串拼接更加方便。
-
解构赋值:可以从数组或对象中快速提取值并赋给变量,简化了变量赋值的过程。
-
扩展运算符(...):用于将数组或对象展开为独立的元素,简化了数组和对象的操作。
-
默认参数:函数参数可以设置默认值,调用函数时如果不传参则会使用默认值。
-
类和继承:引入了 class 关键字,使得面向对象编程更加直观和易用,支持了类的继承。
-
Promise:提供了更好的异步编程解决方案,避免了回调地狱,简化了异步操作的处理。
-
模块化:使用
import和export关键字进行模块导入和导出,实现了更好的代码组织和复用。 -
生成器函数:使用 function* 和 yield 关键字创建生成器函数,可以实现迭代器和异步编程。
-
Map 和 Set:引入了 Map 和 Set 数据结构,提供了更方便的键值对存储和集合操作方式。
-
Symbol:引入了 Symbol 数据类型,用于创建唯一的标识符,可以用于对象属性的命名。