前端整理 —— javascript 2

1. es5 实现继承

【 常用继承方式使用组合式继承、寄生式组合继承,具体场景具体使用 】

  1. 通过原型链继承:

    • 就是让对象实例通过原型链的方式串联起来,当访问目标对象的某一属性时,能顺着原型链进行查找,从而达到类似继承的效果,让子类的构造函数的原型对象指向父类的实例对象
    • 存在缺陷:
      • 在创建子类实例的时候,不能向父类的构造函数中传递参数
      • 原型中包含的引用值会在所有实例之间共享,修改一个实例,另一个实例会跟着修改
        (不同子类实例,修改的同一个父类实例的值)
    function Father() {
        this.colors = ['red', 'blue', 'green']
    }
    
    function Son() { }
    Son.prototype = new Father()
    
    let instance1 = new Son()
    instance1.colors.push('black')
    console.log(instance1.colors)  // "red,blue,green,black"
    
    let instance2 = new Son()
    console.log(instance2.colors)  // "red,blue,green,black"
    
  2. 借用构造函数继承:

    • 利用 call 调用父构造函数,且让 this 指向子构造函数
    • 存在缺陷:
      • 子类不能访问父类原型上定义的方法,所以必须在父构造函数中定义方法,因此函数不能复用
    function Father(num) {
        this.num = num;
    }
    Father.prototype.test = function() {
        console.log(111);
    }
    
    function Son(num) {
        Father.call(this, num);
        this.n = num;
    }
    
    let instance = new Son(123)
    console.log(instance.n)  // 123
    console.log(instance.num)  // 123
    console.log(instance.test()) // instance.test is not a function
    
  3. 组合继承:

    • 通过原型链继承来继承方法,借用构造函数继承来继承属性,这样函数可以复用,且子实例进行修改时,不会相互影响
    • 存在缺陷:
      • 组合式继承存在效率问题,父类构造函数会被调用两次,第一次在创建子类原型,第二次在子类构造函数中调用
      • 最终导致子类原型包含父类所有的实例属性,浪费空间(因为属性在子实例上,方法在原型链上,所以子构造函数的原型对象不需要包含父类属性)
    function Father(name) {
       this.name = name;
        this.colors = ["red", "blue", "green"];
    }
    Father.prototype.sayName = function () {
        console.log(this.name);
    };
    
    function Son(name, age) {
        Father.call(this, name); // 第二次调用 Father()
        this.age = age;
    }
    Son.prototype = new Father(); // 第一次调用 Father()
    subType.prototype.constructor = subType;
    Son.prototype.sayAge = function () {
        console.log(this.age);
    };
    
    let instance1 = new Son("Nicholas", 29);
    instance1.colors.push("black");
    console.log(instance1.colors); // "red,blue,green,black"
    instance1.sayName(); // "Nicholas";
    instance1.sayAge(); // 29
    
    let instance2 = new Son("Greg", 27);
    console.log(instance2.colors); // "red,blue,green"
    instance2.sayName(); // "Greg";
    instance2.sayAge(); // 27
    
  4. 原型式继承:

    • 在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制
      【 新对象将 person 作为原型,所以它的原型中就包含一个基本类型值属性 name 和一个引用类型值属性 friends。这意味着 person.friends不仅仅属于 person 所有,而且也会被 anotherPerson 以及 yetAnotherPerson 共享。实际上,这就相当于又创建了 person 对象的两个副本 】
    • 存在缺陷:
      • 属性中包含的引用数值,会在对象之间共享,与原型链继承中引用数值会共享类似
    function object(o) {
       function F() { }
        F.prototype = o
        return new F()
    }
    
    let person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
    };
    
    let anotherPerson = object(person);
    anotherPerson.name = "Greg";
    anotherPerson.friends.push("Rob");
    
    let yetAnotherPerson = object(person);
    yetAnotherPerson.name = "Linda";
    yetAnotherPerson.friends.push("Barbie");
    
    console.log(person.name); // "Nicholas"
    console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
    
  5. 寄生式继承:

    • 将 object 封装,在函数中以某种方式增强这个继承后的对象
    • 存在缺陷:给对象添加函数会导致函数难以重用,与构造函数模式类似
    function object(o) {
       	function F() { }
        F.prototype = o
        return new F()
    }
    
    function createAnother(original, sex) {
        let clone = object(original); // 通过调用函数创建一个新对象
        clone.sex = sex;
        clone.sayHi = function () { // 以某种方式增强这个对象
            console.log("hi");
        };
        return clone; // 返回这个对象
    }
    
    let person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
    };
    
    let anotherPerson = createAnother(person, "famle");
    console.log(anotherPerson.sex); // "famle"
    anotherPerson.sayHi(); // "hi"
    
  6. 寄生式组合继承:

    • 寄生组合继承的模式是现在业内公认的比较可靠的 JS 继承模式,ES6 的 class 继承在 babel 转义后,底层也是使用的寄生组合继承的方式实现的
    • 解决了上述几个方法的问题:
      • 继承属性,靠的是借用构造函数继承,修改不会影响其他子实例
      • 继承方法,指向一个 “空” 的构造函数的实例,这个空的构造函数的原型指向父构造函数的原型
        【 就是在这里,优化了 “父类构造函数会被调用两次” 这个问题,不通过指向 new 父构造函数,间接让父构造函数的原型在原型链上,而是想直接指向。但为了让子实例可以加私有方法,所以 new 一个空构造函数,让这个空构造函数指向父构造函数的原型,这样就避免了 new 父构造函数时,重复运行父类构造函数 】
    // 实现继承的核心函数
    function inheritPrototype(subType, superType) {
        function F() {};
        //F()的原型指向的是superType
        F.prototype = superType.prototype; 
        //subType的原型指向的是F()
        subType.prototype = new F(); 
        // 重新将构造函数指向自己,修正构造函数
        subType.prototype.constructor = subType; 
    }
    
    // 设置父类
    function SuperType(name) {
        this.name = name;
        this.colors = ["red", "blue", "green"];
        SuperType.prototype.sayName = function () {
            console.log(this.name)
        }
    }
    
    // 设置子类
    function SubType(name, age) {
        //构造函数式继承--子类构造函数中执行父类构造函数
        SuperType.call(this, name); // 只在这里运行了一次父类构造函数
        this.age = age;
    }
    
    // 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
    inheritPrototype(SubType, SuperType)
    
    // 添加子类私有方法
    SubType.prototype.sayAge = function () {
        console.log(this.age);
    }
    
    var instance = new SubType("Taec", 18)
    console.dir(instance)
    

2. es5 实现箭头函数

其实就是拿个变量保存上下文的 this(通常用 _this)

ES6 function foo() { 
    setTimeout(() => { 
        console.log('id:', this.id); 
    }, 100); 
  } 

ES5 function foo() { 
    var _this = this; 
    setTimeout(function () { 
        console.log('id:', _this.id); 
    }, 100); 
}

3. == 和 ===

  • ==

    • 用来进行一般比较检测两个操作数是否相等,可以允许进行类型转换

    • 规则 1:NaN 和其他任何类型比较永远返回 false(包括和他自己)

    • 规则 2:Boolean 和其他任何类型比较,Boolean 首先被转换为 Number 类型

    • 规则 3:String 和 Number比较,先将 String 转换为 Number 类型

    • 规则 4:null == undefined 比较结果是 true,除此之外,null,undefined 和其他任何结果的比较值都为 false

      • 特殊规则,并不是转换为 Number 类型
        Number(null); // 0
        Number(undefined); // NaN
        
    • 规则 5:原始类型和引用类型做比较时,引用类型会依照 ToPrimitive 规则转换为原始类型

      • ToPrimitive 规则,是引用类型向原始类型转变的规则,它遵循先 valueOf 后 toString 的模式期望得到一个原始类型
      • 如果还是没法得到一个原始类型,就会抛出 TypeError
    • [ ] == ! [ ]

      第一步,! [ ] 会变成 false
      第二步,应用 规则2 ,题目变成: [ ] == 0
      第三步,应用 规则5 ,[ ] 的 valueOf 是 0,题目变成: 0 == 0
      所以, 答案是 true

    • [ undefined ] == false

      第一步,应用 规则5 ,[ undefined ] 通过 toString 变成 ‘’,
      题目变成 ‘’ == false
      第二步,应用 规则 2 ,题目变成 ‘’ == 0
      第三步,应用 规则 3 ,题目变成 0 == 0
      所以, 答案是 true
      但是 if( [ undefined ] ) 又是个 true

  • ===

    • 用于严格比较,只要类型不匹配就返回flase
    • null === null 和 undefined === undefined 都是返回 true
      (虽然显而易见,但我在被面试官追问的时候,说了为 false,细节掌握的不自信了)
    • 不过 NaN === NaN 结果为 false
  • 但是对于 Array,Object 等高级类型之间比较,== 和 === 是没有区别的

4. js 数据类型

es5 的时候,是 number,string,boolean,object,null,undefined
es6 的时候新增 symbol
es10 的时候新增 bigint

基本类型:number,string,boolean,null,undefined,symbol,bigint
引用类型:object (但这个时候并不是等着你只说 object,我就翻过车 )

引用类型包括 Object 类型,Array 类型,Function 类型,Map 类型,WeakMap 类型,WeakSet 类型,Set 类型,RegExp 类型等等

5. 深拷贝

(深拷贝需考虑特殊情况,undefined,function,symbol,相互引用)

  • 通过 JSON.parse(JSON.stringify(obj))
    • 但这种方式有局限性,当值为 undefined,symbol,Function 会在转换过程中被忽略
    • 如果被拷贝的对象中,存在 bigint,会报错
    • 如果被拷贝的对象中,存在循环引用的时候,会报错
      let obj = { }
      obj.a = obj
      JSON.parse(JSON.stringify(obj)) // 会报错
      
    • 如果 obj 里面存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串
    • 如果 obj 里有 Set,Map,RegExp,Error 对象,则序列化的结果将只得到空对象,如果直接 JSON.parse(JSON.stringify( )) 这些对象的时候,得到的也只是空对象,但是可以拷贝 Array 数组
    • 如果 obj 里有 NaN、Infinity 和 -Infinity,则序列化的结果会变成 null
    • 不能拷贝函数,JSON.stringify(function) 为 undefined
  • 所以还得自己实现递归函数进行深拷贝
    • 先考虑各种特殊情况
      • 拷贝函数
        function fn(a, b, c) {
        	return a * b * c;
        }
        
        // 方式 1,很多函数库都是用这个方法
        var newFn = new Function('return ' + fn.toString())();
        
        // 方式 2,利用 bind 返回函数,如果 bind 没有参数,或者传入的为 null,则指向 window
        var newFn = fn.prototype.bind();
        
      • 拷贝 symbol
        // 方法 1,返回对象身上全部 symbol 类型组成的数组
        Object.getOwnPropertySymbols(…)
        
        // 方法 2, 获取所有的键,同时包括 Symbol,但不会获取深拷贝原型链上的数据
        Reflect.ownKeys(…)
        
      • 解决循环引用
        • 用 map 存各个值或引用,如果碰到重复,直接返回引用

6. 强引用 和 弱引用

  • 引用数据类型,即引用类型在栈中存储了指针,这个指针指向堆内存中的地址,真实的数据存放在堆内存里
  • 强引用
    • 将一个引用类型数据通过变量或常量保存,这个变量或常量就是强引用
    • 如果存在强引用指向堆内存,则堆内存不会被当作垃圾回收掉
    // test 就是强引用
    let test = { a: 1, b: 2 }
    
  • 弱引用
    • WeakMap 的键只能是弱引用,WeakSet 的值只能是弱引用,WeakRef 传入对象,返回其弱引用
    • 如果存在弱引用指向堆内存,但没有强引用指向堆内存,则堆内存仍然会被当做垃圾回收掉
    • 但是注意,并不是说,没有强引用指向,就直接垃圾回收了,垃圾回收是懒回收,当觉得你这个占用内存过多,影响到运行,才会值行垃圾回收,否则频繁执行垃圾回收,会阻塞
    let a = new Set();
    let b = new WeakSet();
    for(let i = 0; i < 100; i++) {
        a.add({});
        b.add({});
    }
    console.log(a);
    console.log(b); // 此时 b 内还有 100 个元素,都没有被回收
    
    let a = new Set();
    let b = new WeakSet();
    for(let i = 0; i < 10000; i++) {
        a.add({});
        b.add({});
    }
    console.log(a);
    console.log(b); // 此时 b 内没有元素,全部都被回收了
    

7. WeakMap,WeakSet,WeakRef

WeakMap

  • 与 Map 的 api 类似,且其中的键都是唯一的
  • 与 Map 的区别为
    • 因为弱引用,WeakMap 的键是不可枚举的(没有方法能给出所有的键),如果键是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果
    • WeakMap 的键只能是 Object 类型,而不能像 Map 那样,可以是任何类型的任意值
    • 键为弱引用,如果没有其他的对 WeakMap 中键的引用,那么这些键值对会被垃圾回收掉

WeakSet

  • 与 Set 的 api 类似,且其中的元素都是唯一的
  • 与 Set 的区别为
    • 因为弱引用,WeakSet 的元素是不可枚举的(没有方法能给出所有的元素),如果元素是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果
    • WeakSet 只能是 Object 的集合,而不能像 Set 那样,可以是任何类型的任意值
    • 集合中对象的引用为弱引用,如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被垃圾回收掉

WeakRef

  • new WeakRef()
    WeakRef 对象包含对对象的弱引用
    let ref = new WeakRef(targetObject) // ref 即是对 targetObject 的弱引用
    
  • WeakRef.prototype.deref()
    deref 方法返回 WeakRef 实例的目标对象,如果目标对象已被垃圾收集,则返回 undefined
    let obj = ref.deref(); // 这个 obj 是强引用
    

猜你喜欢

转载自blog.csdn.net/m0_52212261/article/details/129772307