js面试题

Jsonp vs. CORS

  1. 定义: 是j son with padding (填充式json),利用了使用src引用静态资源时不受跨域限制的机制。主要在客户端搞一个回调做一些数据接收与操作的处理,并把这个回调函数名告知服务端,而服务端需要做的是按照javascript的语法把数据放到约定好的回调函数之中即可。jQuery很早之前就已经把JSONP语法糖化了,使用起来会更加方便。

    CORS(Cross-origin resource sharing 跨域资源共享),依附于AJAX ,通过添加HTTP Hearder部分字段请求与获取有权限访问的资源。CORS对开发者是透明的,因为浏览器会自动根据请求的情况(简单和复杂)做出不同的处理。CORS的关键是服务端的配置支持。由于CORS是W3C中一项较“新”的方案,以至于各大网页解析引擎还没有对其进行严格规格的实现,所以不同引擎下可能会有一些不一致。

    CORS背后的基本思想是使用自定义的HTTP头部允许浏览器和服务器相互了解对方,比如设置:

    Access-Control-Allow-Origin:指定授权访问的域
    Access-Control-Allow-Methods:授权请求的方法(GET, POST, PUT, DELETE,OPTIONS等)

    从而决定请求或响应成功与否; CORS 并不是为了解决服务端安全问题,而是为了解决如何跨域调用资源。

  2. 优缺点

    1. 它只支持GET请求而不支持POST等其它类型的HTTP请求

    2. 在调用失败的时候不会返回各种HTTP状态码。安全性。万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。那么结果是什么?所有调用这个 jsonp的网站都会存在漏洞。于是无法把危险控制在一个域名下…所以在使用jsonp的时候必须要保证使用的jsonp服务必须是安全可信的。

    3. JSONP的主要优势在于对浏览器的支持较好;虽然目前主流浏览器支持CORS,但IE10以下不支持CORS。

      JSONP只能用于获取资源(即只读,类似于GET请求);CORS支持所有类型的HTTP请求,功能完善。(这点JSONP被完虐,但大部分情况下GET已经能满足需求了)

      JSONP的错误处理机制并不完善,我们没办法进行错误处理;而CORS可以通过onerror事件监听错误,并且浏览器控制台会看到报错信息,利于排查。

      JSONP只会发一次请求;而对于复杂请求,CORS会发两次请求。

      始终觉得安全性这个东西是相对的,没有绝对的安全,也做不到绝对的安全。毕竟JSONP并不是跨域规范,它存在很明显的安全问题:callback参数注入和资源访问授权设置。CORS好歹也算是个跨域规范,在资源访问授权方面进行了限制(Access-Control-Allow-Origin),而且标准浏览器都做了安全限制,比如拒绝手动设置origin字段,相对来说是安全了一点。
      但是回过头来看一下,就算是不安全的JSONP,我们依然可以在服务端端进行一些权限的限制,服务端和客户端也都依然可以做一些注入的安全处理,哪怕被攻克,它也只能读一些东西。就算是比较安全的CORS,同样可以在服务端设置出现漏洞或者不在浏览器的跨域限制环境下进行攻击,而且它不仅可以读,还可以写。

    js事件绑定

    三种方式:

    1. 在DOM元素中属性绑定;
    2. 在JavaScript代码中绑定;
    3. 绑定事件监听函数。

JWT TOken

分为三部分——头、负载、签名:全部使用base64编码,之间用.链接在一起,第一部分是加密算法说明,第二部分是携带信息

线程 和进程的区别

进程是资源分配的最小单位,线程是CPU调度的最小单位

一个程序就是一个进程,一个进程里面可以有很多线程

js单线程的优势为:切换开销小,不会引起复杂的同步问题。新标准(HTML5)提出了web worker标准,允许有多线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

js事件循环

js在执行过程中存在执行栈:

什么是执行栈:当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。

还有一个任务队列:

当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。

宏任务和微任务:

微任务永远在宏任务之前执行。

  • 宏任务 浏览器 Node
    setTimeout
    setInterval
    setImmediate x
    requestAnimationFrame x
    微任务 浏览器 Node
    process.nextTick x
    MutationObserver x
    Promise.then catch finally

window.onload和$(document).ready的区别

1.执行时间

window.onload必须等到页面内包括图片的所有元素加载完毕后才能执行。
$(document).ready()是DOM结构绘制完毕后就执行,不必等到加载完毕。

2.编写个数不同

window.onload不能同时编写多个,如果有多个window.onload方法,只会执行一个
$(document).ready()可以同时编写多个,并且都可以得到执行

3.简化写法

window.onload没有简化写法
\((document).ready(function(){})可以简写成\)(function(){});

<script>中的defer属性和async属性的区别

正常情况下浏览器执行到script标签就会停止页面的渲染,先下载因后执行该代码,为js是单线程。

加了defer属性后,浏览器会先下载,下载中js主线程对页面的处理不停止;等到文档解析完成后脚本才会执行(解析完成后, 在DOMContentLoaded之前)

加了async属性,浏览器会先下载,下载中js主线程对页面的处理不停止;下载后立即执行,执行过程中页面处理会停止

两个都有时,优先遵从async属性

当有了async属性的时候,两个脚本的执行顺序是不能确定的

因此,如果脚本依赖于dom,则用defer,否则async更快

浏览器的渲染是如何构建渲染树的

解析html模版生成ast,然后构建dom树,然后构建css树,把css的属性合成到dom树上,就变成了渲染树。根据渲染树浏览器会对它们进行重排(又叫做回流)和重绘

浏览器缓存cookie的控制缓存的属性

maxAge,domain,path(某个域名下,限定访问特定的目录/路由)

js的原型链

⚠️ 原型链是根据__proto__查找的

![image-20210828202607137](/Users/bravojack/Library/Application Support/typora-user-images/image-20210828202607137.png)

![image-20210828202545264](/Users/bravojack/Library/Application Support/typora-user-images/image-20210828202545264.png)

![image-20210828204702602](/Users/bravojack/Library/Application Support/typora-user-images/image-20210828204702602.png)

Person就像一个Machine, 它有一个模具,就是Person.prototype, 那么这个模具的constructor顺理成章的就应该是这个Machine—— Person构造函数了。

那么这个Machine的产品——person外包装上面自然有个标签,这个标签就是__proto__说明了这个产品的模型或者说模具是什么,当然是Person.prototype

js深拷贝和浅拷贝

直接上代码:

// 深拷贝
function deepClone(target){
  if(typeof target == 'object'){
    let ret = Array.isArray(target)?[]: {}
    for(let key of target){
      ret[key] = deepClone(target[key])
      return ret
    }
  }else{
    return target
  }
}

Vue双向绑定原理

Vue 3.0与Vue 2.0的区别仅是数据劫持的方式由Object.defineProperty更改为Proxy代理,其他代码不变

6.<keep-alive></keep-alive>的作用是什么?
答:keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。

10.为什么使用key?
答:需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点。
作用主要是为了高效的更新虚拟DOM。

**16.\(nextTick的使用** 答:当你修改了data的值然后马上获取这个dom元素的值,是不能获取到更新后的值, 你需要使用\)nextTick这个回调,让修改后的data值渲染更新到dom元素之后在获取,才能成功。

17.vue组件中data为什么必须是一个函数?
答:样每次复用组件的时候,都会返回一份新的data,相当于每个组件实例都有自己私有的数据空间,它们只负责各自维护的数据,不会造成混乱。而单纯的写成对象形式,就是所有的组件实例共用了一个data,这样改一个全都改了。(类似于函数闭包)

20.单页面应用和多页面应用区别及优缺点
答:单页面应用(SPA),通俗一点说就是指只有一个主页面的应用,浏览器一开始要加载所有必须的 html, js, css。所有的页面内容都包含在这个所谓的主页面中。但在写的时候,还是会分开写(页面片段),然后在交互的时候由路由程序动态载入,单页面的页面跳转,仅刷新局部资源。多应用于pc端。
多页面(MPA),就是指一个应用中有多个页面,页面跳转时是整页刷新
单页面的优点:
用户体验好,快,内容的改变不需要重新加载整个页面,基于这一点spa对服务器压力较小;前后端分离;页面效果会比较炫酷(比如切换页面内容时的专场动画)。
单页面缺点:
不利于seo;导航不可用,如果一定要导航需要自行实现前进、后退。(由于是单页面不能用浏览器的前进后退功能,所以需要自己建立堆栈管理);初次加载时耗时多;页面复杂度提高很多。

21.v-if和v-for的优先级
答:当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中。所以,不推荐v-if和v-for同时使用。
如果v-if和v-for一起用的话,vue中的的会自动提示v-if应该放到外层去。

23.vue常用的修饰符
答:.stop:等同于JavaScript中的event.stopPropagation(),防止事件冒泡;
.prevent:等同于JavaScript中的event.preventDefault(),防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
.capture:在事件的捕获阶段就进行触发
.self:只会触发自己范围内的事件,不包含子元素;
.once:只会触发一次。

24.vue的两个核心点
答:数据驱动、组件系统
数据驱动:ViewModel,保证数据和视图的一致性。
组件系统:应用类UI可以看作全部是由组件树构成的。

25.vue和jQuery的区别重点!!!!!
答:jQuery是使用选择器(\()选取DOM对象,对其进行赋值、取值、事件绑定等操作,其实和原生的HTML的区别只在于可以更方便的选取和操作DOM对象,而数据和界面是在一起的。比如需要获取label标签的内容:\)("lable").val();,它还是依赖DOM元素的值。
Vue则是通过Vue对象将数据和View完全分离开来了。对数据进行操作不再需要引用相应的DOM对象,可以说数据和View是分离的,他们通过Vue对象这个vm实现相互的绑定。这就是传说中的MVVM。

28.SPA首屏加载慢如何解决
答:安装动态懒加载所需插件;使用CDN资源。

29.Vue-router跳转和location.href有什么区别
答:使用location.href='/url'来跳转,简单方便,但是刷新了页面;
使用history.pushState('/url'),无刷新页面,静态跳转;
引进router,然后使用router.push('/url')来跳转,使用了diff算法,实现了按需加载,减少了dom的消耗。
其实使用router跳转和使用history.pushState()没什么差别的,因为vue-router就是用了history.pushState(),尤其是在history模式下。

37.params和query的区别
答:用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是this.\(route.query.name和this.\)route.params.name。
url地址显示:query更加类似于我们ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示
注意点:query刷新不会丢失query里面的数据
params刷新 会 丢失 params里面的数据。

38.vue初始化页面闪动问题
答:使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是我们还是有必要让解决这个问题的。
首先:在css里加上[v-cloak] {
display: none;
}。

1.mvvm 框架是什么?
答:vue是实现了双向数据绑定的mvvm框架,当视图改变更新模型层,当模型层改变更新视图层。在vue中,使用了双向绑定技术,就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。

**6.$route 和 \(router 的区别** 答:\)router是VueRouter的实例,在script标签中想要导航到不同的URL,使用\(router.push方法。返回上一个历史history用\)router.to(-1)
$route为当前router跳转对象。里面可以获取当前路由的name,path,query,parmas等。

7.vue-router的两种模式
答:hash模式:即地址栏 URL 中的 # 符号;
history模式:window.history对象打印出来可以看到里边提供的方法和记录长度。利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)。

VUE核心

  1. 什么是Reflect?

    Reflect是ES6为了操作对象而新增的API, 为什么要添加Reflect对象呢?它这样设计的目的是为了什么?

    1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上,那么以后我们就可以从Reflect对象上可以拿到语言内部的方法。

    2)在使用对象的 Object.defineProperty(obj, name, {})时,如果出现异常的话,会抛出一个错误,需要使用try catch去捕获,但是使用 Reflect.defineProperty(obj, name, desc) 则会返回false。

  2. reactive原理:

    const proxy = new Proxy(target, reactiveHandler)
    const readonlyHandler = {
      get (target, key) {
        if (key==='_is_readonly') return true
    
        return Reflect.get(target, key)
      },
      set () {
        console.warn('只读的, 不能修改')
        return true
      },
      deleteProperty () {
        console.warn('只读的, 不能删除')
        return true
      },
    }
    
  3. vue compile的过程

    Compile主要分为3大块:

    1. parse 接受template原始模板,按着模板的节点和数据生成对应的ast
    2. optimize 遍历ast的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,减少去对比这部分DOM,提升性能
    3. generate 把前两步生成完善的ast,组成render字符串,然后将render字符串通过new Function的方式转换成渲染函数

如何解决移动端300毫秒延迟

  1. 禁用缩放

<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">
  1. 更改默认的视口宽度

还是在HTML文档头部添加如下meta标签

<meta name="viewport" content="width=device-width">

方法三、CSS touch-action

html {
    touch-action: none
}

这是个CSS属性,如果值为 none 的话, 就表示禁用掉该元素上的浏览器代理的任何默认行为(缩放,移动, 拖拽),无300ms延迟。你也可以在html标签上面设置该属性,虽然该方法实现了禁用缩放。但是其他的触摸类型交互事件都被禁用了。导致页面无法滚动。

方法四、FastClick.js

熟悉移动端开发的同学,相信都见过这个轻量级库文件。我们引入库文件之后,添加如下代码就可以了。

window.addEventListener( "load", function() {
FastClick.attach( document.body );
}, false );

那FastClick的原理是什么呢? 其实就是FastClick在检测到touchend事件之后,会通过 DOM 自定义事件立即触发一个模拟 click 事件,并把浏览器在 300 毫秒之后真正触发的 click 事件阻止掉。

点击穿透

什么是点击穿透
假如页面上有两个元素A和B。B元素在A元素之上。我们在B元素的touchstart事件上注册了一个回调函数,该回调函数的作用是隐藏B元素。我们发现,当我们点击B元素,B元素被隐藏了,随后,A元素触发了click事件。

这是因为在移动端浏览器,事件执行的顺序是touchstart > touchend > click。而click事件有300ms的延迟,当touchstart事件把B元素隐藏之后,隔了300ms,浏览器触发了click事件,但是此时B元素不见了,所以该事件被派发到了A元素身上。如果A元素是一个链接,那此时页面就会意外地跳转。

var 与let的区别

  1. 作用域不同:

    由于var的变量提升,在函数中声明了var,整个函数内都是有效的,比如说在for循环内定义的一个var变量,实际上其在for循环以外也是可以访问的;而let由于是块作用域,所以如果在块作用域内定义的变量,比如说在for循环内,在其外面是不可被访问的,所以for循环推荐用let

  2. let不能被重新定义,但是var是可以的

  3. let必须先声明,在使用。而var先使用后声明也行,只不过直接使用但没有定义的时候,其值是undefined。

js反转一个字符串

'hello world'.split('').reverse().join('')

js 如何解决继承问题

有待完善......

小程序和h5的区别

官方文档表明脚本内无法使用浏览器中常用的window对象和document对象(基于这一点,像zepto/jquery这种操作dom的库就被完全抛弃了)。

小程序: 运行更加流畅,布局对移动端更加优化; 小程序的宿主App给小程序提供了登录认证,支付等能力;更加便捷: 和h5的url获取相比,用户寻找更快

防抖和截流

防抖debounce:维护一个计时器,delay后触发,delay时间内触发则顺延,最后一次执行

截流throttle: 判断是否有计时器,若有则跳过,无则设置一个计时器,延期执行

// debounce

// throttle

Promise的all和race

all是可迭代对象里面所有的都变成resolved才会执行下一步,race是只要有一个resolved,就会执行下一步

如果Promise.all方法和Promise.race方法的参数,不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

前端内存处理

js有着内存的垃圾回收机制,其算法有以下几种:

  1. 引用计数法(存在循环饮用导致无法回收的问题)

  2. 标记清除法:算法会检测所有的根变量及他们的后代变量并标记它们为激活状态(表示它们不可回收)。任何根变量所到达不了的变量(或者对象等等)都会被标记为内存垃圾。垃圾回收器会释放所有非激活状态的内存片段然后返还给操作系统。

    • 根:一般来说,根指的是代码中引用的全局变量。就拿 JavaScript 来说,window 对象即是根的全局变量。Node.js 中相对应的变量为 "global"。垃圾回收器会构建出一份所有根变量的完整列表。

    img

    四种常见的 JavaScript 内存泄漏

    1: 全局变量:

    ​ 在js中,当引用一个未声明的变量的时候,会在全局对象上创建一个新的变量。在浏览器中是window。这意味着如下代码:

    function foo(arg) {
      bar = "some text";
      this.foo = "some text"
    }
    

    等同于:

    function foo(arg) {
       window.bar = "some text";
    	 window.foo = "some text";
    }
    

    2. 闭包

    3. 计时器

    4. 保存的dom元素(即使html中的dom已经被清除的时候)

    GC比较耗时,优化内存的方法:

    1. 由于GC的触发是不固定的,一般在向浏览器申请新内存时,浏览器会检测是否到达一个临界值再进行触发。所以我们可以减少内存使用,另外若能避免申请新内存,则可避免GC 触发。
    2. ImageData 对象是 JS 内存杀手,避免重复创建 ImageData 对象。
    3. 重复使用 ArrayBuffer。
    4. 压缩图片、按需加载图片、按需渲染图片,使用恰当的图片尺寸、图片格式,如 WebP 格式。

什么是304缓存

客户端在请求一个文件的时候,发现自己缓存的文件有 Last Modified字段(服务端添加上去的) ,那么在请求中会包含 If Modified Since字段

对于静态文件,例如:CSS、图片,服务器会自动完成 Last Modified 和 If Modified Since 的比较,完成缓存(返回304)或者更新(返回200)

虽然在返回 304 的时候已经做了一次数据库查询,但是可以避免接下来更多的数据库查询,并且没有返回页面内容而只是一个 HTTP Header,从而大大的降低带宽的消耗,对于用户的感觉也是提高。

ETag是什么,怎么产生的

etag是http header的一个字段,用来表示资源是否被修改过等信息,通常是个哈希值。在Nginx中是lastModified和content-length的哈希值的拼写

cookie和token的区别:

  1. token是无状态的,后端服务不需要记录token
  2. 可以跨域,而cookie只能绑定到单个域名
  3. 不需要查询数据库(cookie在服务端数据库存储着),性能提升
  4. 能防止CSRF(跨站请求伪造)

箭头函数和普通函数的10个区别

  1. 没有this,this指向window

  2. 不能使用new

  3. 不绑定arguments,用展开运算符代替,如下

    // 常规函数
    function test1(a){
      console.log(arguments);
    }
    test(1) // 1
    // 箭头函数
    var test2 = (a)=>{console.log(arguments)}  //ReferenceError: arguments is not defined
    
    let test3=(...a)=>{console.log(a[1])} 
    
    test3(33,22,44); // 22
    
  4. 会捕获其上下文的this值,作为自己的this值

  5. 箭头函数没有prototype原型属性

  6. 返回对象字面量需要用小括号包裹起来let fun5 = ()=>({ foo: x }) //如果x => { foo: x } //则语法出错

事件委托

利用事件冒泡的原理,在父元素上绑定事件处理函数,优点是可以大大的减少dom操作。另外,如果在每个子元素中都绑定事件处理函数,不仅内存开销大,而且新增的子元素也不会自动添加事件处理函数,那么使用事件委托就可以对新的子元素也能捕获到事件。

null和undefined的区别:

null是一个表示"无"的对象,转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

null表示"没有对象"

(1) 作为函数的参数,表示该函数的参数不是对象。

(2) 作为对象原型链的终点。

(3)typeof null == object

undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

(1)变量被声明了,但没有赋值时,就等于undefined。

(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。

(3)对象没有赋值的属性,该属性的值为undefined。

(4)函数没有返回值时,默认返回undefined。

js的继承

JavaScript实现继承共6种方式:

原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。

构造函数继承是每次继承都会把父类的所有属性方法全部拷贝一份,而对于公用的方法重复拷贝会浪费内存

原型链继承所有对象都公用一份原型属性和方法,对一个类的修改回影响的其他类

组合继承是结合两种继承方式,用构造函数方式继承属性,原型链方式继承方法

原型继承了解吗?我给你个场景,有一个FUNA,FUNB,让B继承A,用原型链怎么B继承A的属性?

  function Person(){
    this.inside = 'inside'
    this.saySecret = function(){
      console.log(this.inside)
    }
  }
  Person.prototype.hello = "hello";
  Person.prototype.sayHello = function(){
    console.log(this.hello);
  }
  

  function Child(){}
  Child.prototype = new Person()

  let child = new Child()
  child.sayHello() // hello
  child.saySecret()// inside
  console.log(child.inside) // inside

判断一个对象是否为空的方法:

  1. 使用ES6新增语法:Object.keys(testObj)返回一个array类型,里面是testObj中用户定义的键值。
  2. 使用JSON.stringify if(JSON.stringify(testObj)=='{}'){ console.log('是空对象!') }
  3. 使用for...in遍历,能够遍历到任何内容的就不为空

为什么0.1 + 0.2 != 0.3

计算时,js解释器将十进制0.1转换为2进制的时候,二进制的表达是无穷的,所以计算结果是不精确的

// 将0.1转换成二进制
console.log(0.1.toString(2)); // 0.0001100110011001100110011001100110011001100110011001101

Number() 的存储空间是多大

是2的53次方,如果超出的话,会发生截断

JS中构造函数和普通函数的区别

  1. 构造函数也是一个普通函数,创建方式和普通函数一样,但构造函数习惯上首字母大写

  2. 调用方式不一样

  3. 构造函数的执行流程以 new Func()为例

    ​ A、立刻在堆内存中创建一个新的对象 {}

    B、将新建的对象设置为函数中的thisFunc.call({})

    C、逐个执行函数中的代码 {}.name=xxx; {}.methods = xxx

    D、将新建的对象作为返回值 return {}

    function person(){}
    let per = person()
    console.log(per) // "undefined"
    
    function Person(){}
    let per = new Person()
    console.log(per) // "[object object]"
    

js正则表达式

常用的正则表达式方法有:

// 1. search() 方法 用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。
str.search(/Runoob/i); // i 表示不区分大小写

// 2. replace() 方法 用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
var txt = str.replace(/microsoft/i,"Runoob");

// 3. test() 方法用于检测一个字符串是否匹配某个模式,如果字符串中含有匹配的文本,则返回 true,否则返回 false。
var patt = /e/;
patt.test("The best things in life are free!"); // expect output



Array.map和Array.forEach

  1. Array.map()和Array.forEach((value, index,array)=>{})都不会改变原数组,map返回新的处理后的对象,而forEach会在遍历的时候做一些其他的事情
posted @ 2021-09-23 10:01  Bravo_Jack  阅读(692)  评论(0编辑  收藏  举报