日常的学习笔记,包括 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 就已经存在了,我们就直接将其返回。

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

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

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

posted on 2021-05-28 15:41  莫小尚  阅读(73)  评论(0编辑  收藏  举报