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


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

    Set / Map

    SetMap 是两种存储结构。

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

    Set

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

    image.png

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

    image.png

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

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

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

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

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

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

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

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

    • 展开运算符

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

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

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

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

    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);
    
    • 并集

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

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

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

    • 差集

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

      // 差集
      function diff() {
          return [...s1].filter(function (val) {
              return !s2.has(val)
          })
      }
      console.log(diff()); // [1]
      

    Map

    Map 也属于 Object 类型

    image.png

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

    先对 Map 进行初始化

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

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

    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,后面的值会把前面的值替换掉

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

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

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

    答案显然是可以的。

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

    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

    image.png

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

    image.png

    针对于上述问题,我们可以提出来另外一个存储结构类型 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 中双向绑定的核心实现原理。

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

    enumerable

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

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

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

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

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

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

    let obj = {}
    Object.defineProperty(obj, 'name', {
      value: 'hello',
      enumerable: true
    })
    console.log(obj) // {name: 'hello'}
    

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

    configurable

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

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

    let obj = {}
    Object.defineProperty(obj, 'name', {
        value: 'hello',
        enumerable: true
    })
    delete obj.name
    console.log(obj) // {name: 'hello'}
    

    答案是不可以。

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

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

    let obj = {}
    Object.defineProperty(obj, 'name', {
      value: 'hello',
      configurable: true,
      enumerable: true
    })
    console.log(obj) // {}
    

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

    writable

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

    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

    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数据劫持

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

    let data = {
      name: 'moxiaoshang',
      age: 26,
      address: {
        location: '昌平'
      }
    }
    

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

    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. 模拟更新方法

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

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

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

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

      function observer(obj){
        // ...
      }
      oberver(data);
      

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

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

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

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

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

      for (const key in obj) {
        Object.defineProperty(obj, key, {
            get(){ // ... }
        })
      }
      

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

      这样我们的代码就变成了

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

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

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

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

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

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

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

      image.png

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

      image.png

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

    4. 处理 Object 内部属性

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

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

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

      image.png

    5. 直接赋值 Object

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

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

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

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

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

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

      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 对象,看一下实例中包含哪些属性。

    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 上包含的属性。

    image.png

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

    再看一下 getter

    image.png

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

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

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

    Vue中的数组

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

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

    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. 模拟更新方法

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

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

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

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

      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 进行一下优化。

      优化后的代码如下:

      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. 解决 自定义函数 错误触发次数的问题

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

      image.png

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

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

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

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

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

    箭头函数

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

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

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

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

    x => {
        if (x > 0) {
            return x * x;
        }
        else {
            return - x * x;
        }
    }
    

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

    // 两个参数:
    (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;
    }
    

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

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

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

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

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

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

    this指向

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

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

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

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

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

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

  • 相关阅读:
    JavaScript将数字转换为大写金额
    css浮动
    JS合并数组的几种方法及优劣比较
    jquery.zclip.js粘贴功能
    iframe获取元素
    某些框架,类库
    web前端基础知识!
    前端开发流程
    学习其他前端技术
    SVN的学习以及使用!
  • 原文地址:https://www.cnblogs.com/moxiaoshang/p/14844824.html
Copyright © 2020-2023  润新知