js继承及实现方式

115 阅读8分钟

继承

一. 继承的介绍:

继承是面向对象编程中的一个重要概念,通过继承可以使子类的实例使用在父类中定义的属性和方法。

1.1 什么是继承

  • 生活:子承父业(好处少奋斗几十年)
  • 编程:子对象继承父对象中的属性或方法。

1.2 继承的作用

  • 减少代码冗余(简化代码)
  • 提高代码的可维护性
  • 提高性能

1.3 javascript 语言的继承机制

Javascript 没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承。

拓展----从很久很久以前说起

原文-阮一峰

我们都知道 js 设置之初是为了做表单校验的 所以当时网景公司的布兰登·艾奇(Brendan Eich)没有把他设计的过于复杂 但是,Javascript 里面都是对象,必须有一种机制,将所有对象联系起来。所以,最后还是设计了"继承"。但是他为了让 Javascript 更加易于学习 降低初学者的难度 没有引用"类”的概念, 而是把 C++和 Java 语言 new 命令引入了 Javascript 同时做了个简化的设计 new 命令后面跟的不是类,而是构造函数。但是用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。考虑到这一点,布兰登·艾奇(Brendan Eich)决定为构造函数设置一个 prototype 属性。实例对象一旦创建,将自动引用 prototype 对象的属性和方法。

1.4 基本数据类型和引用数据类型在内存中的区别

  • 基本数据类型:string、number、boolean、undefined、null
  • 引用数据类型: 对象、数组、函数 内存:栈、堆 栈:基本数据类型的数据 和 引用数据类型的引用(地址) 堆:引用数据类型的数据

1.5 new关键字的作用

作用:通过调用构造函数创建对象 过程

  • 创建一个空对象object
  • 设置原型链
  • 改变构造函数中的this
  • 返回一个对象
        function Parent(name) {
            this.name = name;
        }
        let zhangsan = new Parent('张三');
        // 上述代码中new了一个Parent,这其中的过程如下:
        // 第一步:创建一个空对象object,创建对象新对象,就是指在栈内新建了一个obj,这个obj实际上是指的堆中对应的一个地址。
        let obj = new Object()
        // 第二步:设置原型链 即设置新建对象obj的隐式原型即_proto_属性指向构造函数Parent的显示原型prototype对象
        obj.proto = Parent.prototype
        // 第三步:改变构造函数Parent的this绑定到新对象obj,并且利用call()或者是apply()来执行构造函数Parent,如下:
        let result = Parent.call(obj,arguments)
        // 第四步:确保构造器总是返回一个对象
        // 将第三步中初始化完成后的对象地址,保存到新对象中,同时要判断构造函数Parent的返回值类型,
        // 为什么要判断值类型呢?
        // 因为如果构造函数中返回this或者是基本数据类型(number数值,string字符串,Boolean布尔,null,undefined)的值时,
        // 这个时候则返回新的实例对象,如果构造函数返回的值是引用类型的,则返回的值是引用类型,如下:
        if (typeof (result) === "object") {
            func = result;
        } else {
            func = obj; // 默认返回
        }

二. 继承分类:

先来个整体印象。如图所示,JS 中继承可以按照是否使用 object 函数(在下文中会提到),将继承分成两部分(Object.create 是 ES5 新增的方法,用来规范化这个函数)。

继承分类.png

其中,原型链继承和原型式继承有一样的优缺点,构造函数继承与寄生式继承也相互对应。寄生组合继承基于 Object.create, 同时优化了组合继承,成为了完美的继承方式。ES6 Class Extends 的结果与寄生组合继承基本一致,但是实现方案又略有不同。

2.1 原型链继承

核心:将父类的实例作为子类的原型

  • 优点:父类的属性或方法可以复用

  • 实现:

        function Parent() {
            this.name = 'czr';
        }

        Parent.prototype.getName = function () {
            console.log(this.name);
        }

        function Child() {}

        Child.prototype = new Parent();

        var child1 = new Child();

        child1.getName()
        console.log(child1.name)
  • 缺点: 1.引用类型的属性被所有实例共享(最重要的) 所以不能在不改变其他实例情况下改变。 2.继承的属性是没有意义的 在创建 Child 的实例时,不能向 Parent 传参
  • 实现:
       function Parent(type, age) {
            this.type = type
            this.age = age
            this.names = ['赵', '钱'];
        }

        function Child() {
            this.name = ''
        }

        Child.prototype = new Parent('Child', 22);

        var child1 = new Child();
        child1.names.push('孙');
        console.log(child1.names); // ['赵', '钱','孙'];

        var child2 = new Child();
        console.log(child2.names); // 预期结果: ['赵', '钱']; 实际结果 ['赵', '钱','孙'];

call方法的使用

函数体内的 this 指向不是在定义时决定的,而是在函数调用时决定的。(函数中 this 指向调用者)

  • 作用:指定this的同时给函数传入一些参数

  • 语法: 函数名.call(指定this,参数 1,参数 2...)

  • call 方法的使用例子

        var value = 'window'
        var foo = {
            value: 'obj-foo'
        }
        function bar () {
            console.log(this.value)
        }
        bar.call(null)

        bar.call(foo)
        Function.prototype.call2 = function (context) {
            // this 参数可以传 null,当为 null 的时候,视为指向 window
            var context = context || window;
             // 获取调用call的函数,用this可以获取
            context.fn = this;

            var args = [];
            // 因为arguments是类数组对象,所以可以用for循环
            // 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]
            for (var i = 1, len = arguments.length; i < len; i++) {
                args.push('arguments[' + i + ']');
            }
            // eval('context.fn(' + args +')') 相当于带参调用了context.fn
            // context.fn("arguments[1],arguments[2],arguments[3]")
            var result = eval('context.fn(' + args + ')');

            delete context.fn
            // 防止函数有返回值
            return result;
        }
        // 测试一下
        var value = 2;

        var obj = {
            value: 1
        }

        function bar(name, age) {
            console.log(this.value);
            return {
                value: this.value,
                name: name,
                age: age
            }
        }

        // bar.call2(null); // 2

        console.log(bar.call2(obj, 'kevin', 18));
  • 模拟apply方法的实现机制
        Function.prototype.apply = function (context, arr) {
            var context = context || window;
            context.fn = this;

            var result;
            if (!arr) {
                result = context.fn();
            } else {
                var args = [];
                for (var i = 0, len = arr.length; i < len; i++) {
                    args.push('arr[' + i + ']');
                }
                result = eval('context.fn(' + args + ')')
            }

            delete context.fn
            return result;
        }

2.2 借用构造函数实现继承

  • 核心:将父类构造函数的内容复制给了子类的构造函数。

  • 优点:

    1. 父类的引用属性不会被共享
    2. 子类构建实例时可以向父类传递参数
  • 实现:

        function Parent(age) {
            this.age = age
            this.names = ['kevin', 'daisy'];
            this.say = function () {
                console.log(this.names)
            }
        }

        function Child(age) {
            Parent.call(this, age);
        }
        var child1 = new Child(22);

        child1.names.push('czr');
        console.log(child1.age)
        console.log(child1.names); // ["kevin", "daisy", "czr"]
        var child2 = new Child();

        console.log(child2.names); // ["kevin", "daisy"]
  • 缺点:
    1. 父类的方法不能复用,子类实例的方法每次都是单独创建的。方法没有实现继承

    原因:call方法在被Person调用执行时仅仅让调用者执行函数内部的程序。函数外部的其他程序没有借用执行

        function Parent(names) {
            this.say = function () {
                console.log(this.names)
            }
        }
        // Parent.prototype.say = function () {
        //     console.log(this.names)
        // }
        function Child() {
            Parent.call(this);
        }

        var child1 = new Child('child1');
        var child2 = new Child('child2');
        console.log(child1.say === child2.say)  // false

2.3 组合继承

  • 核心:原型链继承和构造函数继承的组合,兼具了二者的优点。
  • 优点
    1. 父类的方法可以被复用
    2. 父类的引用属性不会被共享
    3. 子类构建实例时可以向父类传递参数
  • 实现:
        function Parent(name) {
            this.name = name;
            this.colors = ['red', 'blue', 'green'];
        }

        Parent.prototype.getName = function () {
            console.log(this.name)
        }
        function Child (name,age) {
            Parent.call(this,name)
            this.age = age
        }
        Child.prototype = new Parent()
        // 此处是为了让新创建的实例能够找到创建它的构造函数。比如:child1.constructor
        // Child.prototype.constructor = Child;
        
        var child1 = new Child('czr')
        // console.log(child1.constructor)
        console.log(child1)
        child1.colors.push('black')
        console.log(child1.colors)

        // var child2 = new Child('czr1')
        // console.log(child2.colors)
  • 缺点: 调用了两次父类的构造函数,第一次给子类的原型添加了父类的name, arr属性,第二次又给子类的构造函数添加了父类的name, arr属性。这种重复创建情况造成了性能上的浪费。
        var num = 0
        function Parent(name) {
            num++
            console.log('我是被调用的次数',num)
            this.name = name;
            this.colors = ['red', 'blue', 'green'];
        }

        Parent.prototype.getName = function () {
            console.log(this.name)
        }
        function Child (name,age) {
            Parent.call(this,name)
            this.age = age
        }
        Child.prototype = new Parent('我是父类');
        // 此处是为了让新创建的实例能够找到创建它的构造函数。比如:child1.constructor
        Child.prototype.constructor = Child;
        
        var child1 = new Child('子类')
        console.log(child1)

2.4 原型式继承

  • 与原型链继承的不一致处:用一个空的构造函数去取代执行了 Parent 这个构造函数。
  • 核心:原型式继承的object方法本质上是对参数对象的一个浅复制。
  • 优点:父类方法可以复用
  • 实现
        // ES5 Object.create 的模拟实现: 将传入的对象作为创建的对象的原型。
        function createObj(o) {
            function F() {}
            F.prototype = o;
            return new F();
        }


        // 原型式继承
        // 核心:原型式继承的createObj方法本质上是对参数对象的一个浅复制。
        // 优点:父类方法可以复用
        var person = {
            name: 'kevin',
            ages: [22, 44],
            foo: () => {
                console.log(this.name)
            }
        }

        var Child1 = createObj(person);
        var Child2 = createObj(person);

        Child1.name = 'heihei';
        console.log(Child1.name); // heihei
  • 缺点: 父类的引用属性会被所有子类实例共享 子类构建实例时不能向父类传递参数
        var person = {
            name: 'kevin',
            ages: [22, 44],
            foo: () => {
                console.log(this.name)
            }
        }

        var Child1 = Object.create(person);
        var Child2 = Object.create(person);


        Child1.ages.push(66);
        console.log(Child1.ages); // [22, 44,66]

        console.log(Child1.ages); // [22, 44,66]

2.5 寄生式继承

  • 核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
  • 优点: 没啥优点 仅是一种思路
  • 缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
  • 实现:
        function createObj(o) {
            var clone = Object.create(o);
            clone.sayName = function () {
                console.log('hi');
            }
            return clone;
        }
        var person = {
            name: "obj对象",
        };
        var child1 = createObj(person) 
        child1.sayName() // hi

2.6 寄生组合继承

  • 刚才说到组合继承有一个会两次调用父类的构造函数造成浪费的缺点,寄生组合继承就可以解决这个问题。同时完美解决了prototype属性的纯净
  • 优缺点:这是一种完美的继承方式。
  • 实现:
    function Parent(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }

    Parent.prototype.getName = function () {
        console.log(this.name)
    }

    function Child(name, age) {
        Parent.call(this, name);
        this.age = age;
    }

    function object(o) {
        function F() {}
        F.prototype = o;
        return new F();
    }

    function prototype(child, parent) {
        var prototype = object(parent.prototype);
        prototype.constructor = child;
        child.prototype = prototype;
    }

    // 当我们使用的时候:
    prototype(Child, Parent);

2.7 ES6 Class extends

  • 核心: ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。
  • es5与es6的区别

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上(Parent.call(this)), ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this;

class A {}

class B extends A {
  constructor() {
    super();
  }
}

ES6实现继承的具体原理:

class A {
}

class B {
}

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

三. 总结

  • ES6 Class extends是ES5继承的语法糖 由于时间关系没来得及给大家分享深感抱歉
  • JS的继承除了构造函数继承之外都基于原型链构建的