前端面试题 —— JavaScript (二)

目录

一、map和weakMap的区别

二、常用的正则表达式有哪些?

三、对JSON的理解

四、Unicode、UTF-8、UTF-16、UTF-32的区别?

五、常见的位运算符有哪些?其计算规则是什么?

六、escape、encodeURI、encodeURIComponent 的区别

七、常见的DOM操作有哪些

八、use strict是什么意思 ? 使用它区别是什么?

九、什么是尾调用,使用尾调用有什么好处?

十、如何使用for...of遍历对象

十一、ajax、fetch、axios的区别

十二、如何获取安全的 undefined 值?

十三、typeof NaN 的结果是什么?

十四、|| 和 && 操作符的返回值?

十五、+ 操作符什么时候用于字符串的拼接?

十六、扩展运算符的作用及使用场景

十七、Proxy 可以实现什么功能?

十八、对对象与数组的解构的理解

十九、原型链的终点是什么?如何打印出原型链的终点?

二十、如何获得对象非原型链上的属性?

二十一、async/await 如何捕获异常

二十二、并发与并行的区别?

二十三、什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?

二十四、setTimeout、setInterval、requestAnimationFrame 各有什么特点?


一、map和weakMap的区别

(1)Map

map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。

实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:

const map = [
     ["name","张三"],
     ["age",18],
]

Map数据结构有以下操作方法:

  • size: map.size 返回Map结构的成员总数。
  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
  • clear():map.clear()清除所有成员,没有返回值。

Map结构原生提供是三个遍历器生成函数和一个遍历方法:

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历Map的所有成员。
const map = new Map([
     ["foo",1],
     ["bar",2],
])
for(let key of map.keys()){
    console.log(key);  // foo bar
}
for(let value of map.values()){
     console.log(value); // 1 2
}
for(let items of map.entries()){
    console.log(items);  // ["foo",1]  ["bar",2]
}
map.forEach( (value,key,map) => {
     console.log(key,value); // foo 1    bar 2
})

(2)WeakMap

WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。

该对象也有以下几种方法:

  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。

其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。

WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。

而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

总结:

  • Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
  • WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

二、常用的正则表达式有哪些?

(1)匹配 16 进制颜色值

var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;

(2)匹配日期,如 yyyy-mm-dd 格式

var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

(3)匹配 qq 号

var regex = /^[1-9][0-9]{4,10}$/g;

(4)手机号码正则

var regex = /^1[34578]\d{9}$/g;

(5)用户名正则

var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;

三、对JSON的理解

JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。

在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。

因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。

在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理

  • JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
  • JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

四、Unicode、UTF-8、UTF-16、UTF-32的区别?

(1)Unicode

在说Unicode之前需要先了解一下ASCII码:ASCII 码(American Standard Code for Information Interchange)称为美国标准信息交换码。

  • 它是基于拉丁字母的一套电脑编码系统。
  • 它定义了一个用于代表常见字符的字典。
  • 它包含了"A-Z"(包含大小写),数据"0-9" 以及一些常见的符号。
  • 它是专门为英语而设计的,有128个编码,对其他语言无能为力

ASCII码可以表示的编码有限,要想表示其他语言的编码,还是要使用Unicode来表示,可以说Unicode是ASCII的超集。

Unicode 全称 Unicode Translation Format,又叫做统一码、万国码、单一码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

Unicode 的实现方式(也就是编码方式)有很多种,常见的是UTF-8、UTF-16、UTF-32和USC-2。

(2)UTF-8

UTF-8是使用最广泛的Unicode编码方式,它是一种可变长的编码方式,可以是1—4个字节不等,它可以完全兼容ASCII码的128个字符。

注意:UTF-8 是一种编码方式,Unicode是一个字符集合。

UTF-8 的编码规则:

  • 对于单字节的符号,字节的第一位为0,后面的7位为这个字符的Unicode编码,因此对于英文字母,它的Unicode编码和ACSII编码一样。
  • 对于n字节的符号,第一个字节的前n位都是1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的Unicode码 。

来看一下具体的Unicode编号范围与对应的UTF-8二进制格式 :

编码范围(编号对应的十进制数)二进制格式
0x00—0x7F (0-127)0xxxxxxx
0x80—0x7FF (128-2047)110xxxxx 10xxxxxx
0x800—0xFFFF  (2048-65535)1110xxxx 10xxxxxx 10xxxxxx
0x10000—0x10FFFF  (65536以上)11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

那该如何通过具体的Unicode编码,进行具体的UTF-8编码呢?步骤如下:

  • 找到该Unicode编码的所在的编号范围,进而找到与之对应的二进制格式
  • 将Unicode编码转换为二进制数(去掉最高位的0)
  • 将二进制数从右往左一次填入二进制格式的X中,如果有X未填,就设为0

来看一个实际的例子:

马字的Unicode编码是:0x9A6C,整数编号是39532

(1)首选确定了该字符在第三个范围内,它的格式是 1110xxxx 10xxxxxx 10xxxxxx

(2)39532对应的二进制数为1001 1010 0110 1100

(3)将二进制数填入X中,结果是:11101001 10101001 10101100

(3)UTF-16

1. 平面的概念

在了解UTF-16之前,先看一下平面的概念:

Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区存放65536(216)个字符,这称为一个平面,目前总共有17 个平面。

最前面的一个平面称为基本平面,它的码点从0 — 2  16 — 1,写成16进制就是U+0000 — U+FFFF,那剩下的16个平面就是辅助平面,码点范围是 U+10000 — U+10FFFF。

2. UTF-16 概念

UTF-16也是Unicode编码集的一种编码形式,把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位需要1个或者2个16位长的码元来表示,因此UTF-16也是用变长字节表示的。

3. UTF-16 编码规则

  • 编号在 U+0000—U+FFFF 的字符(常用字符集),直接用两个字节表示。
  • 编号在 U+10000—U+10FFFF 之间的字符,需要用四个字节表示。

4. 编码识别

那么问题来了,当遇到两个字节时,怎么知道是把它当做一个字符还是和后面的两个字节一起当做一个字符呢?

UTF-16 编码肯定也考虑到了这个问题,在基本平面内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因此这些空段就可以用来映射辅助平面的字符。

辅助平面共有 220个字符位,因此表示这些字符至少需要 20 个二进制位。UTF-16将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为高位(H),后 10 位映射在 U+DC00 — U+DFFF,称为低位(L)。这就相当于,将一个辅助平面的字符拆成了两个基本平面的字符来表示。

因此,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就可以知道,它后面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。

5. 举例说明

以 "𡠀" 字为例,它的 Unicode 码点为 0x21800,该码点超出了基本平面的范围,因此需要用四个字节来表示,步骤如下:

  • 首先计算超出部分的结果:0x21800 - 0x10000
  • 将上面的计算结果转为20位的二进制数,不足20位就在前面补0,结果为:0001000110 0000000000
  • 将得到的两个10位二进制数分别对应到两个区间中
  • U+D800 对应的二进制数为 1101100000000000, 将0001000110填充在它的后10 个二进制位,得到1101100001000110,转成 16 进制数为 0xD846。同理,低位为 0xDC00,所以这个字的UTF-16编码为 0xD846 0xDC00

(4) UTF-32

UTF-32 就是字符所对应编号的整数二进制形式,每个字符占四个字节,这个是直接进行转换的。该编码方式占用的储存空间较多,所以使用较少。

比如“马” 字的Unicode编号是:U+9A6C,整数编号是39532,直接转化为二进制:1001 1010 0110 1100,这就是它的UTF-32编码。

(5)总结

Unicode、UTF-8、UTF-16、UTF-32有什么区别?

  • Unicode 是编码字符集(字符集),而UTF-8、UTF-16、UTF-32是字符集编码(编码规则);
  • UTF-16 使用变长码元序列的编码方式,相较于定长码元序列的UTF-32算法更复杂,甚至比同样是变长码元序列的UTF-8也更为复杂,因为其引入了独特的代理对这样的代理机制;
  • UTF-8需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力教强;
  • 如果字符内容全部英文或英文与其他文字混合,但英文占绝大部分,那么用UTF-8就比UTF-16节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16就占优势了,可以节省很多空间;

五、常见的位运算符有哪些?其计算规则是什么?

现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。

常见的位运算有以下几种:

运算符描述运算规则
&两个位都为1时,结果才为1 
|两个位都为0时,结果才为0
^异或两个位相同为0,相异为1
~取反0变1,1变0
<<左移各二进制位全部左移若干位,高位丢弃,低位补0
>>右移各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃

六、escape、encodeURI、encodeURIComponent 的区别

  • encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。
  • encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。
  • escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。

七、常见的DOM操作有哪些

DOM 节点的获取

(1)DOM 节点的获取的API及使用:

getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询

按照 id 查询

var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素

按照标签名查询

var pList = document.getElementsByTagName('p')  // 查询到标签为 p 的集合
console.log(divList.length)
console.log(divList[0])

按照类名查询

var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合

按照 css 选择器查询

var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合

(2)DOM 节点的创建

创建一个新节点,并把它添加到指定节点的后面。已知的 HTML 结构如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container">
      <h1 id="title">我是标题</h1>
    </div>  
  </body>
</html>

要求添加一个有内容的 span 节点到 id 为 title 的节点后面,做法就是:

1.首先获取父节点

var container = document.getElementById('container')

2.创建新节点

var targetSpan = document.createElement('span')

3.设置 span 节点的内容

targetSpan.innerHTML = 'hello world'

4.把新创建的元素塞进父节点里去

container.appendChild(targetSpan)

(3)DOM 节点的删除

删除指定的 DOM 节点,已知的 HTML 结构如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container">
      <h1 id="title">我是标题</h1>
    </div>  
  </body>
</html>

需要删除 id 为 title 的元素,做法是:

1.获取目标元素的父元素

var container = document.getElementById('container')

2.获取目标元素

var targetNode = document.getElementById('title')

3.删除目标元素

container.removeChild(targetNode)

或者通过子节点数组来完成删除:

1.获取目标元素的父元素

var container = document.getElementById('container')

2.获取目标元素

var targetNode = container.childNodes[1]

3.删除目标元素

container.removeChild(targetNode)

(4)修改 DOM 元素

修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。

将指定的两个 DOM 元素交换位置,已知的 HTML 结构如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container">
      <h1 id="title">我是标题</h1>
      <p id="content">我是内容</p>
    </div>  
  </body>
</html>

现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:

1.获取父元素

var container = document.getElementById('container')  

2.获取两个需要被交换的元素

var title = document.getElementById('title')
var content = document.getElementById('content')

3.交换两个元素,把 content 置于 title 前面

container.insertBefore(content, title)

八、use strict是什么意思 ? 使用它区别是什么?

use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:

  • 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;
  • 消除代码运行的不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫。

区别:

  • 禁止使用 with 语句。
  • 禁止 this 关键字指向全局对象。
  • 对象不能有重名的属性。

九、什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

十、如何使用for...of遍历对象

for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。

如果需要遍历的对象是类数组对象,用Array.from转成数组即可。

var obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(var k of obj){
    console.log(k)
}

如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可。

//方法一:
var obj = {
    a:1,
    b:2,
    c:3
};

obj[Symbol.iterator] = function(){
    var keys = Object.keys(this);
    var count = 0;
    return {
        next(){
            if(count<keys.length){
                return {value: obj[keys[count++]],done:false};
            }else{
                return {value:undefined,done:true};
            }
        }
    }
};

for(var k of obj){
    console.log(k);
}





// 方法二
var obj = {
    a:1,
    b:2,
    c:3
};
obj[Symbol.iterator] = function*(){
    var keys = Object.keys(obj);
    for(var k of keys){
        yield [k,obj[k]]
    }
};

for(var [k,v] of obj){
   console.log(k,v);
}

十一、ajax、fetch、axios的区别

(1)AJAX

Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式[网页]应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

(2)Fetch

fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。

fetch的优点:

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 更加底层,提供的API丰富(request, response)
  • 脱离了XHR,是ES规范里新的实现方式

fetch的缺点:

  • fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
  • fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
  • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以

(3)Axios

Axios 是一种基于Promise封装的HTTP客户端,其特点如下:

  • 浏览器端发起XMLHttpRequests请求
  • node端发起http请求
  • 支持Promise API
  • 监听请求和返回
  • 对请求和返回进行转化
  • 取消请求
  • 自动转换json数据
  • 客户端支持抵御XSRF攻击

十二、如何获取安全的 undefined 值?

因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。

十三、typeof NaN 的结果是什么?

NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

typeof NaN; // "number"

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。

十四、|| 和 && 操作符的返回值?

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果。

十五、+ 操作符什么时候用于字符串的拼接?

根据 ES5 规范:

  • 如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作
  • 如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用 [[DefaultValue]],以数字作为上下文
  • 如果不能转换为字符串,则会将其转换为数字类型来进行计算

简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤最终得到字符串),则执行字符串拼接,否则执行数字加法。

那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字。

十六、扩展运算符的作用及使用场景

(1)对象扩展运算符

对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。

let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }

上述方法实际上等价于:

let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。

同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

let bar = {a: 1, b: 2};
let baz = {...bar, ...{a:2, b: 4}};  // {a: 2, b: 4}

利用上述特性就可以很方便的修改对象的部分属性。在redux中的reducer函数规定必须是一个纯函数,reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。

需要注意:扩展运算符对对象实例的拷贝属于浅拷贝。

(2)数组扩展运算符

数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。

console.log(...[1, 2, 3])    // 1 2 3
console.log(...[1, [2, 3, 4], 5])    // 1 [2, 3, 4] 5

下面是数组的扩展运算符的应用:

  •  将数组转换为参数序列
function add(x, y) {
  return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
  • 复制数组
const arr1 = [1, 2];
const arr2 = [...arr1];
  • 合并数组

如果想在数组内合并数组,可以这样:

const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five'];
// ["one", "two", "three", "four", "five"]
  • 扩展运算符与解构赋值结合起来,用于生成数组
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

const [...rest, last] = [1, 2, 3, 4, 5];         // 报错
const [first, ...rest, last] = [1, 2, 3, 4, 5];  // 报错
  • 将字符串转为真正的数组
[...'hello']    // [ "h", "e", "l", "l", "o" ]
  • 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组

比较常见的应用是可以将某些数据结构转为数组:

  • // arguments对象
    function foo() {
      const args = [...arguments];
    }

    用于替换es5中的Array.prototype.slice.call(arguments)写法。

  • 使用Math函数获取数组中特定的值
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9

十七、Proxy 可以实现什么功能?

在 Vue3.0 中通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。

Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。

let p = new Proxy(target, handler)

target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

 下面来通过 Proxy 来实现一个数据响应式:

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
      setBind(value, property)
      return Reflect.set(target, property, value)
    }
  }
  return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`监听到属性${property}改变为${v}`)
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`)
  }
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2

在上述代码中,通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。

十八、对对象与数组的解构的理解

解构是 ES6 提供的一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。

(1)数组的解构

在解构数组时,以元素的位置为匹配条件来提取想要的数据的:

const [a, b, c] = [1, 2, 3]

最终,a、b、c分别被赋予了数组第0、1、2个索引位的值:

数组里的0、1、2索引位的元素值,精准地被映射到了左侧的第0、1、2个变量里去,这就是数组解构的工作模式。还可以通过给左侧变量数组设置空占位的方式,实现对数组中某几个元素的精准提取:

const [a,,c] = [1,2,3]

通过把中间位留空,可以顺利地把数组第一位和最后一位的值赋给 a、c 两个变量:

(2)对象的解构

对象解构比数组结构稍微复杂一些,也更显强大。在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。现在定义一个对象:

const stu = {
  name: 'Bob',
  age: 24
}

假如想要解构它的两个自有属性,可以这样:

const { name, age } = stu

这样就得到了 name 和 age 两个和 stu 平级的变量:

注意,对象解构严格以属性名作为定位依据,所以就算调换了 name 和 age 的位置,结果也是一样的:

const { age, name } = stu

十九、原型链的终点是什么?如何打印出原型链的终点?

由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null        // true,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__。

二十、如何获得对象非原型链上的属性?

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:

function iterate(obj){
   var res=[];
   for(var key in obj){
        if(obj.hasOwnProperty(key))
           res.push(key+': '+obj[key]);
   }
   return res;
}

二十一、async/await 如何捕获异常

async function fn(){
    try{
        let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

二十二、并发与并行的区别?

  • 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
  • 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

二十三、什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?

以下代码就是一个回调函数的例子:

ajax(url, () => {
    // 处理逻辑
})

回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,可能会有如下代码:

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})

以上代码看起来不利于阅读和维护,当然,也可以把函数分开来写:

function firstAjax() {
  ajax(url1, () => {
    // 处理逻辑
    secondAjax()
  })
}
function secondAjax() {
  ajax(url2, () => {
    // 处理逻辑
  })
}
ajax(url, () => {
  // 处理逻辑
  firstAjax()
})

以上的代码虽然看上去利于阅读了,但是还是没有解决根本问题。回调地狱的根本问题就是:

  1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  2. 嵌套函数一多,就很难处理错误

当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。

二十四、setTimeout、setInterval、requestAnimationFrame 各有什么特点?

异步编程当然少不了定时器了,常见的定时器函数有 setTimeout、setInterval、requestAnimationFrame。最常用的是setTimeout,很多人认为 setTimeout 是延时多久,那就应该是多久后执行。

其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,可以通过代码去修正 setTimeout,从而使定时器相对准确:

let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
  count++
  // 代码执行所消耗的时间
  let offset = new Date().getTime() - (startTime + count * interval);
  let diff = end - new Date().getTime()
  let h = Math.floor(diff / (60 * 1000 * 60))
  let hdiff = diff % (60 * 1000 * 60)
  let m = Math.floor(hdiff / (60 * 1000))
  let mdiff = hdiff % (60 * 1000)
  let s = mdiff / (1000)
  let sCeil = Math.ceil(s)
  let sFloor = Math.floor(s)
  // 得到下一次循环所消耗的时间
  currentInterval = interval - offset
  console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval)
  setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

接下来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。

通常来说不建议使用 setInterval。第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题,请看以下伪代码

function demo() {
  setInterval(function(){
    console.log(2)
  },1000)
  sleep(2000)
}
demo()

以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。

如果有循环定时器的需求,其实完全可以通过 requestAnimationFrame 来实现:

function setInterval(callback, interval) {
  let timer
  const now = Date.now
  let startTime = now()
  let endTime = startTime
  const loop = () => {
    timer = window.requestAnimationFrame(loop)
    endTime = now()
    if (endTime - startTime >= interval) {
      startTime = endTime = now()
      callback(timer)
    }
  }
  timer = window.requestAnimationFrame(loop)
  return timer
}
let a = 0
setInterval(timer => {
  console.log(1)
  a++
  if (a === 3) cancelAnimationFrame(timer)
}, 1000)

首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

想散在风中

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

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

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

打赏作者

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

抵扣说明:

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

余额充值