js基础学习(持续学习中,未完成)

80 阅读10分钟

js基础学习

类型转换

  1. 隐式类型转换

​ 凡是通过逻辑运算符(&& 、|| 、!)、运算符(+ ,- , * , /)、关系运算符(>、 <、 <= 、>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到类型同都会进行隐式转换。

'==' 的隐式类型转换规则

  • 如果类型相同,无需进行转换

  • 如果其中一个操作符是null或undefined,则另一个必须是null或undefined才会返回true,否则都返回false

  • 如果其中一个是Symbol类型,则返回false

  • 如果两个操作值为string和number,则转换成number进行对比

  • 如果一个操作值是boolean,则转换为number

  • 如果一个操作值是object且另一个是string、number或Symbol,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString 方法进行转换)。

    null == undefined       // true  规则2
    
    null == 0               // false 规则2
    
    '' == null              // false 规则2
    
    '' == 0                 // true  规则4 字符串转隐式转换成Number之后再对比
    
    '123' == 123            // true  规则4 字符串转隐式转换成Number之后再对比
    
    0 == false              // true  5规则 布尔型隐式转换成Number之后再对比
    
    1 == true               // true  5规则 布尔型隐式转换成Number之后再对比
    
    var a = {
    
      value: 0,
    
      valueOf: function() {
    
        this.value++;
    
        return this.value;
    
      }
    
    };
    
    // 注意这里a又可以等于1、2、3
    
    console.log(a == 1 && a == 2 && a ==3);  //true 规则6 Object隐式转换
    
    // 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下
    

'+'的隐式类型转换

'+' 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。当 '+' 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。

特殊的规则如下所示:

  • 如果一个是string,另一个是undefined、null或boolean,则调用toString()方法进行字符串拼接。如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,然后再进行拼接。
  • 如果其中有一个是数字,另外一个是 undefined、null、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则。
  • 如果一个是字符串一个是数字,按照字符串规则进行拼接
1 + 2        // 3  常规情况

'1' + '2'    // '12' 常规情况

// 下面看一下特殊情况

'1' + undefined   // "1undefined" 规则1,undefined转换字符串

'1' + null        // "1null" 规则1,null转换字符串

'1' + true        // "1true" 规则1,true转换字符串

'1' + 1n          // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串

1 + undefined     // NaN  规则2,undefined转换数字相加NaN

1 + null          // 1    规则2,null转换为0

1 + true          // 2    规则2,true转换为1,二者相加为2

1 + 1n            // 错误  不能把BigInt和Number类型直接混合相加

'1' + 3           // '13' 规则3,字符串拼接

Object 的转换规则

对象转换的规则,会先调用内置的[ToPrimitive]函数,逻辑如下:

  • 如果部署了 Symbol.toPrimitive 方法,优先调用再返回;
  • 调用对象自身的valueOf方法,如果该方法返回原始类型的值(数值、字符串和布尔值),则直接对该值使用Number方法,不再进行后续步骤。
  • 如果valueOf方法返回复合类型的值,再调用对象自身的toString方法,如果toString方法返回原始类型的值,则对该值使用Number方法,不再进行后续步骤。
  • 如果toString方法返回的是复合类型的值,则报错。
var obj = {

  value: 1,

  valueOf() {

    return 2;

  },

  toString() {

    return '3'

  },

  [Symbol.toPrimitive]() {

    return 4

  }

}

console.log(obj + 1); // 输出5

// 因为有Symbol.toPrimitive,就优先执行这个;如果Symbol.toPrimitive这段代码删掉,则执行valueOf打印结果为3;如果valueOf也去掉,则调用toString返回'31'(字符串拼接)

// 再看两个特殊的case:

10 + {}

// "10[object Object]",注意:{}会默认调用valueOf是{},不是基础类型继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来,参考'+'的规则C

[1,2,undefined,4,5] + 10

// "1,2,,4,510",注意[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,也还是调用toString,返回"1,2,,4,5",然后再和10进行运算,还是按照字符串拼接规则,参考'+'的第3条规则

  • 判断类型
function getType(obj) {
    let type = typeof obj;
    if (type !== 'object') {
        return obj
    }
    return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
}

深拷贝和浅拷贝

  1. 浅拷贝理解

    创建一个新对象,来接收你要复制或者你用的对象值。如果对象属性是基本数据类型,复制的就是基本数据类型给对象,如果对象属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了,会影响另一个对象。

    浅拷贝的方法

    • object.assign

      Object.assign是ES6中Object的一个方法,可以用于js对象的合并等多个用途,也可以进行浅拷贝,该方法等第一个参数是拷贝的目标对象,后面的参数是拷贝的目标对象(也可以是多个来源)。

      // 语法
      object.assign(target,...sources)
      

      有几个注意点

      • 它不会拷贝对象的继承属性
      • 它不会拷贝对象的不可枚举的属性
      • 可以拷贝Symbol的属性
      let obj1 = { a: { b: 1 }, sym:Symbol(1) };
      Object.defineproperty(obj1, 'innumerable', {
        value: '不可枚举属性',
        enumerable: false
      });
      let obj2 = {};
      object.assign(obj2, obj1);
      obj1.a.b = 2;
      console.log('obj1',obj1);
      console.log('obj2',obj2);
      

      image-20210417230316979.png 从上面的样例代码中可以看到,利用 object.assign 也可以拷贝 Symbol 类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能。

    • 扩展运算符方式

      // 语法
      let cloneObj = {...obj}
      

      扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。

    • concat 拷贝数组

      只能拷贝数组。

    • slice拷贝数组

      仅针对数组,截取数组返回一个新的数组对象。两个参数分别代表原数组的开始和结束位置。

      // 语法为:
      arr.slice(begin, end);
      

      浅拷贝简单实现

      const shallowClone = target => {
        if(typeof target === 'object' && typeof target !== 'null') {
          const cloneTarget = Array.isArray(target) ? [] : {}
          for(i in target) {
            if(target.hasOwnProperty(i)) {
              cloneTarget[i] = target[i]
            }
          }
          return cloneTarget
        } else {
          return target
        }
      }
      
  2. 深拷贝的原理和实现

    浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的属性无法被拷贝。深拷贝对于复杂类型的拷贝,是在堆内存中开辟了新的一块内存地址,并将原有的对象完全复制过来存放。

    将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。

    深拷贝的方法

    • JSON.stringify

      JSON.stringify是目前开发中最简单的深拷贝方法,其中之一就是把对象序列化成为JSON字符串,并将对象里面的内容转化成字符串,最后再用JSON.parse()的方法将JSON 字符串生成一个新的对象。

      但是使用 JSON.stringify 实现深拷贝还是有一些地方值得注意:

      1. 拷贝的对象中值如果有函数、undefined、symbol这几种类型,经过JSON.stringify之后的字符串中,键值对都会消失。
      2. 拷贝Date引用类型会变成字符串。
      3. 无法拷贝不可枚举的属性。
      4. 无法拷贝对象的原型链。
      5. 拷贝RegExp引用类型会变成空对象。
      6. 对象中含有NaN、infinity、-infinity,JSON序列化的结果会变成null。
      7. 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
      function Obj() { 
      
        this.func = function () { alert(1) }; 
      
        this.obj = {a:1};
      
        this.arr = [1,2,3];
      
        this.und = undefined; 
      
        this.reg = /123/; 
      
        this.date = new Date(0); 
      
        this.NaN = NaN;
      
        this.infinity = Infinity;
      
        this.sym = Symbol(1);
      
      } 
      
      let obj1 = new Obj();
      
      Object.defineProperty(obj1,'innumerable',{ 
      
        enumerable:false,
      
        value:'innumerable'
      
      });
      
      console.log('obj1',obj1);
      
      let str = JSON.stringify(obj1);
      
      let obj2 = JSON.parse(str);
      
      console.log('obj2',obj2);
      
      

      jsonStringify.png

    • 递归实现

      该怎么做:

      1. 针对能够遍历对象的不可枚举属性以及Symbol类型,我们可以使用Reflect.ownKeys方法。
      2. 当参数是Date和RegExp类型,直接生成一个新的实例返回。
      3. 利用Object的getOwnPropertyDescriptors方法可以获得对象的所有属性,以及对应的特性,结合Object的create方法,创建一个新对象,并继承传入原对象的原型链。
      4. 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。
      const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
      
      const deepClone = function (obj, hash = new WeakMap()) {
      
        if (obj instanceof Date) 
      
        return new Date(obj)       // 日期对象直接返回一个新的日期对象
      
        if (obj.constructor === RegExp)
      
        return new RegExp(obj)     //正则对象直接返回一个新的正则对象
      
        //如果循环引用了就用 weakMap 来解决
      
        if (hash.has(obj)) return hash.get(obj)
      
        let allDesc = Object.getOwnPropertyDescriptors(obj)
      
        //遍历传入参数所有键的特性
      
        let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
      
        //继承原型链
      
        hash.set(obj, cloneObj)
      
        for (let key of Reflect.ownKeys(obj)) { 
      
          cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
      
        }
      
        return cloneObj
      
      }
      
      let obj = {
      
        num: 0,
      
        str: '',
      
        boolean: true,
      
        unf: undefined,
      
        nul: null,
      
        obj: { name: '我是一个对象', id: 1 },
      
        arr: [0, 1, 2],
      
        func: function () { console.log('我是一个函数') },
      
        date: new Date(0),
      
        reg: new RegExp('/我是一个正则/ig'),
      
        [Symbol('1')]: 1,
      
      };
      
      Object.defineProperty(obj, 'innumerable', {
      
        enumerable: false, value: '不可枚举属性' }
      
      );
      
      obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
      
      obj.loop = obj    // 设置loop成循环引用的属性
      
      let cloneObj = deepClone(obj)
      
      cloneObj.arr.push(4)
      
      console.log('obj', obj)
      
      console.log('cloneObj', cloneObj)
      
      

对象

  1. new操作符都做了些什么

    • 创建一个新对象
    • 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
    • 执行构造函数中的代码(为这个新的对象添加属性)
    • 返回新对象

    实现一个new

    function myNew(fn) {
      // 创建一个新对象
      let obj = new Object();
      // 取出参数中的第一个参数,获得构造函数
      let consturc = Array.prototype.shift.call(arguments) // 下列示例中的第一个参数Person
      // 连接原型,新对象可以访问原型中的属性
      obj.__protp__ = contruc.prototype
      // 执行构造函数,即绑定 this,并且为这个新对象添加属性
      let result = contruc.apply(obj,arguments)
      return result instanceof Object ? result : obj
    }
    
    
    
    function Person (name,age){
      this.name = name;
      this.age = age;
      this.say = function () {
          console.log("I am " + this.name)
      }
    }
    
    let person1 = new Person("Star",20);
    console.log(person1.name);      
    console.log(person1.age);       
    person1.say();  
    
    let person2 = myNew (Person,"Star",20);
    console.log(person2.name);      
    console.log(person2.age);       
    person2.say(); 
    

对象继承实现

  1. 原型链继承

    原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数(constructor)都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

    function Parent1 = function() {
      this.name = 'parent1';
      this.play = [1,2,3];
    }
    function Child1() {
      this.type = 'child2';
    }
    Child1.prototype = new Parent1()
    console.log(new Child1())
    

    缺陷:多个实例使用的是同一原型对象,他们的内存空间是共享的,当一个发生变化,全部都发生变化。