【前端】JavaScript 纯干货 基础知识 + 面试题分享!


最近忙着找工作,在这期间的复习过程中,为了方便查阅,总结了很多我常用到的基础内容和面试题。在这里分享给大家,大家也可以参考复习。


最基本的几个用法

/*控制浏览器弹出一个警告框*/
alert("Hello!");
/*在页面中输出一个内容*/
document.write("我的第一行JS代码");
/*向控制台输出一个内容*/
console.log("HelloWorld!");

基本数据类型和引用类型的区别

基本数据类型:Number、String、Boolean、Undefined、Null
引用数据类型:Object、Function、Array
ES6 中新增了一种 Symbol 。这种类型的对象永不相等,即始创建的时候传入相同的值,可以解决属性名冲突的问题,做为标记。

它们的区别有以下几个方面:

首先是声明变量时不同的内存分配:
  1)对于基本数据类型而言,这些简单数据段会被存放在栈中,也就是说,它们的值直接存储在变量访问的位置。这是因为基本数据类型占据的空间是固定的,所以可将他们存储在较小的内存区域中。这样存储便于迅速查寻变量的值。
  2)而对于引用数据类型,它们会被存储在堆中,也就是说,存储在变量处的值其实是一个指针,这个指针指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。
  
这样不同的内存分配机制也带来了不同的访问机制:
  1)在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,也就是按引用访问。
  2)而基本数据类型的值则是可以直接访问到的。
  
除此以外,复制变量时也有所不同:
  1)对基本数据类型而言,在将一个变量的值复制给另一个变量时,会将原变量的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的值而已。
  2)而对于引用数据类型,在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。

推荐文章:数据类型


声明变量(let var const区别)

在ES6之前,我们都是用var来声明变量,而且没有块级作用域的概念,{}无法限定var声明变量的范围。而ES6新增的let,可以声明块级作用域的变量,即只在{}内有效。

在这里插入图片描述
其次,用let声明的变量,不存在变量提升,要求必须等let声明语句执行完之后,变量才能使用,不然会报错。

在这里插入图片描述
然后,let不允许在相同作用域内,重复声明同一个变量,否则报错。
在这里插入图片描述
对于const:const也可以声明块级作用域的变量,也不存在变量提升,不能声明同名变量。const最特殊的点在于,它一旦声明必须赋值,且声明后不能再修改。(如果声明的是引用数据类型,可以修改其属性)

而var无法声明形成块级作用域,存在变量提升,可以声明同名变量。


数据类型转换的总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这其中:
数据类型的转换分为:强制转换和隐式转换。
对于强制转换,就是使用String(),Number(),Boolean()函数强制转换。
对于隐式转换,比如字符串加数字,数字就会转成字符串。数字减乘除字符串,字符串就换转成数字。除此以外 == 时也会触发隐式转换。

== 和 === 的差异?

在这里插入图片描述
三个等于号不仅要比较值,也要比较数据类型。

两个等于号只比较值,不比较数据类型。那如果两个等于号左右两侧的数据类型不一样,在这里就会触发隐式转换。

它的转换规则为:
对象 == 字符串 :会使用 对象.toString() 把对象变成字符串
null == undefined :相等,但是和其他值比较就不再相等了
NaN == NaN: 不相等,NaN和任何值都不相等
剩下的所有情况比较时,都转换为数字
比如:“1”==true,等号左右两侧全都会变成数字1
对于对象也是一样会转成数字


引用数据类型:对象Object

创建对象的两种方式:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
上面的这两个用法,会出现一个十分关键的问题,就是下面这个for…in用法。

在for…in语句中,对象有几个属性,循环体就会执行几次。每次执行时,就会把对象中的一个属性的名字赋值给变量。就比如说,它应该是这么用的:
在这里插入图片描述
它会出现什么问题呢?

我们在刚才说到,每一次循环,这个变量取得的都是某属性的名字,但是我们并不能直接通过使用这个变量,来获得这个属性名下对应的属性值。即这样不行:
console.log(obj.n);如果这样使用,它获取的是obj对象中,属性名为n的属性值,而不会是我们在这里想要的结果。那么我们获取到了这个属性名n,到底该怎么使用?就是使用上面的第二种用法:console.log(obj[n]); 为什么这样就行?因为这种方式可以做到,判断n是变量名还是属性名。如果要找的是属性名,是需要双引号包围的,那么这里只要不加双引号,那么它就会把它当成变量名,这时它就会找到这个变量名所取得的属性名的属性值。也就是说,如果console.log(obj[“n”]);,那么它找到的还会是属性名为n的那个属性值。

在这里插入图片描述
即:基本数据类型的比较是值的比较,也就是只要两个变量的值相等,我们就认为这两个变量相等。而比较两个引用数据类型时,它是比较的对象的内存地址。原因也就是,当一个变量是一个对象时,实际上变量中保存的并不是对象本身,而是对象的引用。当从一个变量向另一个变量复制引用类型的值时,会将对象的引用复制到变量中,并不是创建一个新的对象。这时,两个变量指向的是同一个对象。因此,改变其中一个变量会影响另一个。
也就是说,当你做如下操作时:
在这里插入图片描述
obj2里的name就也会改变。这是因为他们指向了同一个内存地址。

上面这个部分是十分关键的,经常会在面试中遇到,当然还要涉及更多的内容,在我的面试文章中就有这些用法。
在这里插入图片描述
最开始提到的创建对象的第二种方式,也叫做对象字面量。这种方式将会是往后建议使用的方式。
在这里插入图片描述
这种方式很明显,创建是很简单的,只需要var obj = {};就完成了,不用new。而且这种方式,相比于第一种,在创建对象的属性时,看起来很清晰,而且也很简洁和简便。


引用数据类型:函数function

在这里插入图片描述
在这里插入图片描述
在之前提到了匿名函数,它也叫做立即执行函数。这个立即执行函数(匿名函数)是个十分重要的用法,尤其是在之后的Vue当中,我也不去具体举例了,只要接触到Vue就可以想到了。
在这里插入图片描述
在这里插入图片描述

箭头函数和function函数的区别

在ES6中,提出了一种更加方便的使用方法,就是箭头函数。而且箭头函数的特性十分重要,在Vue中就经常使用箭头函数。
在这里插入图片描述
在这里插入图片描述
这个特性,就是Vue十分看重的,之后会遇到。这个this,如果用function定义的函数,this的指向随着调用环境的变化而变化的,而箭头函数中的this指向是固定不变的,一直指向的是定义函数的环境。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
也就是说,箭头函数你只能传进去参数,让它实现某些功能,而不能让它来存储对象或者其他数据。
在这里插入图片描述
由于js的内存机制,function的级别最高,而用箭头函数定义函数的时候,需要var(let const定义的时候更不必说)关键词,而var所定义的变量不能得到变量提升,故箭头函数一定要定义于调用之前!
在这里插入图片描述
相信大家对于变量提升,已经有所了解,那么变量提升到底是怎样的机制?在这里做个小总结:
在这里插入图片描述

工厂方法

在知道了this的用法之后,我们可以用this来使用“工厂方法”来创建多组对象。
在这里插入图片描述
比如:
在这里插入图片描述

构造函数

除了用这个工厂方法以外,还有更简便的方法,就是用构造函数。

构造函数和普通函数的区别在于构造函数的首字母需要大写。除此以外,普通函数就直接调用,而构造函数需要使用new关键字来调用。

这里的this,当以构造函数的形式调用时,this就是新创建的那个对象。

比如:
在这里插入图片描述
同时,这里就会出现一个问题,在每次执行构造函数后,它都会创建一个新的sayName函数,这样显然没必要。所以,从目前来看,我们可以将sayName方法在全局作用域中定义,这样就解决了问题。但是,将函数定义在全局作用域,污染了全局作用域的命名空间,而且定义在全局作用域中也很不安全。那么,可以使用一种新的解决办法,原型。

原型

在这里插入图片描述
在这里插入图片描述
因此,通过原型这种方式,我们就可以把这些对象共有的属性和方法,统一添加到构造函数的原型对象上,这样就可以不用分别为每一个对象添加,也不会影响到全局作用域,就可以使每个对象都具有这些属性或方法了。

除此以外,在之前提到的in的用法,可以取出对象中的属性名,现在使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true。

还可以使用console.log(mc.hasOwnProperty(“age”));来检查对象自身中是否含有该属性(不会检查原型)。只有当对象自身中含有属性时,才会返回true。

在原型中还有一个需要注意的函数,Person.prototype.toString。之前,直接输出一个对象,我们会得到[object Object]这样的输出结果,为什么会输出这样的结果,其实是因为就是因为输出的是toString()方法的返回值。它默认输出的就会是这样的结果。我们只要修改它的内容,就可以改变输出结果了。
在这里插入图片描述
有了这种方式,就可以不用for…in的方式来输出结果了,因为用toString可以更好的设置输出结果。

原型链

原型链就是:每个对象都有一个指向它的原型对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止,组成这条链的最后一环。这种一级一级的链结构就称为原型链。
在这里插入图片描述

this的指向

在这里插入图片描述

call / apply / bind的区别(更改this指向的三个方法)

推荐文章:call、apply、bind的区别
在这里插入图片描述
在这里插入图片描述
相同点:都可以改变函数内部的this指向
区别:
1.call和apply会调用函数,并且改变函数内部的this指向
2.call和apply传递的参数不一样,apply必须以数组形式
3.bind不会调用函数,但是会改变函数内部的this指向


函数作用域

这种类型的问题,属于面试必被问到的题型之一。
在这里插入图片描述
举一些实际例子:
在这里插入图片描述
在这里插入图片描述


对象的一种:数组

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
除了这些以外,还需要说明以下几点。
这个Array可以当作构造函数来理解,当我们想给这个数组一些初始值时,就可以通过new来初始化。
在这里插入图片描述
它不会认为,你是想初始化一个数组中只有一个元素10的数组,而是会创建一个长度为10的数组。当然,大家平常也不会初始化一个固定长度的数组,因为毕竟在当下,可能无法得知最后数组中元素的个数(如果能确定,那就无所谓了)。
既然如此,那就肯定有另一种方法,可以实现只初始化一个元素。如下:
在这里插入图片描述
数组中存储的元素可以是任意的数据类型,或者对象、函数、数组。
在这里插入图片描述
之前也说到了,我们有时是无法确定数组的长度的,那么必不可少的就是数组能够使用的方法。

数组方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


数组的遍历

首先遍历的最基本方法:for
在这里插入图片描述
通常使用的也都是for。但是JS中还有一个方法,可以用来遍历数组:forEach()

在这里插入图片描述


Date、Math的用法

Date、Math的用法


字符串相关方法以及正则表达式

字符串相关方法以及正则表达式


延时调用和定时调用

在这里插入图片描述
但通常还是使用定时调用多一些。
在这里插入图片描述


BOM操作

在这里插入图片描述
更多详细使用方法及属性:

window对象属性和方法

BOM介绍及BOM相关操作


如果递归多了,会发生什么?如何解决?

在这里插入图片描述
递归栈溢出。
有些递归问题可以通过改成for循环解决,避免了递归栈溢出的问题。还有一种利用js运行机制的技巧,我们可以通过setTimeout让任务在下一事件循环执行来改进。
在这里插入图片描述
它的原理就是,当sum1运行到setTimeout处,任务会被存至回调队列,然后本次的sum1返回,sum1执行完毕弹出调用栈,任务队列的任务再进入调用栈中运行。所以,每次调用栈里只会运行1个sum函数,不会导致调用栈溢出。但是用这种方法会发现结果需要等待很久才会出来,原因是任务队列的任务到调用栈是有一定时间损耗的,一般为几毫秒到几十毫秒不等。如果按10ms计算,sum1每计算一个数都得需要加入任务队列,那处理十万个数的额外损耗就有1000s。所以我们需要对代码进行优化。
在这里插入图片描述
在sum2中设置一定的元素,在这里设置1000,那么相当于只有(100000/1000)次会进入任务队列,所以延迟只有1s。


互联网是如何运作的?

推荐讲解视频:互联网是如何运作的?

浏览器是如何运作的?

推荐讲解视频:浏览器是如何运作的?

作者想说的话:这两个视频及该作者讲解的JS运行原理十分清晰,各位感兴趣的可以自行观看。而为什么要了解以上内容,是因为面试的时候有可能被问到,而且这部分的内容其实很关键,因为理解之后才会明白前端部分的一些原理到底是怎么一回事,并且懂得浏览器的兼容问题给我们前端程序员带来了多大的麻烦…
在这里插入图片描述
那么具体的内容视频里都说了,现在就总结一些内容,方便各位抓到接下来涉及到内容的重点。

真实DOM的具体流程(重排重绘)

html首先会经过tokeniser标记化,通过词法分析将输入的html内容解析成多个标记,根据识别后的标记进行DOM树构造。在DOM树构造过程中会创建document对象,然后以document的为根节点的DOM树不断进行修改,向其中添加各种元素。html代码中往往会引入一些额外的资源,比如说图片、CSS、JS脚本等。图片和CSS这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,因为它们不会影响DOM的生成。但当html解析过程中遇到script标签,就会停止html解析流程,转而去加载解析并且执行JS。

那么为什么会是这样的结果呢?因为浏览器并不知道JS的执行是否会改变当前页面的html结构,如果JS代码里用了document.write方法来修改html,那么之前的html解析就没有了任何意义。这也就是为什么,script标签一定要放在合适的位置,或者使用async或defer属性来异步加载执行JS。
在这里插入图片描述
在html解析完成后,我们就会获得一个DOM树,但我们还不知道DOM树上的每个节点应该长什么样子。主线程需要解析CSS并确定每个DOM节点的计算样式,即使你没有提供自定义的CSS样式,浏览器也会用自己默认的样式表。

比如h2的字体比h3的大。
在这里插入图片描述
接下来,我们需要知道每个节点需要放在页面上的哪个位置,也就是节点的坐标以及该节点需要占用多大的区域,这个阶段被称为layout布局。
在这里插入图片描述
主线程通过遍历DOM和计算好的样式来生成Layout tree。Layout tree上的每个节点都记录了x,y坐标和边框尺寸。
在这里插入图片描述
注:DOM Tree和Layout Tree并不是一一对应的,设置为display:none的节点不会出现在Layout Tree上。
在这里插入图片描述
而在before伪类中添加了content值的元素,content里的内容会出现在Layout Tree上,不会出现在DOM Tree上。这是因为DOM是通过HTML解析获得的,并不关心样式,而Layout Tree是根据DOM Tree和计算好的样式来生成。Layout Tree是和最后显示在屏幕上的节点是对应的。
在这里插入图片描述
到目前位置,我们已经知道了元素的大小、形状和位置,我们还需要知道的就是它以什么样的顺序绘制这个节点。举例来说,z-index属性会影响节点绘制的层级关系,如果按照dom的层级结构来绘制页面,则会导致错误的渲染。所以为了保证在屏幕上展示正确的层级,主线程遍历Layout Tree创建一个绘制记录表。该表记录了绘制的顺序,这个阶段被称为绘制。
在这里插入图片描述
现在知道了文档的绘制顺序,终于到了该把这些信息转化成像素点显示在屏幕上的时候了,这种行为被称为栅格化。Chrome最早使用了一种很简单的方式:只栅格化用户可视区域的内容,当用户滚动页面时,再栅格化更多的内容来填充缺失的部分。这种方式带来的问题显而易见,会导致展示延迟。

随着不断的优化升级,现在的Chrome使用了一种更为复杂的栅格化流程,叫做合成。合成是一种将页面的各个部分分成多个图层,分别对其进行栅格化,并在合成器线程中单独进行合成页面的技术。简单来说就是,页面所有的元素按照某种规则进行分图层,并把图层都栅格化好了,然后只需要把可视区的内容组合成一帧展示给用户即可。

主线程遍历Layout Tree,生成Layer Tree。当Layer Tree生成完毕和绘制顺序确定后,主线程将这些信息传递给合成器线程,合成器线程再将每个图层栅格化。由于一层可能像页面的整个长度一样大,因此合成器线程将他们切分为许多图块,然后将每个图块发送给栅格化线程。栅格化线程栅格化每个图块,并将它们存储在GPU内存中。当图块栅格化完成后,合成器线程将收集称为“draw quads”的图块信息,这些信息里记录了图块在内存中的位置和在页面的哪个位置绘制图块的信息。根据这些信息,合成器线程生成了一个合成器帧,然后合成器帧通过IPC传送给浏览器进程。接着浏览器进程将合成器帧传送给GPU,然后GPU渲染展示到屏幕上。

经过了上述过程,最终我们看到了页面的内容。当页面发生变化,比如滚动了页面,都会生成一个新的合成器帧,新的帧会再传给GPU,然后再次渲染到屏幕上。
在这里插入图片描述
但是在这整个过程中,当我们改变一个元素的尺寸位置属性时,会重新进行样式计算、布局、绘制以及后面的所有流程,这种行为被称为重排。
在这里插入图片描述
当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制,这种行为被称为重绘。
在这里插入图片描述
我们会发现重排和重绘都会占用主线程,但除此以外还有另外一个东西也运行在主线程上,那就是JS。既然它们都在主线程运行,就一定会出现抢占执行时间的问题。如果我们写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式、计算布局和绘制的操作。我们知道当页面以每秒60帧的刷新率刷新时(也就是每帧16ms),才不会让用户感觉到页面卡顿。
在这里插入图片描述
那如果在运行动画时还有大量的JS任务需要执行,因为布局、绘制和JS执行都是在主线程运行的,当在一帧的时间内布局和绘制结束后,如果还有剩余时间,JS就会拿到主线程的使用权。如果JS执行时间过长,就会导致在下一帧开始时JS没有及时归还主线程,导致下一帧动画没有按时渲染,那就会出现页面动画的卡顿。
在这里插入图片描述
但实际上面对上述问题,其实是有优化手段的。

requestAnimationFrame

第一种方式可以通过requestAnimationFrame这个API来帮助我们解决问题。它属于异步执行的方法,它是专门用来实现高性能的帧动画的。为了让各种动画效果能够有统一的刷新机制,这个方法会在每一帧被调用,通过API的回调,我们可以把JS运行任务分成一些更小的任务块去分配到每一帧,然后在每一帧时间用完前暂停JS执行,归还主线程。这样的话,在下一帧开始时,主线程可以按时执行布局和绘制。React最新的渲染引擎React Fiber就是用到了这个API来做了很多优化。

这样一来就可以省系统资源,提高系统性能,改善视觉效果。它的缺点也有,目前它还存在兼容性问题,而且因为它不属于宏任务,也不属于微任务,所以它需要在主线程上完成。如果主线程比较繁忙,那么它的效果会大打折扣。
在这里插入图片描述
通过之前的过程,我们知道栅格化的整个流程是不占用主线程的,只在合成器线程和栅格线程中运行,这就意味着它无需和JS抢夺主线程。我们刚才提到如果反复进行重绘和重排,可能会导致掉帧,这是因为有可能JS执行阻塞了主线程。而CSS中有个动画属性叫transform,通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格线程中,所以不会受到主线程中JS执行的影响。更重要的是通过transform实现的动画由于不需要经过布局绘制、样式计算等操作,所以节省了很多运算时间。这就是第二种方式。

综上所述,其实我们可以选择很多其他的方式来实现动画。位置变化、宽高变化(旋转、3D等)这些都是可以使用transform来代替的。
在这里插入图片描述
通过上述的描述,我们就可以得知一类面试题的答案,问题是:为什么要避免大量的重绘和重排。上述的内容就是问题答案。

所以总结如下:

一、真实DOM和其解析流程?
浏览器渲染引擎的大致工作流程:
第一步,用HTML分析器,分析HTML元素,构建一颗DOM树。
第二步,用CSS分析器,分析CSS文件和元素上的样式,生成页面的样式表。
第三步,将DOM树和样式表,关联起来,构建一颗Render树。
第四步,有了Render树,浏览器开始布局,为每个Render树上的节点确定一个在显示屏上出现的精确坐标。
第五步,Render树和节点显示坐标都有了,就调用每个节点paint方法,把它们绘制出来。

然后再经过一个复杂的渲染过程,就将内容渲染了出来。

当我们改变一个元素的尺寸位置属性时,会重新进行样式计算、布局、绘制以及后面的所有流程,这种行为被称为重排。

当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制,这种行为被称为重绘。

二、JS操作真实DOM的代价?
用传统的开发模式,原生JS操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值就是白白的浪费性能。

虚拟DOM

三、为什么需要虚拟DOM,它有什么好处?
虚拟DOM就是为了解决浏览器性能问题而被设计出来的。若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的内容保存到本地一个JS对象中,最终将这个JS对象一次性挂到DOM树上,再进行后续操作,避免大量无谓的计算量。

script为什么放在页面的底部

首先,如果js代码放在body标签上面,当html解析过程中遇到script标签,就会停止html解析流程,转而去加载解析且执行JS。它会选择这样做是因为浏览器并不知道JS的执行是否会改变当前页面的html结构,如果JS代码里用了document.write方法来修改html,那么之前的html解析就没有了任何意义。也正因如此,此时在JS中对HTML部分作出相关的操作时,body部分还没开始解析渲染,所以我们就得不到我们想要的结果。这也就是为什么,script标签一定要放在合适的位置,或者使用async来异步加载执行JS。当然如果就想放在上面也可以选择将js包裹在window.οnlοad=function(){}里面。


ES6异步编程:Promise和async await的区别

基本用法:
在这里插入图片描述
Promise 是之前异步编程的解决方案,比传统的回调函数更合理。而ES6出现的async / await,在代码量上可以直观地看出它很节省。对比Promise,我们不需要再把数据赋值给一个我们其实并不需要的变量,也不需要书写then,不需要新建一个匿名函数处理响应。正因如此,async / await相比Promise,再次减少嵌套数量,从而写出可读性更高的代码。
在这里插入图片描述
除此以外,还要说到async / await 的错误捕获。

在说到错误捕获前,就要对async / await详细说明一点。如果要使用这种方式,首先要在主体函数之前使用async关键字。在函数体内,使用await关键字。当然await关键字只能出现在用async声明的函数体内。该函数会隐式地返回一个Promise对象,函数体内的return值将会作为这个Promise对象resolve时的参数。它都返回一个Promise对象了,当然也就可以使用then方法去添加回调函数。因此错误捕获的第一种方案:就是在await 后,加上then和catch。第二种很简单的方案就是用try / catch 去包裹await部分。但是如果我们有很多请求,那么每一个await都要对应一个try / catch,那显然很麻烦。但是如果用一个try / catch 将所有的await包裹起来,那又不方便我们对某一个错误进行对应的处理。
在这里插入图片描述

Promise.race / Promise.all的用法

Promise.all可以将多个Promise实例包装成一个新的Promise实例。它在成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败的值。
在这里插入图片描述
Promse.race返回的结果是多个Promise实例里获得结果最快的那个,不管结果本身是成功状态还是失败状态。
在这里插入图片描述

Promise的状态

Promise的三种状态:pending、fulfilled、rejected,同一时间只能存在一种状态,且状态一旦从pending挂起状态改变后就不能再变,直到调用成功,状态由pengding=>fulfilled或者调用失败,状态由pending=>rejected。


defer和async的区别

浏览器渲染页面,读取到包含defer属性的外部<script>标签时不会停止DOM渲染,而是异步下载,加载完整个页面再运行js。对于async,它也是异步下载,不同于defer的是,下载完成会立即执行,此时会阻塞dom渲染。


JavaScript的运行机制(微任务、宏任务、事件循环)

首先需要说明的是,浏览器是多线程的,而JS是单线程的,也就导致浏览器只给一个线程让它来渲染。浏览器提供了一个事件队列,里面存放的就是一些延后执行的任务。而在事件队列中,又分为微任务队列和宏任务队列。整体的执行顺序就是:主线程代码先执行,执行过后,到事件队列中执行。在事件队列中,先去微任务队列中执行,没有微任务再去执行宏任务。执行过程中还会检查是否存在微任务,如果有就又会再去微任务队列执行。如此往复。这个流程就是事件循环,即eventloop。
宏任务:定时器、事件绑定…
微任务:promise、 async、 await…
在这里插入图片描述
在这里插入图片描述


JS如何判断数组?

首先对于typeof用法,它判断不出数组:
在这里插入图片描述
方法一: 使用instanceof方法
instanceof原理是通过判断左操作数的对象的原型链上是否具有右操作数的构造函数的prototype属性。

var arr=[];
console.log(arr instanceof Array) //返回true

方法二: 使用constructor方法
constructor 属性返回对象相对应的构造函数。

console.log([].constructor == Array);  //true
console.log({}.constructor == Object);  //true
console.log("string".constructor == String); //true
console.log((123).constructor == Number);  //true
console.log(true.constructor == Boolean);  //true

方法三: 使用Object.prototype.toString.call(arr) === '[object Array]'方法

function isArray(o) {
  return Object.prototype.toString.call(o);
}
var arr=[2,5,6,8];
var obj={name:'zhangsan',age:25};
var fn = function () {}
console.log(isArray(arr)); //[object Array]
console.log(isArray(obj)); //[object Object]
console.log(isArray(fn));  //[object function]

方法四:ES5定义了Array.isArray:

Array.isArray([]) //true

new的过程 + Object.create()的内部原理 + Object.create(null)和{}的区别

在这里插入图片描述
Object.create是内部定义一个对象,并且让F.prototype对象 赋值为引进的对象函数 o,并return出一个新的对象。

而new做法是新建一个obj对象o1,并且让o1的__proto__指向了Base.prototype对象。并且使用call 进行强转作用环境。从而实现了实例的创建。

而Object.create(null)和{}到底有什么区别呢?
在这里插入图片描述
直接从结果上看,Object.create(null)新创建出的对象除了会有自身属性之外,原型链上没有任何属性,也就是没有继承Object的任何东西。主要是因为第一个参数使用了null。也就是说将null设置成了新创建对象的原型,自然也就不会有原型链上的属性。但这里很重要的问题就是,为什么很多源码作者要选择用Object.create(null)来创建新对象呢?

在我看来,用create创建的对象,没有任何属性,那我们可以把它当作一个非常纯净的map来使用,我们可以自己定义hasOwnProperty、toString方法,完全不必担心会将原型链上的同名方法覆盖掉。


import 和 require的区别

1、require可以理解为一个全局方法,所以它可以在任何地方执行。而import必须写在文件的顶部。
2、require的性能相对于import稍低,因为require是在运行时才引入模块并且赋值给某个变量,而import只需要在编译时引入指定模块即可。
在这里插入图片描述


实现一个正方形拖拽效果

<script type="text/javascript">
  window.onload = () => {
    var div = document.getElementById('test2')
    var disX = 0
    var disY = 0
    div.onmousedown = (e) => {
      disX = e.clientX - div.offsetLeft;
      disY = e.clientY - div.offsetTop;
      document.onmousemove = (e) => {
        div.style.left = e.clientX - disX + 'px'
        div.style.top = e.clientY - disY + 'px'
      }
      document.onmouseup = () => {
        document.onmousemove = null
        document.onmouseup = null
      }
    }
  }
</script>

判断空对象方法

在这里插入图片描述
在这里插入图片描述


闭包及优缺点

在说闭包之前,首先提到变量的作用域。变量的作用域无非就两种: 全局作用域和局部作用域。全局作用域就是直接编写在 script 标签中的代码,而局部作用域就是在函数内部编写的代码。而这个闭包就可以简单理解成“定义在一个函数内部的函数”。闭包有三个特性:
1、函数嵌套。
2、内部函数可以引用外部的参数和变量。
3、参数和变量不会被垃圾回收机制回收。

闭包的优点:让一个变量长期储存在内存中。

闭包的缺点:因为它常驻内存,所以增加了内存的使用量,而且使用不当容易造成内存泄漏。

因此需要注意的是:合理的使用闭包,用完闭包要及时清除。

垃圾回收

首先需要说什么是垃圾,一般来说没有被引用的对象就是垃圾,因为此时我们无法操作该对象。如果这种对象过多,那么它们将会占用大量的内存空间,导致运行变慢。所以这种垃圾必须进行清理。

在JS中,其实拥有自动的垃圾回收机制,会自动地将这些垃圾对象从内存中销毁,而我们能做的就是将不再使用的对象设置为null即可。


DOM事件流和事件委托

在说到DOM事件流和事件委托前,需要说一下事件冒泡和事件捕获。

首先,事件就是浏览器窗口中发生的一些特定的交互瞬间,而事件流描述的是从页面中接收事件的顺序。

首先说到事件冒泡。它的思想:事件开始时由文档中嵌套层次最深的那个节点接收,然后逐级向上传播到嵌套层次最浅的节点。事件将一直冒泡到window对象。
在这里插入图片描述
事件捕获的思想是文档中嵌套层次最浅的那个节点更早的接收到事件,文档中嵌套层次最深的节点应该最后接收到事件。

接下来说到DOM事件流。其中共包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。目标接收到事件后是冒泡阶段,可以在这个阶段对事件做出响应。

事件委托顾名思义:将事件委托给另外的元素。其实就是利用DOM的事件冒泡原理,将事件绑定到目标元素的父节点。如果要为大量的元素节点绑定事件,使用事件委托就可以完美解决,只需要绑定一次,就可以在所有子节点触发事件。

最适合使用事件委托技术的事件包括click、mousedown、mouseup、keydown、keyup和keypress。

对于事件委托的优点:减少事件注册,节省内存。
它的缺点:因为事件委托基于冒泡,所以不支持不冒泡的事件。


深克隆和浅克隆

首先,一个对象里不仅能存放基本属性值,还有可能会存放数组、对象等。

在这里提到一个概念,维度。比如这个对象就是一维的,其中的数组和对象,还要再深一个层级,变成二维。如果再深就三维,以此类推。

浅克隆使用的方法就是for.in或者是使用ES6的方式。
在这里插入图片描述
但是浅克隆只会把第一层克隆下来,也就导致如果操作第二层数据,那么它们的值都会发生改变。那如果我们用到的数据有很多层,而不想修改原有的数据结构,因此就肯定需要使用深克隆。
在这里插入图片描述


函数防抖和节流

推荐文章:函数防抖和节流

在开发的过程中,我们经常需要绑定一些持续触发的事件,如mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中频繁地执行函数。那么防抖和节流就是用来解决这个问题的。

所谓防抖,就是指触发事件后,在n秒内函数只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间。

所谓节流,就是指触发事件后,每隔指定时间执行一次函数。


实现继承的几种方式

推荐文章:实现继承的几种方式

1、原型链方式:将父类的实例作为子类的原型。
在这里插入图片描述


奇怪的JS

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


函数的柯里化

推荐文章:深入详解函数的柯里化函数柯里化

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

只爭朝夕不負韶華

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值