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


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

    let、const和var的区别

    letconstvar都是用来定义变量的,那它们有什么区别呢?

    var的特点

    1. 全局变量造成污染

      var a = 1;
      console.log(window.a); //1
      
    2. 存在变量提升机制

      console.log(name); // undefined
      var name = "a";
      
    3. var可以被重复声明

      var a = 1;
      var a = 2;
      var a = 3;
      
    4. var的作用域只有全局作用域函数作用域

      {
        var a = 1;
      }
      console.log(a); // 1
      

    let的特点

    1. 不可以被重复声明

      let a = 1;
      let a = 2;
      let a = 3;
      // Identifier 'a' has already been declared
      
    2. 存在块级作用域

      for(let i = 0; i <10; i++){
        setTimeout(function(){
          console.log(i) // 0 1 2 ... 8 9
        })
      }
      

      如果使用var进行定义,则会全部输出10

      for(var i = 0; i <10; i++){
        setTimeout(function(){
          console.log(i) // 10
        })
      }
      
    3. 暂时性死区

      let a = 1;
      {
        console.log(a); // Cannot access 'a' before initialization
        let a = 2;
      }
      

      因为ES6在定义变量的时候,会把同名的变量定义为两个变量(如 下图 所示)

      image.png

    const的特点

    const不可变的量,也就是常量

    1. const定义的变量不可以对其值进行修改。

      const PI = 3.14;
      PI = 3.15; // Assignment to constant variable.
      
    2. const可以修改同一地址(堆内存)中的值。

      const a = { b: 1 };
      a.b = 2;
      console.log(a); // {b: 2}
      

    解构赋值

    在解构中,有下面两部分参与:

    解构的源:解构赋值表达式的右边部分。

    解构的目标:解构赋值表达式的左边部分。

    数组解构(Array)

    1. 基本使用

      let [a, b, c] = [1, 2, 3];
      // a = 1
      // b = 2
      // c = 3
      
    2. 嵌套使用

      let [a, [[b], c]] = [1, [[2], 3]];
      // a = 1
      // b = 2
      // c = 3
      
    3. 可以忽略未定义变量

      let [a, , b] = [1, 2, 3];
      // a = 1
      // b = 3
      
    4. 非完全解构

      let [a = 1, b] = []; // a = 1, b = undefined
      
    5. 字符串解构等

      在数组的解构中,解构的目标若为可遍历对象,皆可进行解构赋值。

      let [a, b, c, d, e] = 'hello';
      // a = 'h'
      // b = 'e'
      // c = 'l'
      // d = 'l'
      // e = 'o'
      
    6. 解构默认值

      let [a = 2] = [undefined]; // a = 2
      

      当解构模式有匹配结果,且匹配结果是 undefined 时,会触发默认值作为返回结果。

      let [a = 3, b = a] = [];     // a = 3, b = 3
      let [a = 3, b = a] = [1];    // a = 1, b = 1
      let [a = 3, b = a] = [1, 2]; // a = 1, b = 2
      
      • a 与 b 匹配结果为 undefined ,触发默认值:a = 3; b = a =3
      • a 正常解构赋值,匹配结果为 a = 1,b 匹配结果 undefined ,触发默认值:b = a =1
      • a 与 b 正常解构赋值,匹配结果为 a = 1,b = 2
    7. 扩展运算符

      let [a, ...b] = [1, 2, 3];
      // a = 1
      // b = [2, 3]
      

      扩展运算符,又叫 展开运算符剩余运算符。可以利用扩展运算符,对数组进行合并。(如 下图 所示)

      image.png

    对象解构(Object)

    1. 基本使用

      let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
      // foo = 'aaa'
      // bar = 'bbb'
       
      let { baz : foo } = { baz : 'ddd' };
      // foo = 'ddd'
      
    2. 可嵌套/可忽略

      let obj = {p: ['hello', {y: 'world'}] };
      let {p: [x, { y }] } = obj;
      // x = 'hello'
      // y = 'world'
      let obj = {p: ['hello', {y: 'world'}] };
      let {p: [x, {  }] } = obj;
      // x = 'hello'
      
    3. 非完全解构

      let obj = {p: [{y: 'world'}] };
      let {p: [{ y }, x ] } = obj;
      // x = undefined
      // y = 'world'
      
    4. 解构默认值

      let {a = 10, b = 5} = {a: 3};
      // a = 3; b = 5;
      let {a: aa = 10, b: bb = 5} = {a: 3};
      // aa = 3; bb = 5;
      
    5. 扩展运算符

      let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40};
      // a = 10
      // b = 20
      // rest = {c: 30, d: 40}
      

      在ES6中,我们可以通过 扩展运算符 实现很多应用,例如 深拷贝和浅拷贝

    参考文献 ES6 解构赋值 | 菜鸟教程

    深拷贝和浅拷贝

    深拷贝:拷贝后与原数组无关,会使 拷贝后的数组 在堆中指向一个新的内存空间。
    浅拷贝:拷贝后与原数组有关,新数组原数组 指向同一个堆内存。

    image.png

    浅拷贝

    Object.assign()

    Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

    let obj = {a: {name: "mxs", age: 26}};
    let obj2 = Object.assign({}, obj);
    obj2.a.name = "zd";
    console.log(obj.a.name); // zd
    

    Array.prototype.concat()

    let arr = [1, 2, {
      name: 'mxs'
    }];
    let arr2 = arr.concat();    
    arr2[2].name = 'zd';
    console.log(arr); // [1, 2, {name:'zd'}]
    

    Array.prototype.slice()

    let arr = [1, 2, {
      name: 'mxs'
    }];
    let arr2 = arr.slice();
    arr2[2].name = 'zd'
    console.log(arr); // [1, 2, {name:'zd'}]
    

    扩展运算符

    扩展运算符只能拷贝一层 对象 / 数组

    let obj1 = {name:'zd'};
    let obj2 = {age:{count:26}};
    let allObj = {...school,...my};
    obj2.age.count = 100;
    console.log(allObj); // {{name: "zd", age: {count: 100}}}
    console.log(obj2); // {age: {count: 100}}
    

    可以发现两个对象都改变了,这就是只实现了 浅拷贝

    如果想要实现 深拷贝,会十分的麻烦。

    let obj1 = {name:'zd'};
    let obj2 = {age:{count:26},name:'mxs'};
    // 把原来的my放到新的对象中,用一个新的对象age将原始的age也拷贝一份
    let newObj2 = {...obj2,age:{...obj2.age}}
    let allObj = {...obj1,...newObj2};
    obj2.age.count = 100;
    console.log(allObj); // {{name: "mxs", age: {count: 26}}}
    console.log(obj2); // {{name: "mxs", age: {count: 100}}}
    

    深拷贝

    JSON.parse(JSON.stringify())

    JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象。

    let obj1 = {name:'zd'};
    let obj2 = {age:{count:26}};
    let allObj = JSON.parse(JSON.stringify({...obj1,...obj2}));
    obj2.age.count = 100;
    console.log(allObj); // {name: 'zd', age: { count: 26 }}
    

    但是需要注意的是,(JSON.stringify([value])) 这种方法虽然可以实现深拷贝,但是却不能拷贝 FunctionundefinedSymbol

    let obj = {name:'zd', age:{}, count:26, a:function(){}, b:null, c:undefined, d:Symbol('zd')}
    let allObj = JSON.parse(JSON.stringify(obj));
    console.log(allObj); // {name: 'zd', age: {}, count: 26, b: null}
    

    我们可以看到,最终被拷贝下来的,只有 StringObjectNumberNull 这几种数据类型。

    lodash库

    我们可以通过 loadash库中的 cloneDeep 方法来实现深克隆。

    const _ = require('lodash');
    let obj = {
       a: 1,
       b: { a: { b: 1 } },
       c: [1, 2, 3]
    };
    var cloneObj = _.cloneDeep(obj1);
    console.log(obj.a.b === cloneObj.a.b); // false
    

    手写实现深拷贝

    我们先来看一下完整的代码

    function deepClone(obj,hash = new WeakMap()) {
      if (obj == null) return obj;
      if (typeof obj !== 'object') return obj;
      if (obj instanceof Date) return new Date(obj);
      if (obj instanceof RegExp) return new RegExp(obj);
      if (hash.has(obj)) return hash.get(obj);
      let cloneObj = new obj.constructor;
      hash.set(obj, cloneObj);
      for (const key in obj) {
        if (Object.hasOwnProperty.call(obj, key)) {
          cloneObj[key] = deepClone(obj[key], hash);
        }
      }
      return cloneObj;
    }
    
    let obj = {name:'zd', age:{}, count:26, a:function(){}, b:null, c:undefined, d:Symbol('zd')}
    let allObj = deepClone(obj);
    console.log(allObj); // {name: 'zd', age: {}, count: 26, a: [Function: a], b: null, c: undefined, d: Symbol(zd)}
    

    如果我们想要手写一套深克隆的函数方法,我们需要先搞懂其实现思路。

    简单来说,其实现思路就是 类型判断克隆数据类型遍历循环,最后进行 结果输出

    1. 我们先思考,为什么要进行 类型判断

      在此之前,我们需要先要清楚 数据类型判断方式

      • typeof
      • instanceof / constructor
      • Object.prototype.toString.call([value])

      然后我们再来看代码

      // 如果obj是null或者undefined,则直接将结果返回
      if (obj == null) return obj;
      // 如果obj是基础数据类型或者函数,则直接将结果返回(也就是说,函数不需要进行任何处理)
      if (typeof obj !== 'object') return obj;
      // 如果obj不是对象或数组,则直接将结果返回
      if (obj instanceof Date) return new Date(obj);
      if (obj instanceof RegExp) return new RegExp(obj);
      

      通过上面的代码,我们可以发现,剩下的只有两种数据类型 数组对象

      那么我们就清楚了,进行 类型判断 ,其目的就是为了将需要进行深克隆数据类型筛选出来。

    2. 然后再思考,如何 克隆 传入对象的 数据类型 呢?

      最常用的方案如下

      let cloneObj = Object.prototype.toString.call(obj) === ['Object Array'] ? [] : {};
      

      但是这种写法太麻烦了,我们有更简单的实现方案。

      // obj不是数组就是对象,将其进行克隆
      let cloneObj = new obj.constructor;
      

      根据 原型链 的指向原则,我们可以利用上述方案来创建一个新的数据类型对象。(如 下图 所示)

      image.png

      克隆数据类型 的目的,其实就是为了进行下一步的 遍历循环

    3. 接着,我们要进行 遍历循环

      for (const key in obj) {
        if (Object.hasOwnProperty.call(obj, key)) {
          // 进行递归,实现深克隆
          cloneObj[key] = deepClone(obj[key]);
        }
      }
      

      利用 forin 进行循环,在对象复制前,我们都会将值进行递归,再次执行当前方法,判断是否有深层属性。直到递归至没有深层属性为止。

      然后将结果赋值给cloneObj,最后把结果进行输出。

      return cloneObj
      

      但是这种写法还存在一个问题,就是无法进行 循环引用

      如果要进行循环引用,就会发生 栈内存溢出 的情况。

      let obj = {a:{name:'mxs'}}
      obj.b = obj;
      let allObj = deepClone(obj);
      obj.a.name = 'zd';
      console.log(obj); // Maximum call stack size exceeded
      

      为了处理这种问题的发生,我们还需要在进行一步操作。

    4. 最后,我们需要对 异常情况 进行处理

      hash = new WeakMap()
      

      设定一个 WeakMap 数据类型(关于 WeakMap ,可以 参考文献 WeakMap-JavaScript | MDN ,或查看我的另一篇博客 ES6 | weakMap

      if(hash.has(obj)) return hash.get(obj);
      hash.set(obj, cloneObj);
      

      如果是 Object,我们就将其放到 weakMap 中。如果在拷贝之前,这个 Object 就已经存在了,我们就直接将其返回。

      至此,我们的 深拷贝 就完成了。

    我们可以通过这种思路,写出很多种 深克隆 的方案。

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

  • 相关阅读:
    用友U8 | 【基础设置】添加财务项目分类
    用友U8 | 【出纳管理】通过收支操作出纳收款生单,生成收款单表头信息带不过去
    MySQL经典45题(一)
    用友U8 | 【出纳管理】出纳模块银行日记账提示"已经加过类型为MD的锁"
    用友U8 | 【凭证打印】如何将凭证输出为PDF电子格式
    用友U8 | 【请购单列表】后台数据库导请购单列表
    用友U8 | 【数据权限档案分配】导出客户档案分配表
    用友U8 | 【出纳管理】收支操作,客户收款,出纳收款查询不了相关数据
    鼠标事件中鼠标的坐标是如何定义的
    Visual Studio 中各种文件后缀名是什么意思
  • 原文地址:https://www.cnblogs.com/moxiaoshang/p/14753644.html
Copyright © 2020-2023  润新知