面经

220 阅读25分钟

文章参考vue样式穿透的几种方式 - 掘金 (juejin.cn)

1. flex布局

给任何一个元素设置display:flex之后,这个元素就变成了容器,他的子元素(注意不是后代元素)就会自动成为成员(项目)。容器默认有两个轴:水平轴和垂直轴。

容器属性

flex-direction 属性决定主轴的方向,可以通过这个属性来设置容器的排列。
flex-wrap 属性当主轴排列不下去的时候,可以设置换行
justify-content 属性定义了项目在主轴上的排列方式,可以设置左、右对齐、居中、项目两端之间对齐、项目距离相等
align-items属性定义项目在交叉轴上如何对齐,可以设置起点对齐、终点对齐、中点对齐、项目的第一行文字对齐、如果项目未设置高度或设为auto将占满整个容器的高度 align-content 属性适用于发生换行,主轴方向上多个轴,用于调整位置,如果主轴方向上只有一个轴,那么该属性就不生效。可用于设置与交叉轴的起点、终点、中点、两端对齐、每根轴线两侧的间隔都相等

项目属性

order属性用来定义项目的排序,数字越小排序就越靠前,默认为0
flex-grow属性用来定义项目的放大比例。默认为0,即如果还有剩余空间也不放大。设置为1则每个项目等分剩余空间
flex-shrink属性用来定义项目的缩小比例。默认为1,即剩余空间不足,则该项目缩小。负值对该属性无效
flex-basis属性定义了项目在主轴空间的宽度。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto、,即项目的大小就是本来的大小。当 flex-basis 值为 0 % 时,项目尺寸会被认为是0,因此无论项目尺寸设置多少都用
flex属性flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个为可选值
align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。

2. 原型和原型链

在我们查找属性和对属性赋值的过程中,我们就会用到原型链。

查找属性时,如果在这个对象上找到了这个属性,那么就返回这个对象,如果没有找到这个属性,那么就通过__proto__属性去原型链上查找,如果查找完了整条原型链都没有找到这个属性就会返回undefined。使用in操作符来检查属性的时候也会去查找整个原型链

在给属性设置值时。如果对象中有这个属性,那么就直接修改这个属性。如果这个对象上没有这个属性,那么就会去查找原型链,如果原型链上也没有这个属性,那么就会将这个属性添加到这个对象上。如果这个属性仅仅出现在原型链中,那么就会有三种情况:1.原型链上的属性是普通数据访问符,并且是可读的(writable:true),那么直接在这个对象上添加新属性,这个属性是屏蔽属性。2.如果原型链上的属性是不可读的(writable:false)。严格模式下报错,非严格模式下被忽略。这是为了模仿类的行为。3.如果原型链上的属性是setter,那么就直接调用setter,不会被添加到对象中。

可以通过 a = Object.create(b)的形式,将a.__proto__设置为b。也可以通过new一个实例对象的方式来将两个对象关联起来。

原型链的另一个操作是instanceof,a instanceof b实际上就是查询a的整条原型链中有没有b.prototype对象。

将一个对象通过原型链的形式关联到另一个对象中,这不同于面向类的语言。提到原型链,很多人的第一想法是原型继承,但是我觉得这不是一个很对的叫法,继承意味着把类的行为复制给实例对象,但是我们在使用原型的时候并没有复制,而是让两个对象互相关联,这种行为的名称应该叫做行为委托。将这个行为委托给另一个对象,去查找另一个对象的方法。

3. 防抖与节流

防抖是指事件被触发之后经过一段时间再执行回调,如果在这段时间内重新触发,那么就开始重新计时。防抖可以应用在搜索框停止输入时发送请求,可以在浏览器窗口调整事件触发时停止一段时间再发送请求。

节流是在一定时间内只触发一次回调,可以用来监听滚动事件。节流可以应用在鼠标滚动事件、频繁点击鼠标按钮上面。

实现防抖

function debounce(func,delay=500,immediate){
    if(timeout) clearTimeout(timeout)
    if(immediate){
        let callnow = !timeout
        timeout = setTimeout(() => {
            timeout = null
        },delay)
        if(callnow) func()
    }else{
        timeout = setTimeout(() => {
            func()
        },delay)
    }
}

实现节流

let pre = 0
function throttle(func,wait = 500){
    let now = Data.now()
    if(now - pre > wait){
        func()
        pre = now
    }
}

4. 前端向后端的传递数据的方式

4.1 通过HTTP URL传递参数

这是最常见的传递方式,我们可以将参数直接写在URL上(比如文章id),或者写在QueryString,即?后面

HTTP报文格式

GET /user/1?userName=admin&arrays=a,b,c,d&ids=1&ids=2&ids=3&ids=4

  1. user/1的这个1是通过URL路径传参数,这是RestFul风格的传参方式
  2. userName=admin这种就是简单的QueryString传参,是最常见的
  3. arrays=a,b,c,d这种是通过QueryString传数组,其实就是使用,分隔
  4. ids=1&ids=2&ids=3&ids=4这种也是传数组参数的一种方式

注意事项

  1. 如果传的参数不是URL安全的需要进行URLEncode
  2. POST、PUT、RELETE方法也是支持通过URL传参的
  3. 使用这种URL拼接不同浏览器支持的最大参数长度是不一样的

4.2 通过HTTP Body传参

通过HTTP Body传参数主要用于前端向服务端提交数据,如添加数据、修改数据、上传文件等等。通过Body传参主要有以下3种方式

  1. application/x-www-form-urlencoded :Content-Type设置为该值后,body报文中使用key=value拼接参数
  2. application/json 将数据转化为JSON格式放在Body中
  3. multipart/form-data 用于文件上传

HTTP报文格式

  • application/x-www-form-urlencoded格式报文
POST /user3/1
Content-Type: application/x-www-form-urlencoded

userName=admin&arrays=a,b,c,d&ids=1&ids=2&ids=3&ids=4
  • application/json格式报文
GET /user4/1
Content-Type: application/json

{
    "id": 1,
    "userName": "admin",
    "arrays": [
        "a",
        "b",
        "c",
        "d"
    ],
    "ids": [
        1,
        2,
        3,
        4
    ]
}

注意事项 1.GET方法也可以通过Body传参,不过只能传application/json,我们在搜索数据的时候就是用GET方法传JSON数据的

4.3 通过Header传参数

通过Header传参(这里并不是说有一个参数叫做Header,而是将参数写在报文头部)主要用于一些通用的用户认证信息,比如常用的Authentication: Bearer、Cookie

HTTP报文格式

GET /user7/1
Accept: application/json
userName : admin
Cookie: userName=admin;
arrays: a,b,c,d
ids:1
ids:2
ids:3
ids:4

注意事项

  1. cookie是浏览器自动添加的,不需要通过request.setRequestHeader("userName","admin")添加到header

5. scoped原理和怎么修改第三方库的属性

scoped的vue中实现了组件内样式的私有化,使得各个组件之间的样式互不影响。

scoped的原理是为各个属性生成一个hash值(data-v-xxxxx),编译器将代码编译成虚拟DOM的时候,全都使用属性选择器来给每一个标签添加样式,使得样式之间不会被影响

<div class="page">
  <span class="content">hello world</span>
</div>
<style lang="scss" scoped>
.page {
  .content {
    color: aquamarine;
    font-size: 20px;
  }
}
</style>

会被编译成下面的代码 image.png 最终样式也会使用属性选择器

image.png

但是如果我们需要在父组件中修改子组件的样式,或者引入了第三方库的时候,如果想修改样式的话,我们会发现在scoped的组件中根本修改不了,原因是应该第三方库并没有属性选择器(即data-v-xxxxx)

<el-card class="box-card">
  <span class="content">hello world</span>
</el-card>

我们这里引入了第三方库,如果想使用下面的代码修改样式是无效的

<style lang="scss" scoped>
.box-card {
  .el-card__body {
    padding: 10px;
  }
}
</style>

因为我们的真实代码里的选择器是属性选择器.box-card .el-card__body[data-v-xxxxx]。这时候我们就要使用到样式穿透了。样式穿透可以理解为父级组件强制修改子级组件

css中样式穿透

如果使用的是css,没有使用css预处理器,则可以使用>>>/deep/::v-deep

.wrapper >>> .el-card__header{//el-card__header此时设置的样式一定会生效
    border:none;
}

sass中样式穿透

使用的是less或者node-sass,那么可以使用/deep/::v-deep都可以生效。

6. 事件冒泡和事件捕获

事件冒泡是指当子元素触发事件时,这个事件会向上传递,一直传递到window,如果传递过程中遇到的DOM元素也有相同的事件,那么该元素也会触发该事件。

事件捕获与事件冒泡相反,当元素触发事件时,会从window开始传播一直到该元素,如果这个过程中遇到的DOM元素也有相同的事件,那么也会触发该事件.

addEventListener的第三个参数默认为false,表示会监听冒泡阶段的事件。若为true,则表示监听捕获阶段的事件。

应用场景(事件代理,又名事件委托) 事件代理就是利用事件捕获或者事件冒泡的机制把一系列的内层元素事件绑定到外层元素(这意味着子元素本身将不用注册自己的事件)

<ul @click='handle'>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

function handle(event){
    console.log(event.target.innerText)
}

上面的伪代码就利用了事件冒泡。当我们点击了第一个li时,此时我们的li并没有click事件,然后向上冒泡,到了ul的时候,发现有click事件,于是执行click事件。这里注意,event表示我们当前点击的事件,即便触发click是在ul元素,但是event指向的还是我们点击的第一个li

阻止事件捕获和冒泡
为什么需要阻止事件的冒泡和捕获?
因此当我们嵌套的元素都有各自的click事件时,事件冒泡和捕获就会影响到除了目标元素以外的元素。可以用event.stopPropagation( )来阻止事件捕获或者冒泡

  function fun(event){
      ...
      event.stopPropagation( )
  }

尽量不要用event.stopPropagation()去阻止捕获阶段执行的事件。因为捕获阶段是从外到内,如果我们要触发内部的某一个事件,那么event.stopPropagation()会让浏览器检测在第一层就阻止往下捕获,也就检测不到我们写在内部的事件。

7. Vue的响应式原理

Vue的响应式原理指的是当我们将数据设为响应式之后,当这个数据(状态)发生变化时,Vue会通知到依赖这个数据的所有组件,然后组件内部会使用虚拟DOM来进行比对,然后进行数据的更新,可以帮助开发人员省去了操作DOM的步骤。

对象的响应式原理

1.侦测变化

响应式首先要解决的第一个问题就是侦测数据的变化。在Vue2中使用的是Object.defineProperty,在Vue3中使用的是Proxy。解决侦测数据变化的做法是为每一个对象中的数据都加上getter和setter,当读取data中的key时,触发get函数;修改data中的key时,触发set函数

2.收集依赖

我们在getter和setter里面应该做些什么呢?答案是收集依赖

我们之所以要侦测数据,其目的是当数据的属性发生变化时,可以通知那些曾经使用过该数据的地方。我们要做的就是在getter中收集依赖,在setter中触发依赖。

我们借助ES6中的‘类’class来封装一个Dep类,它的作用是专门帮助我们管理依赖。使用这个类我们可以收集依赖(哪个组件使用了数据),删除依赖或者向依赖发送通知(修改数据)等。Dep类可以简单理解成一个数组,这个数组是用来存储被收集的依赖,当数据触发setter时,循环dep来触发里面的每个依赖

3.什么是依赖

我们现在知道,触发key的setter时,会将依赖收进Dep中;当修改key的属性时,会循环执行这个key的所有依赖。那依赖是什么?

依赖也是一个我们抽象出来的类,可以称为Watcher。因为使用了这个数据的地方很多,而且类型还不一样(可能是模板,也可能是用户写的watch),因此我们要抽象出一个可以集中处理这些情况的类。我们在收集依赖阶段只收集这个封装好的类的实例进来,通知也只通知它一个。

换句话说,Watcher是一个中介角色,数据变化的时候通知它,然后它再通知其他地方。可以理解为再Watcher中传入组件实例,然后组件实例里有修改DOM的方法,因此可以通过这个类来修改组件。

4.单个属性的响应式过程

在类Watcher的实现中,一旦属性触发了getter,那么就会触发收集依赖的了逻辑,会从Watcher中读取一个依赖添加到Dep中。当数据发生变化时,会触发Dep中的依赖,然后通过Watcher来通知具体的组件来修改

5.递归侦测所有key

现在,我们已经可以实现变化侦测的功能了,但是前面介绍的只能侦测对象中的一个属性,我们希望把data中的所有数据都能侦测到,因此我们要封装一个类Observer,这个类的作用是将data里的所有属性(包括子属性)都转换为getter和setter形式,然后去跟踪他们的变化。这样,我们就实现了Object类型的响应式。

6.Vue2的响应式问题

Vue2中是通过Object.defineProperty将对象的key转换为setter和getter实现了数据的追踪和拦截,但getter和setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,所以会导致我们给一个数组添加新的属性时,会失去响应式

但这是没有办法的事情,在ES6前,JavaScript没有提供元编程(元编程可以拦截原型方法)的能力,无法侦测一个属性被添加或者删除。Vue2提供了vm.setvm.set和vm.delete两个API来解决,而Vue3是采用Proxy来解决这个问题

image.png

8.Vue中数组的响应式原理

很多人不清楚Array和Object的响应式原理有什么不同。我们侦测Object的时候是通过getter和setter来实现的,但是我们修改数组需要用到push、shift等方法,并不会触发getter和setter

8.1 如何追踪数组的变化

Object是通过setter来追踪变化,数组通过push来改变数组,不同的是数组的push方法是在原型对象上的,而在ES6之前,JavaScript并没有提供元编程的能力,也就是没有提供可以拦截原型方法的能力,但是我们可以用自定义的方法去覆盖原生的原型方法

我们可以实现一个拦截器覆盖Array.prototype。当数组使用push方法时,push方法来源于我们的拦截器,并不是来源于原生的Array.prototype。然后我们的拦截器再去调用原生的Array.prototype上的push方法实现数组的添加数据。

8.2 拦截器的实现原理

拦截器可以理解成为push、pop、shift、unshift、splice、sort、reverse这些改变数组的方法的封装。当数组触发push操作的时候,会先执行拦截器里的方法实现数据的侦测,然后拦截器再去原生的Array.prototype对象上执行push方法

8.3 使用拦截器覆盖Array原型

有了拦截器之后,我们希望它可以覆盖Array.prototype。但是不能直接替换,因为我们只需要拦截响应式的数组。而将一个数组变为响应式需要通过上文提到的Observer类,因此我们要在Observer中使用拦截器来覆盖响应式Array类型数据的原型

8.4 如何收集依赖

拦截器实现了当数组的内容发生变化时,我们收到通知的能力,通过前文的学习,我们收到数组变化的通知后,就要去执行Dep类中的依赖了。那么数组的依赖是怎么收集的呢?

数组也是定义在data中的,它也有getter和setter,当我们读取this.list的时候,我们就会触发list的getter,然后添加到依赖中。与Object不同的是,数组的改变不会触发setter,因为并没有执行setter,而是调用了push这些函数,然后去到拦截器中触发依赖

也就是说,Array在getter中收集依赖,在拦截器中触发依赖

8.5 依赖列表存在哪里

我们知道了要在拦截器中触发依赖,但是依赖在哪里呢?Vue.js将Array的依赖存放在Observer中。为什么存在Observer中而不是和Object一样存在Watcher类中呢?

将Dep实例保存在Observer中,getter可以访问到Observer实例,Array拦截器也可以访问额到Observer实例。因为要保证getter可以访问到依赖,拦截器也要访问到依赖,由于拦截器实质上是一个原型对象,所以不能像Object那样将Dep独立出来,而是要保存在Observer实例上,让组件都能访问到。

8.6 向数组的依赖发送通知

当侦测到数组发生变化时,会向依赖发送通知,通知Watcher数据发生了改变。然后Watcher再去通知各个组件

8.7 先总结一下Array和Object的响应式原理不同

Object和Array都是Observer类的实例,这个类为他们创建了getter和setter,当Object和Array触发getter的时候,将依赖收集进Dep实例中。Object的Dep实例是独立的,但是Array的Dep实例是存在Observer实例中当数据发生变化时,Object会通过setter来触发Dep中的依赖,而数组触发了push等方法时,会去拦截器中触发依赖。触发了依赖后,都会通过Watcher来通知各个组件。

8.8 关于Array的问题

通过前面的介绍,我们知道对Array的变化时通过拦截原型的方式实现的,也正因为这种实现方式,其实有些数组操作Vue.js拦截不到

this.list[1] = 2
this.list.length = 0

修改数组中的元素和清空数组操作时,Vue.js是不会触发响应式的。Vue2的实现方式决定了无法对上面的两个例子做拦截。只有ES6提供了元编程的能力,使用了proxy才解决了这个问题

9. axios的封装

axios是基于Promise的ajax库。我们在使用ajax的时候都要进行封装,原因如下

  1. 如果我们用原生的axios,一旦axios发生了修改,那么我们就要在每个使用了axios的地方都修改,但是如果封装了一层,那么就只要在封装的地方修改就行。也就是便于维护
  2. 可以设置请求拦截器和响应拦截器。可以对请求统一加上请求头(比如cookie),也可以对响应的结果进行转化

9.1 创建axios实例

// 引入axios
import axios from 'axios'

//根据不同的环境选择不同的baseURL
let baseURL; 
if(process.env.NODE_ENV === 'development') {
    baseURL = 'xxx本地环境xxx';
} else if(process.env.NODE_ENV === 'production') {
    baseURL = 'xxx生产环境xxx';
}


// 创建实例
let instance = axios.create({
    baseURL: baseURL,
    timeout: 15000  // 毫秒,表示超时时间
})

9.2 设置请求拦截器和响应拦截器

请求拦截器

// use(两个参数)
instance.interceptors.request.use(req => {
    // 在发送请求前要做的事儿
    ...
    return req
}, err => {
    // 在请求错误时要做的事儿
    ...
    // 该返回的数据则是axios.catch(err)中接收的数据
    return Promise.reject(err)
})

响应拦截器

// use(两个参数)
instance.interceptors.reponse.use(res => {
    // 请求成功对响应数据做处理
    ...
    // 该返回的数据则是axios.then(res)中接收的数据
    return res
}, err => {
    // 在请求错误时要做的事儿
    ...
    // 该返回的数据则是axios.catch(err)中接收的数据
    return Promise.reject(err)
})

10. postcss-pxtorem

postcss-pxtorem是postcss的一个插件,它适用于移动端的适配,将CSS中的px转化为rem。

我们只需要在postcss.config.js中进行配置即可

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 16,
      unitPrecision: 5,
      propList: ['*'],
      selectorBlackList: [],
      replace: true,
      mediaQuery: false,
      minPixelValue: 0,
    },
  },
};
  • rootValue:根元素字体大小,这是根据设计图来的,设置为16表明16px = 1rem
  • unitPrecision:rem的小数位数
  • propList:需要转化的属性列表,['*']表示所有属性都会被转化
  • selectorBlackList:需要忽略的选择器,可以是正则表达式或字符串
  • replace:是否替换原始值
  • mediaQuery:是否在媒体查询中转换px
  • minPixelValue:小于或等于该值的像素单位不被转换

11. 详细描述font-family属性的用法+详细描述line-height属性的用法

font-family属性指定一个元素的字体。font-family可以通过设置一个有先后顺序、由字体名组成的列表来设置字体,通过逗号隔开。如果浏览器不支持第一个字体,那么就会尝试下一个。

line-height设置行间的距离。具体来说是两行文字间基线的距离。line-height用来控制行与行之间的垂直距离,不允许使用负值。有4个参数可以选择:normal,number(设置数字,此数字会与当前的字体尺寸相乘来设置行间距),length(设置固定的行间距),inherit(从父元素继承line-height属性的值)

12. 什么是内存泄漏?什么情况下会内存泄漏

程序运行需要内存,只要程序提出要求,那么系统或者运行时就必须供给内存。对于持续运行的服务程序,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。不再用到的内存,没有及时释放,就叫做内存泄漏。

闭包、意外的全局变量、计时器、死循环、反复重写一个属性、没有清理的DOM元素引用会造成大量的内存泄漏

13. 请解释一下inline和inline-block的区别

inline 将元素显示为内联元素,一系列inline元素都在一行显示,直到该行排满。inline元素设置width、height属性无效。可以设置margin,但是不能设置padding。

inline-block 行内块元素,让inline-block元素显示在同一行不换行,但是可以设置宽度和高度

14. 请写出至少5种让客户端不缓存页面图片的方式

  1. 使用随机的URL参数。在图片的URL中添加随机化的参数,确保每一次URL请求都是唯一的
<img src="example.jpg?random=<%= DateTime.Now.Ticks %>" alt="Randomized Image">
  1. 设置HTTP响应头。在服务器端设置Cache-Control和Pragma头来实现
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetNoStore();
  1. 修改文件名或路径。修改图片文件的名称或者存储路径,以确保每次请求的资源路径都是不同的
  2. 使用meta标签。在模版HTML的头部添加meta标签,通过设置no-cache等属性来指示浏览器不要缓存页面内容
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">

5.添加版本号。在资源的URL中添加版本号,当资源发生变化时,更新版本号,这样可以迫使客户端获取新版本的资源

<img src="example.jpg?v=2" alt="Versioned Image">

15. vuex和localStorage的区别

  1. vuex是存储在内存中,而localStorage存储在本地。且只能存储字符串类型的数据,需要通过JSON的strigify和parse进行处理(读取内存更快)
  2. vuex用于状态管理,可以在应用到的所有组件之间共享和传递状态。而localStorage将数据存储在浏览器中,用于跨页面传递数据时使用,可以让开发者在不同页面之间共享少量数据
  3. 当刷新页面时,vuex由于数据存储在运行时内存中,一旦刷新vuex存储的数据就会丢失,这使得vuex更合适管理应用的瞬时状态。localStorage存储的数据在页面刷新后依然保留,适合存储用户设置和持久化数据的选择。
  4. vuex实现了数据的响应式,而localStorage没有

16. Vue-Router路由的hash模式和history模式

Hash模式

  1. Hash模式的URL带有一个#,比如www.abc.com/#/vue,其中#vu…
  2. Hash值出现在URL中,但是不会出现在HTTP请求中,对后端没有影响
  3. 改变Hash值不会重新加载页面,因为浏览器可以通过onhashchange()事件监听Hash值的变化,从而实现前端路由切换
  4. Hash模式的原理是onhashchange()事件,浏览器无须向后端发起请求,而是监听Hash值的变化,并按规则加载相应的代码。同时,Hash值的变化也会被浏览器记录,实现页面的前进和后退

History模式

  1. History模式的URL没有#符号,采用传统的路由分发模式,即用户输入URL时,服务器接受请求并解析URL,然后进行相应的逻辑处理
  2. 由于要向后端发请求,因此需要后台配置支持,否则访问时可能返回404错误
  3. History采用History API,不能通过浏览器来实现页面的前进和后退,而是通过forward()、back()、go()等方法来切换历史状态。

使用history模式,网页打开了之后刷新出现404的原因是什么。

比如官网为www.aaa.com, 然后我们通过点击打开了www.aaa.com/editor/0123…, 然后在这个页面进行刷新会出现404,是因为浏览器直接向服务器寻找www.aaa.com/editor/0123… 的文件,但是由于后端只配置了请求体为www.aaa.com 时返回index.html,导致找不到子路径,因此返回404

History模式的优势

  1. pushState()设置新的URL可以与当前URL同源的任意URL,而Hash只能修改#后面的部分。可以理解为History模式在某一个站点客流量过大时可以去cdn的另一处站点访问
  2. 可以通过stateObject参数添加任意类型的数据到记录中,而Hash只能添加到短字符串中
  3. 优雅

17.Map和WeakMap的区别

Map

Map出现的原因是因为对象作为映射的缺点是不能使用非字符串值作为键

var m = {}

var x = {id:1}
var y = {id:2}

m[x] = 'foo'
m[y] = 'bar'

m[x] //'bar'
m[y] //'bar'

这里发生了什么?x和y两个对象都字符串化都是"[object Object]",因此m中只设置了一个键。

  1. m.set(x,'foo')可以添加元素。
  2. m.delete(x)删掉一个元素。
  3. m.clear()可以清除整个map的内容
  4. m.size属性可以获得map的长度
  5. [...m.values()]可以获得map中的值
  6. [...m.keys()] 可以获得map中的键
  7. m.has(x) 可以确定map中是否含有给定的键

对于map来说,尽管可以用任意类型的值作为键,但一般使用对象为键,否则方便程度不如普通对象。

对于map有一个问题,当使用对象作为键,后来这个对象被丢弃(所有引用解除),试图让垃圾回收(GC)回收其内存。但是由于map本身会保存期项目,因此还需要在map中移除这个项目来支持GC

WeakMap

  1. WeakMap是map的变体,它只允许对象作为键。并且这个对象是弱持有的,也就是说如果对象本身被垃圾回收了,在WeakMap中这个项目也会移除,但是一旦移除了,我们就观测不到了
  2. WeakMap也有set、has方法,但是没有size属性和clear方法
var m = new WeakMap()
m.set(x:y)

x = {id:1}
y = {id:2}
z = {id:3}
w = {id:4}

x = null //{id:1}可以被GC
y = null //{id:2}可以被GC

m.set(z:w)
w = null //{id:4}不可以被GC

x=null之后,WeakMap只是弱持有了它的键,因此{id:1}可以被GC。当键可以被GC了,那么值当然也可以,因此当y=null后,{id:2}也可以被GC。

当直接w=null的时候,由于WeakMap不是弱持有了值,因此WeakMap还是引用了{id:4}这个对象,因此这个对象不能被GC。

18. Set和WeakSet

  1. Set是一个值的集合,类似于数组,其中的值唯一
  2. s.add(a)添加一个元素
  3. Set没有get方法,因为Set不会从集合中取一个值,而是通过has()来测试这个值是否存在
  4. WeakSet弱持有他的值