学习笔记 — 前端基础之 ES6 (2)

日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue 全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。

Set / Map

SetMap 是两种存储结构。

参考文献 Map 和 Set | 廖雪峰的官网

Set

首先,Set 属于 object 类型(如 下图 所示)

new Set([value]) [value]:Array

Set 是一组 key 集合,但不存储 value。由于 key 不能重复,所以,在 Set 中,没有重复的 key

因此,我们常常利用 Set 来实现 数组去重

1
2
let s = new Set([1,2,3,4,4,3,2,1])
console.log(s); // Set {1, 2, 3, 4}

通过 add(key) 方法可以添加元素到 Set 中,可以重复添加,但不会有效果。

1
2
3
4
s.add(4);
s; // Set {1, 2, 3, 4}
s.add(4);
s; // 仍然是 Set {1, 2, 3, 4}

通过 delete(key) 方法可以删除元素

1
2
s.delete(3);
s; // Set {1, 2, 4}

我们可以用以下方法对 Set {1, 2, 3, 4} 进行数组转换处理。

  • 展开运算符

    1
    2
    let arr = [...s];
    console.log(arr); // [1, 2, 3, 4]
  • Array.form()

    1
    2
    let arr = Array.from(s);
    console.log(arr); // // [1, 2, 3, 4]

同时,我们可以利用 Set 实现各种处理,例如实现集合的 并集交集差集 等。

假如我们现在有以下两个数组。

1
2
3
4
let arr1 = [1, 2, 3, 4, 4, 3, 2, 1];
let arr2 = [2, 3, 4, 5, 5, 4, 3, 2];
let s1 = new Set(arr1);
let s2 = new Set(arr2);
  • 并集

    1
    2
    3
    4
    5
    // 并集
    function union() {
    return [...new Set([...s1, ...s2])]
    }
    console.log(union()); // [1, 2, 3, 4, 5]
  • 交集

    1
    2
    3
    4
    5
    6
    7
    // 交集
    function intersection() {
    return [...s1].filter(function (val) {
    return s2.has(val)
    })
    }
    console.log(intersection()); // [2, 3, 4]

    这里我们用到了 filter 这个高阶函数来进行处理。

  • 差集

    差集很好理解,其实就是交集取反,就是 差集

    1
    2
    3
    4
    5
    6
    7
    // 差集
    function diff() {
    return [...s1].filter(function (val) {
    return !s2.has(val)
    })
    }
    console.log(diff()); // [1]

Map

Map 也属于 Object 类型

Map 是一组键值对的结构,具有极快的查找速度

先对 Map 进行初始化

1
2
let m = new Map([['a', 1], ['b', 2], ['3', 3]]);
m.get('b'); // 2

我们新建一个 Map ,需要一个二维数组,或者直接初始化一个空的 Map

1
2
3
4
5
6
7
let m = new Map(); // 空Map
m.set('a', 1); // 添加新的key-value
m.set('b', 2);
m.has('a'); // 是否存在key 'a': true
m.get('a'); // 1
m.delete('a'); // 删除key 'a'
m.get('a'); // undefined

由于一个 key 只能对应一个 value ,所以,多次对一个 key 放入 value,后面的值会把前面的值替换掉

1
2
3
4
let m = new Map();
m.set('a', 1);
m.set('a', 11);
m.get('a'); // 11

在这里我们可以思考一个问题,Mapkey 是否可以是一个对象呢?

1
2
3
4
let m = new Map();
let obj = {a: 1};
m.set(obj, 2);
console.log(m);// {{a: 1} => 2}

答案显然是可以的。

这里还有一个小问题,假如我们清空上述的对象类型,那么 key 值是否还存在呢?

1
2
3
4
5
6
let m = new Map();
let obj = {a: 1};
m.set(obj, 2);
obj = null;
console.log(m); // {{a: 1} => 2}
console.log(obj); // null

这里我们可以理解为,我们定义的 变量 obj 指向 内存空间 obj ,然后我们定义了一个 Set 类型,其 key 值指向 内存空间 obj

而后我们又将 变量 obj 清空,其原来的 内存空间 obj 并没有被销毁,只是改变了其指向。所以 变量 obj 的指向并不影响 Setkey 的指向,所以才有了上述问题的产生和结果。

针对于上述问题,我们可以提出来另外一个存储结构类型 weakMap,其 key 值是会被清空的。

weakMap

参考文献 WeakMap-JavaScript | MDN

WeakMap 对象是一组 key/value (键值对) 的集合,其中的键是 弱引用 的。其 key 必须是对象,而 value 可以是任意的。

WeakMap 的 key 只能是 Object 类型。 原始数据类型 是不能作为 key 的(比如 Symbol)。

所以我们就可以得出来一个结论了。

Mapkey 值是强引用类型,在堆内存中存在指向关系,所以不会被垃圾回收机制给清除掉。

weakMapkey 值是弱引用类型,会被垃圾回收机制清除掉。

Object.defineProperty

参考文献 Object.defineProperty() | MDN Web

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

同时,Object.defineProperty() 也是 Vue2.0 中双向绑定的核心实现原理。

1
2
3
let obj = {}
Object.defineProperty(obj, 'name', {value: 'hello'})
console.log(obj.name) // hello

enumerable

当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中,默认为 false

在这里我们可以引出来一个问题,假如我们直接打印 obj 变量,会输出变量的属性和值吗?

1
2
3
let obj = {}
Object.defineProperty(obj, 'name', {value: 'hello'})
console.log(obj) // {}

我们可以发现,控制台中并未输出 obj 的任何属性。

原因是通过 Object.defineProperty() 定义的属性,都是不可枚举的(enumerable: false)。

我们可以通过修改 enumerable 来达到枚举的效果。

1
2
3
4
5
6
let obj = {}
Object.defineProperty(obj, 'name', {
value: 'hello',
enumerable: true
})
console.log(obj) // {name: 'hello'}

这样我们就可以打印出我们定义的属性了。

configurable

当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除,默认为 false

同样我们可以先思考一个问题,可以通过描述符 delete 删除我们自定义的属性吗?

1
2
3
4
5
6
7
let obj = {}
Object.defineProperty(obj, 'name', {
value: 'hello',
enumerable: true
})
delete obj.name
console.log(obj) // {name: 'hello'}

答案是不可以。

原因是通过 Object.defineProperty() 定义的属性,都是不可配置的(configurable: false)。

我们可以通过修改 configurable 来达到想要的结果。

1
2
3
4
5
6
7
let obj = {}
Object.defineProperty(obj, 'name', {
value: 'hello',
configurable: true,
enumerable: true
})
console.log(obj) // {}

这样我们定义的属性就被删除了。

writable

当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。

1
2
3
4
5
6
7
8
9
let obj = {}
Object.defineProperty(obj, 'name', {
value: 'hello',
configurable: true,
writable: true,
enumerable: true
})
obj.name = 'world'
console.log(obj) // 'world'

getter/setter

getter :属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。

setter :属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。

(注:如果我们定义了 getter,则不能再定义 writable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {}
let other = '' // 额外设置一个变量,用来设置setter
Object.defineProperty(obj, 'name', {
enumerable: true,
configurable: true,
get(){
console.log('--------');
return other;
},
set(val){
other = val
}
})
obj.name = 'world'
console.log(obj) // -------- 'world'

(注:我们需要额外定义一个变量 other

Vue 的 数据劫持 ,就是利用的 setter/getter

Vue 数据劫持

我们先定义一个需要进行劫持的对象。

1
2
3
4
5
6
7
let data = {
name: 'moxiaoshang',
age: 26,
address: {
location: '昌平'
}
}

随后我们去观察 Vue 的源码,一步一步的分析 数据劫持 的实现原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function updata() {
console.log('更新视图');
}
function observer(obj) {
if (typeof obj !== 'object') return obj;
for (const key in obj) {
defineReactive(obj, key, obj[key])
}
}
function defineReactive(obj, key, value) {
observer(value)
Object.defineProperty(obj, key, {
get() {
return value
},
set(val) {
if (val !== value) {
observer(val)
updata()
value = val
}
}
})
}
observer(data);
  1. 模拟更新方法

    1
    2
    3
    function updata() {
    console.log('更新视图');
    }

    手写一个模拟更新的方法,使我们在调用 get/set 的时候更直观。

  2. 使用 observer 函数观察 data 的变化

    将我们需要监听的对象传入函数中。

    1
    2
    3
    4
    function observer(obj){
    // ...
    }
    oberver(data);

    Object.defineProperty 封装成一个可递归调用的函数。

    (注:Object.defineProperty 只能用在 Object 上,数组不识别)

    所以我们第一步需要进行类型判断,将不是 Object 的数据类型返回。

    1
    if(typeof obj !== 'object') return obj; // 类型判断

    随后,我们需要循环 obj 的每一个属性,并利用 Object.defineProperty 进行 getter 的遍历输出。

    1
    2
    3
    4
    5
    for (const key in obj) {
    Object.defineProperty(obj, key, {
    get(){ // ... }
    })
    }

    但是这样写会有一个问题,那就是整个代码的灵活性不高,所以在 Vue 源码中,我们会用一个新的函数 defineReactive 将内层代码进行封装。

    这样我们的代码就变成了

    1
    2
    3
    for (const key in obj) {
    defineReactive(obj, key, obj[key])
    }
  3. 定义响应式函数 defineReactive

    1
    2
    3
    function defineReactive(obj, key, value) {
    // ...
    }

    继续将 Object.defineProperty 封装成一个函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Object.defineProperty(obj, key, {
    get() {
    return value
    },
    set(val) {
    update() // 在此设置更新视图触发的函数,使其更直观
    value = val // 不需要额外定义全局变量 other
    }
    })

    这里我们用到了 闭包 的思想,形参 value 被调用,所以不会被销毁。

    所以我们在 set 的时候,不需要额外定义一个全局变量,直接使用 value 即可。

    到这一步,我们就可以直接将 set/get 绑定在对象上了。

    通过在控制台中的输出,我们又可以发现一个问题

    内部属性并没有被绑定 get/set ,所以我们需要进行递归处理。

  4. 处理 Object 内部属性

    非常简单,只需要在处理属性前,也就是响应式函数中进行递归处理即可。

    1
    2
    3
    4
    function defineReactive(obj, key, value) {
    observer(value) // 将传入的值进行递归
    // ...
    }

    这样,内部属性就被绑定了 get/set 了。

  5. 直接赋值 Object

    接下来,我们再来处理另外一个特殊情况。

    假如我们在属性中,直接赋值一个新的 Object

    1
    2
    3
    4
    data.address = {
    location:'北京'
    } // 更新视图
    data.address.location = '昌平' // 没有任何输出

    这里我们原本应该会触发两次 update 函数 ,但是最终却只触发了一次。

    因为我们在 address 属性中绑定了一个新的 Object ,而这个对象我们并未进行监听。

    所以我们只需要在 setter 中,添加一个监听函数即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Object.defineProperty(obj, key, {
    get() {
    return value
    },
    set(val) {
    if (val !== value) { // 假如值相同,则不需要进行处理
    observer(val) // 进行属性监听
    update()
    value = val
    }
    }
    })

这种方法我们只能劫持 Object 对象类型,如果我们想要劫持 Array 数组,需要使用 Proxy

Proxy

参考文献 Proxy - JavaScript | MDN

Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如 属性查找赋值枚举函数调用等)。

我们来实例化一个 Proxy 对象,看一下实例中包含哪些属性。

1
2
3
4
5
6
7
let arr = [1, 2, 3];
let proxy = new Proxy(arr, {
get() { console.log(arguments) },
set() { console.log(arguments) }
})
proxy[0] = 100;
console.log(proxy[0])

先来看一下 setter 上包含的属性。

  • 目标源
  • 传入的 key 值
  • 取到的 value 值
  • Proxy

再看一下 getter

  • 目标源
  • 传入的 key 值
  • Proxy

这样,我们可以清楚的看到,settergetter 多了一个 value 值。

在 Vue 中,我们希望数组中的数据一变化,视图就会更新。但是 Object.defindProperty 并不支持数组的更新,所以我们通常会用 Proxy 将数组的方法进行重写。(push(),shift(),unshift(),pop() 等等…)

Vue 中的数组

(注:在 Vue3 中,已经用 Proxy 代替 Object.defindProperty 来做数据劫持)

先来看一下完全写法,随后我们一点一点来分析代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function update() {
console.log('更新视图')
}
let arr = [1, 2, 3];
let proxy = new Proxy(arr, {
set(target, key, value) {
if (key === 'length') return true;
update();
return Reflect.set(target, key, value)
},
get(target, key) {
return Reflect.get(target, key)
}
})
proxy.push(1);
  1. 模拟更新方法

    1
    2
    3
    function updata() {
    console.log('更新视图');
    }

    手写一个模拟更新的方法,使我们在调用 get/set 的时候更直观。

  2. Proxy 中的 getter/setter 的返回值

    我们可以将 Proxy 中的属性进行操作,然后在 getter/setter 中,增加我们自定义的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let proxy = new Proxy(arr, {
    set(target, key, value) {
    update();
    return target[key] = value;
    },
    get(target, key) {
    return target[key]
    }
    })
    proxy.push(1);

    但是这种写法是不推荐的。我们尽量不要去操作原数组,因为数组变化时,可能会调用 push()pop() 等方法,这个时候 key 值可能会出现问题。所以我们需要使用 Reflect 进行一下优化。

    优化后的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let proxy = new Proxy(arr, {
    set(target, key, value) {
    update();
    return Reflect.set(target, key, value)
    },
    get(target, key) {
    return Reflect.get(target, key)
    }
    })
    proxy.push(1);
  3. 解决 自定义函数 错误触发次数的问题

    这个时候我们会发现一个问题,我们自定义的函数被触发了两次,但是我们只使用了一次方法。

    关于这个问题,原因也很简单。我们打印一下 key 值,就可以轻松发现,我们在修改数组时,不仅添加了值,还触发了一次 length

    因为数组的长度发生了改变,所以 length 也被传递到了 Proxysetter 中。

    我们可以通过判断 length 属性,来完成这个问题的修复。

    1
    if (key === 'length') return true;

    update() 前,加上此判断即可。

箭头函数

参考文献 箭头函数 | 廖雪峰的官网

首先,箭头函数简单来说,就是函数的缩写

x => x * x 等同于 function (x) { return x * x }

箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连 { ... }return 都省略掉了。还有一种可以包含多条语句,这时候就不能省略 { ... }return

1
2
3
4
5
6
7
8
x => {
if (x > 0) {
return x * x;
}
else {
return - x * x;
}
}

如果参数不是一个,就需要用括号 () 括起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 两个参数:
(x, y) => x * x + y * y

// 无参数:
() => 3.14

// 可变参数:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}

如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:

1
2
// SyntaxError:
x => { foo: x }

因为和函数体的 { ... } 有语法冲突,所以要改为:

1
2
// ok:
x => ({ foo: x })

这里我们先要明确箭头函数的几个特点

  • 箭头函数内部的 this 是词法作用域,由上下文确定。
  • 箭头函数不存在 arguments 属性

this 指向

  1. 普通函数执行,. 前面是哪个对象,this 就指向哪个对象。如果 . 前面没有调用的对象,那么就指向 window (严格模式下指向 undefined

  2. 构造函数执行,this 是当前类的实例

  3. 箭头函数内部的 this 是词法作用域,由上下文确定

  4. 给元素的某个事件绑定函数,函数触发,this 指向当前元素

  5. call/apply/bind 可以改变 this 的指向。

本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
您也可以关注我的 个人站点博客园掘金,我会在文章产出后同步上传到这些平台上。
最后感谢您的支持!

请打赏并支持一下作者吧~

欢迎关注我的微信公众号