• js语法中一些容易被忽略,但会造成严重后果的细节


    一、复杂数据类型-“对象”的地址引用方式,不理解清楚,会出大乱子

    复习一下基础概念(老司机略过):

          JS的数据可以分为简单类型(数字、字符串、布尔值、null和undefined)和 复杂数据类型(对象),主要的不同是:简单数据类型是栈内存直接引用,复杂数据类型是地址引用的,放在堆内存中,所以也叫引用类型(堆栈概念庞大,这里不讲,百度资料很多)。下边讲到的诸多问题的导火索,就是这个地址引用。

         这里不谈“js中一切皆对象”这个言论,因为数字、字符串、布尔值等貌似‘对象’,因为他们有用方法,但请看定义,【js中‘对象’是可变的键控集合】,很显然,数字、字符串等不满足,因为他们不可变,所以说,js中俗定意义来讲,能称满足对象属性的大众意义数据类型是:对象、数组、函数、正则。

         记住这四个家伙,他们都是地址引用,接下来的诸多场景,出现问题,他们都有份。

    1、场景一:函数形参引用复杂类型,请小心赋值,会改变实参的哦 ,下面是错误情况

        var obj = {};//全局建一个对象
        obj.n = 1;
        var arr = [];//全局建一个数组
        arr.n = 1;
        var fun = function(){};//全局建一个函数
        fun.n = 1;
        var reg = /d/;////全局建一个正则
        reg.n = 1;
    
        function bang(p){//形参p 改变形参p的n属性 回头看看会不会影响实参
            p.n = 2;
        }
        bang(obj);
        console.log(obj)//{n:2}  //obj.n 已经改变 1->2 影响了实参 全局变量obj
        bang(arr);
        console.log(arr.n)//2   arr也沦陷 arr.n值变为2
        bang(fun);
        console.log(fun.n)//2   fun.n也变了
        bang(reg);
        console.log(reg.n)//2   reg.n也变了

    PS:同理参数是数组的,在函数内改变形参内的值,也会改变实参

    有一些看似解决的方案,但实际并没有解决,或是遗留隐患,希望你不要中枪,PS:下面主要讨论对象和数组。(因为实际中少有给正则变量增加属性,后边则不讨论正则,function用作形参时大多体现在回调上,基本也不会去给函数赋一个属性,构建函数也是在原型上赋值新属性,所以函数也pass)

    失败方案1:形参使用解构语句,或使用Object.assign重复制对象

    失败原因:解构与Object.assign都是潜复制

    var obj = {
        n:1,
        x:{
            nn:1
        }
    };//全局建一个对象 注意obj.x也是一个对象
    
    function bang({...p}){//形参p 改变形参p的n属性 p.x.nn属性 回头看看会不会影响实参
        p.n = 2;
        p.x.nn = 2;
    }
    bang(obj);
    console.log(obj.n)//1  //解构确实保证了一级属性 没有改变
    console.log(obj.x.nn)//2  //但是二级属性还是受到了污染
    //为什么会这样?
    //因为解构只是浅复制,只是对第一层简单类型的数据实现复制,而对象类型的属性复制的只是地址,引用的还是一个堆内存中的存储位置

    失败方案2:使用Object.assign复制对象

    失败原因:解构与Object.assign都是潜复制

    var obj = {
        n:1,
        x:{
            nn:1
        }
    };//全局建一个对象 注意obj.x也是一个对象
    
    function bang({...p}){//先assign 形参p 改变形参p的n属性 p.x.nn属性 回头看看会不会影响实参
        var pp = Object.assign({},p);
        pp.n = 2;
        pp.x.nn = 2;
    }
    bang(obj);
    console.log(obj.n)//1  //解构确实保证了一级属性 没有改变
    console.log(obj.x.nn)//2  //但是二级属性还是受到了污染
    //为什么会这样?
    //因为Object.assign同样也是浅复制

    2、场景二:数组方法slice只传递引用,不是真正意义的复制 

    //slice只是传递引用,不是真正意义的复制
    var arr = [{a:11},{a:11}];  //arr [{a:11},{a:11}]
    var arr1 = arr.slice(0);   //arr1 [{a:11},{a:11}]
    arr1[0].a = 22  
    //arr1[0]指向的还是arr[0]的引用地址
    //arr1 [{a:22},{a:11}]  arr [{a:22},{a:11}]
    //concat也存在同样的问题 [].concat(arr)  

    还有forEach、map、filter等,回调中的引用会不会也受地址引用的影响,答案是肯定的。如果在回调中修改了形参,会修改原数组,这是我们都不想要的。

    var arr = [{n:{nn:1}},{n:{nn:2}}]
    
    //我们常用map来加工数组,生产新数组,但如果在回调中修改了item,是否会影响到原数组呢?
    var newArr = arr.map(item=>{
        item.n.nn++;
        return item;
    })
    console.log(newArr)//[{n:{nn:2}},{n:{nn:3}}]  我们得到了加工完毕的新数组
    console.log(arr)//[{n:{nn:2}},{n:{nn:3}}]   但原数组arr也被影响了

    会不会有这种想法,想要使用map实现数组复制的,但这种复制没什么用哦

    var arr = [{n:{nn:1}},{n:{nn:2}}]
    
    var newArr = arr.map(v=>v);//已经用map“复制”了个newArr,但它真的“干净”么?
    newArr[0].n.nn = 99;//我修改了newArr,会不会改变arr呢
    
    console.log(newArr)//[{n:{nn:99}},{n:{nn:3}}]  
    console.log(arr)//[{n:{nn:99}},{n:{nn:3}}]   arr改变了,很明显map的复制没什么用
    var arr = [{n:{nn:1}},{n:{nn:2}}]
    
    //你会说这样直接复制太简单了,肯定会影响,我见过下面这样写的
    var newArr = arr.map(v=>{
        var vv = Object.assign({},v);
        return vv;
    });
    newArr[0].n.nn = 99;//我修改了newArr,会不会改变arr呢
    
    console.log(newArr)//[{n:{nn:99}},{n:{nn:3}}]  
    console.log(arr)//[{n:{nn:99}},{n:{nn:3}}]   arr改变了

    总结:[对象通过引用来传递,他们永远不会被复制],函数形参引用和“=”等号赋值传递的都是引用地址,解构赋值和Object.assign都是浅复制,复制的对象属性、数组属性都是地址指针。

    解决办法,jQuery的$.extend()方法已经封装了深拷贝,项目中没有依赖jQuery的可以自己封装一个deepCope方法,网上资源有的是。

    此处有但是转折:地址引用虽然给我们带来了一些麻烦,但是事有两面性,有时候利用这个特性,能帮我们简化一些问题。比如数组的include方法,就可以有如下操作

    var arr1 = [{n:1},{n:2}]
    var arr2 = [{n:3},{n:4}]
    var newArr= arr1.concat(arr2);//arr1、arr2合成了一个新数组 newArr
    
    //在后边的使用中,我又想知道newArr中的元素都出自哪里,就可以用includes查出
    newArr.forEach(v=>{
        console.log(arr1.includes(v)) // true true false false
    })
    
    //因为引用的是地址,includes对比的也是地址,所以对于追踪来说反而有利
    //这个场景在dom中的列表点击上,比较常见

    、原型继承的问题,原型链中慎用引用类型的属性,不要修改实例继承的原型的引用类型属性,因为这将改变原型的值,造成不好的连锁反应。(原型的这些东西比较抽象,老手不用说就明白,新手怎么说也晕乎,还是直接上demo吧)

    //方法 以参数为原型,创建实例
    function creatObject (o){
        var Fun = function(){}
        Fun.prototype = o;
        return new Fun();
    }
    //创建目标原型
    var peo = {
        name:'中国人',
        body:{
            height:180,
            weight:140
        }
    }
    //以peo为原型,创建继承了其属性的实例 stu
    var stu = creatObject(peo);
    var teacher = creatObject(peo);
    console.log(stu.body.weight);//140
    //重点:修改实例stu继承的原型属性 body.weight 会反射到原型peo中的去 又会进而影响其他所有继承peo的实例
    //这是原型链本身经典问题:就是原型中的引用类型属性会被实例共享,也就能在实例中重写原型属性,造成连锁灾难
    //所以尽量在构造函数中,而不是在原型上定义属性;开发中尽量少单独使用原型链,最好在没搞清原型继承关系前,不要轻易使用原型链
    stu.body.weight = 200;
    console.log(peo.body.weight)//200
    console.log(teacher.body.weight)//200

     三、合理使用链式写法和try cache,保持必要的代码容错,提升代码的健壮性。避免常见的卡死级错误,如 Uncaught TypeError: Cannot read property 'XX' of undefined 

      上面这个报错信息大家应该都很熟悉,很常见,但危害很大,能直接卡死进程,导致下面的程序都无法进行,经常在新手代码中出现,还有就是对接api接口,因为数据格式不稳定时产生(api接口数据中没有出现期待的字段)。解决办法有try catch 和 链式写法,个人推荐一般场景用链式就足够了,但像转JSON的时候,就要用try catch了。

    //1、链式写法 设置默认值,增强容错的同时,还very优雅简洁
    var data = {};
    var array = [];
    
    //下面这样就会出问题  因为data.d是undefined  undefined是没有属性的,调用undefined的属性就会报错
    var errData = data.d.d;  //Uncaught TypeError: Cannot read property 'd' of undefined
    
    //怎样避免这样的错误
    //我推荐链式写法,干净利索,如下
    
    //错误写法
    var res = data.a.b;  // error
    var res1 = array[0].a;// error
    //正确写法
    var res = (data.a||{}).b;
    var res1 = (array[0]||{}).a;
    // 2、try catch解决json转换问题  这里不谈try catch的缺陷,万事两面性,只看取舍
    var jsonStr = '{n:11}';
    
    //json格式不对 报错 Uncaught SyntaxError: Unexpected token n in JSON at position 1
    // var jsonData = JSON.parse(jsonStr) //error
    
    //这个情景经常在对接api接口时碰到,后台没有给我们返回期望值,导致程序报错崩溃,怎么办?
    //好的程序应该在这里预先做好容错,保证代码的健壮性
    var jsonData;
    try{
        jsonData = JSON.parse(jsonStr)
    }catch(err){
        jsonData = {};//当出现问题时,我们要给一个容错的默认值
        console.warn("JSON转换失败:",err)
    }
    console.log(jsonData)

     四、Math.max 和 Math.min 求极值

    //Math.max求极值简单方便  Math.min同理
    Math.max.apply( null, [1,11,2])//11
    Math.max.apply( null, [1,11,2,null])//11
    Math.max.apply( null, [1,11,2,''])//11
    
    //但Math.max对参数类型兼容比较低,如下操作都会失败 Math.min同理
    Math.max.apply( null, [1,11,2,undefined]) //NaN
    Math.max.apply( null, [1,11,2,'string']) //NaN
    
    // 我们可以用map加一层容错处理 
    Math.max.apply( null, [1,11,2,'string',0,undefined,null,'',-22].map(v=>Number(v)||0)) //11

     原创博文,求收藏、引用,不要复制粘贴

     

  • 相关阅读:
    10 vue中 v-model ,计算机demo
    linear-gradient
    flexible.js
    九宫格抽奖原理
    js匿名函数与闭包作用
    HTML5实现九宫格布局
    scrollLeft/scrollTop/scrollHeight
    通过media媒体查询设置ie7/8样式、使用media判断各机型、手淘flexible.js
    右击事件oncontentmenu
    js/jquery判断一个对象是否为空
  • 原文地址:https://www.cnblogs.com/liujinyu/p/9559189.html
Copyright © 2020-2023  润新知