继承的原理 、 继承,深浅拷贝 ,闭包 , 沙箱模式

109 阅读8分钟

继承的原理

如果 子类没有某一种属性或者方法 我们可以 通过继承的方式 让子类继承到父类的方法和属性

  • 优点: 实现了继承 可以使用自身没有的属性和方法
  • 缺点:继承了父类的方法和属性,但不在自己身上,失去了自己的原型
        function Person(name) {
            this.name = name;
        }
        Person.prototype.sayName = () => {
            console.log('name');
        }

        function Stu(age) {
            this.age = age;
        }
        
        const s = new Stu(18);

        console.log(s);  // {age: 18}
        
            // 如果我们想要 s 这个对象里面有一个属性, 可以使用 sayName 这个方法 我们就可以通过继承的方式 让Stu这个类,继承上Person上的方法和属性

原型继承

此时第一个函数时父类, 第二个函数是子类 子类可以继承父类内部的属性和方法 在继承中 父类和子类不是绝对的 第一个函数也可以为字类 第二个函数也可以为父类

  1. 去 对象内部查找 name 属性, 然后发现对象内部没有这个属性
  2. 去这个对象内部的 proto 上查找, 对象的__proto__ 指向了自己构造函数的 原型 也就是 指向了 Stu.prototype
  3. 因为我们手动修改了 Stu.prototype, 给他赋值为了 Person 构造函数的实例化对象
  4. 也就是说 我们 Stu.prototype 就指向了 Person的实例化对象
  5. Person 的实例化对象 内部 有一个属性叫做 name, 此时找到并返回
        // 第一个函数 
        function Person(name) {
            this.name = name
        }
        Person.prototype.sayName = () => {
            console.log('name')
        }


        // 2. 第二函数 
        function Stu(age) {
            this.age = age;
        }
        Stu.prototype = new Person('QF001'); 
        const s = new Stu(18); 
        console.log(s);
        console.log(s.age);   // 18
        console.log(s.name) // QF001
        

借用构造函数继承

核心: 借用 call 方法修改 父类构造函数内部的 this 指向

  1. 优点: 将属性继承在自己身上,保留了自己的原型
  2. 缺点: 只能继承父类的 属性, 不能继承父类原型上的方法
            function Person(name) {
            this.abc = name
            this.name = name
        }
        Person.prototype.sayName = () => {
            console.log('name')
        }
        
        
        
        function Stu(age) {
            this.age = age
            Person.call(this, 'QF002')
        }

        const s = new Stu(18)

        // 需求 使用 Person 内部的 name 和 sayName 方法
        console.log(s)
        console.log(s.age)
        console.log(s.abc)
        console.log(s.name)
  1. 通过 new 关键字调用 Stu 这个构造函数

  2. new 关键会在这个函数内部创建一个空对象, 并且 会把这个函数内部的 this 指定刚才创建的空对象

  3. 我们现在开始正常执行函数代码

    3.1 给这个对象 添加一个 age 属性, 并且给他赋值 为 参数 age 参数 age === 18

    3.2 调用 Perosn 这个 函数, 并通过 call 方法 改变这个函数内部的 this 指向, this 指向了 刚才 new 关键字创建的 对象,Person 函数开始执行,给这个对象 添加一个 abc 的属性, 并赋值为 参数 name 参数 name === 'QF002' , 给这个对象 添加一个 name 的属性, 并赋值为 参数 name 参数 name === 'QF002'

  4. 现在函数执行完毕, 然后 new关键字 会将刚才创建的 对象 return

  5. 将这个对象 保存在了 变量 s 中

  6. 通过上述流程, 我们认为 这个对象中应该是 {age:18, abc: 'QF002', name: 'QF002'}

image.png

组合继承

  1. 利用 原型继承 继承到 父类的 原型上的方法
  2. 利用 借用继承 继承到 父类的 构造函数内部的 属性
    • 好处:
      1. 能继承到属性, 并且是在自己对象内部(自己身上)
      2. 能够继承到 父类原型上的方法
    • 缺点: 在原型上 有一套多余的属性
        function Person(name) {
            this.abc = name
            this.name = name
        }
        Person.prototype.sayName = () => {
            console.log('name')
        }

        function Stu(age) {
            this.age = age

            // 2. 利用 借用继承 继承到 父类的 构造函数内部的 属性
            Person.call(this, 'QF001')
        }

        // 1. 利用 原型继承 继承到 父类的 原型上的方法
        Stu.prototype = new Person()
        

        const s = new Stu(18)

        console.log(s)
        console.log(s.name)
        /**
         * 查找对象内部属性的时候, 会先在对象内部查找, 找到直接使用, 并停止查找
        */
        s.sayName()

拷贝继承

通过 for...in 循环遍历 父类Person 实例化对象上的所有属性以及方法 将它拷贝到字类上

       function Person(name){
            this.name = name;
        }
        Person.prototype.setName = () => {
            console.log('name');
        }

        function Stu(age){
            this.age = age;
            const p = new Person('QW111');
            for(let key in p){
                // 通过for in 循环遍历到父类实例化对象上的所有属性,拷贝到字类上 
                Stu.prototype[key] =p[key];
            }
        }
        

        const s = new Stu(18);
        console.log(s);
        // 使用 Person 上的name属性
        console.log(s.name); // QW111
        // Stu 使用到 Person原型上的方法
        s.setName();  // name

image.png

ES6 类的继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

        class Person{   // 父类  =>   Person

        }
        class Stu extends Person{   // 子类  =>   Stu    
            
        }
            // 父类是 Person 字类是 Stu , Stu 通过关键字 extends继承了父类 Person 上的所有属性和方法
  1. 在书写 子类的时候 语法: class 子类类名 extends 父类类名
  2. 在书写 子类 constructor 需要在内部书写 super()
  • 注意:
    1. 两个写法 必须同时存在, 才能完成继承
    2. super() 必须在 constructor 在开始位置
  • 类的继承, 除了能够继承 calss 类, 还能够继承 ES5 的构造函数
class Person{
            constructor(name){
                this.name = name;
            }
            setName(){
                console.log('name');
            }
        }

        class Stu extends Person{
            constructor(age){  
                super('QF111');
                this.age = age;
            }
        }

        const s = new Stu(19);
        console.log(s);
        console.log(s.name);  // 父类继承来的属性name
        s.setName();  // 父类继承来的方法 setName

image.png

深浅拷贝

  • 一定是 引用数据类型

  • 对象, 数组, 函数(基本只有对象和数组)

  • 赋值:

    • 只要是引用数据类型, 那么在赋值的时候, 就是引用地址的传递
  • 浅拷贝

    1. 遍历对象拿到对象的每一个key与value
    2. 然后赋值给另外一个 对象
    3. 如果所有的 value 都是基本数据类型, 那么浅拷贝完成之后, 修改新对象不会影响到老对象
    4. 如果 value 有引用数据类型(出现了多层数据结构), 那么浅拷贝只能拷走第一层数据结构, 多层的没有办法处理
        let obj = {
            a:1,
            b:2,
            c:3,
            d:{
                d1:011,
                d2:02,
                d3:03
            }
        }

        let obj1 = {}; // 定义一个空对象  用于存储拷贝后的数据

        // 通过for..in 遍历拿到对象 obj 的所有 key 值
        for(let key in obj){
            // 将 对象 obj 中的所有 key 值 赋值给 空对象 obj1
            obj1[key] = obj[key];
        }
        console.log(obj1);
        console.log(obj);
        console.log(obj === obj1); // false
        
        obj1.b = 999;  
        console.log(obj1); 
        console.log(obj);
        console.log(obj1.d.d1);
        // 修改拷贝后的数据  原数据不会发生改变

image.png

  • JS 提供的浅拷贝方法
    • 语法:Object.assign(新对象, 原始对象);
    • 将 原始对象 中所有的 key 全部浅拷贝到 新对象中, 然后返回一个 对象
        let obj = {
            a:1,
            b:2,
            c:3,
            d:{
                d1:011,
                d2:02,
                d3:03
            }
        }

        let obj1 = Object.assign({},obj)
        console.log(obj1);
        console.log(obj);

深拷贝

不管数据结构有多少层数据结构,都会把它复制出来,其一模一样,但复制出来的数据和原数据毫不相干

        let obj = {
            a:1,
            b:2,
            c:3,
            d:{
                d1:11,
                d2:22,
                d3:33
            }
        }
     

        let obj1 = {};   // 定义一个空对象   用于存储拷贝后的对象
        function fun1(target,origin){
            // 将origin 中的数据 拷贝到 target中
            for(let key in origin){
                // 对象 就让它
                if(origin[key].constructor === Object){
                    target[key] = {};
                    fun1(target[key],origin[key]);
                // 数组 
                }else if (origin[key].constructor === Array) {
                    target[key] = [];
                    deepClone(target[key], origin[key])
                // 数值或字符串
                } else {
                    target[key] = origin[key]
                }
            }

        }

        fun1(obj1,obj);
        console.log(obj1);
        console.log(obj);  
        console.log(obj1 === obj);  // false

image.png

JS中 深拷贝的方法

 // 方法
        let obj2 = JSON.parse(JSON.stringify(obj));
        obj2.d.d2 = 666;
        console.log('拷贝后的对象:',obj2);
        console.log('原对象:',obj);

image.png

闭包

  • 闭包:就是能够读取外层函数内部变量的函数。

  • 闭包需要满足三个条件

    1. 需要一个不会被销毁的函数执行空间
    2. 需要 直接 或 间接 的返回一个函数
    3. 内部函数使用着 外部函数的私有变
  • 优点: 可以重复使用变量 (延长变量的作用域 [使用时间] ),并且不会造成变量污染 (函数外部可以使用函数内部的变量) 。

  • 缺点: 会引起内存泄漏,因为闭包的存在就一定代表一个函数执行空间不会被销毁, 如果大量使用闭包, 会导致内存空间占据严重

  • 使用闭包的注意点:

    • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会 造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的 局部变量全部删除。
    • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象 (object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性 (private value),这时一定要小心,不要随便改变父函数内部变量的值
       function outer() {
            let a = 100;
            let str = 'QF111';
            let obj = {
                a: 1,
                b: 2,
                c: 3
            }
            function inner() {
                return a;  // 返回外部变量
            }
            return inner;  // 返回内部函数
        }

        let res = outer();
        console.log(res);  // inner函数
        let newRws = res();
        console.log(newRws); // 100

image.png

沙箱模式

沙箱模式 就是 利用 间接 返回一个函数, 然后去拿到 外部函数内的私有变量

 function outer(){
            let a = 100;
            let str = 'QF111';
            const obj = {
                getA:function(){
                    return a;
                },
                getStr:function(){
                    return str;
                },
                setA(val){
                    a = val;
                }
            }
            return obj;
        }

        let res = outer();
        console.log(res);  // 返回对象

        let res1 = res.getA(); // 100
        console.log(res1);

        let res2 = res.getStr();
        console.log(res2);    // QF111

        res.setA(999);
        console.log(res.getA()); // 999

image.png

沙箱模式的优化

通过 getter 和setter 来简化代码的书写

        function outer(){
            let a = 100;
            let b = 300;
            const obj = {
                get a(){
                    return a;
                },
                set a(val){
                    a = val;
                },
                get b(){
                    return b;
                },
                set b(val){
                    b = val;
                }
            }
            return obj;
        }

        let res = outer();
        console.log(res);
        console.log(res.a); // 100
        res.a = 999;
        console.log(res.a); // 999

image.png