javascript-继承和class

loading 2022年10月26日 226次浏览

1. es5中的继承

1.1 原型链继承

最关键的一行是SubType.prototype = new SuperType();

        function SuperType() {
            this.property = "super";
        }
        SuperType.prototype.getSuperValue = function () {
            return this.property;
        };
        function SubType() {
            this.subproperty = "sub";
        }

        // 实现继承的核心语句
        SubType.prototype = new SuperType();
	
	// 修复constructor指向
	SubType.prototype.constructor = SubType;

        SubType.prototype.getSubValue = function () {
            return this.subproperty;
        };
        let instance = new SubType();
        console.log(instance.getSuperValue() , instance.getSubValue()); // super sub

但是要切记要想为子类添加新的属性或方法,一定要等到实现了继承的核心语句后再执行,就像上面的例子一样。

否则先添加属性或方法,再改变prototype的指向是无效的。

	// 实现继承的核心语句
        SubType.prototype = new SuperType();

        SubType.prototype.getSubValue = function () {
            return this.subproperty;
        };

还有一个缺点是引用类型属性容易受到影响

原型上的所有属性是在实例间共享的,如果在一个实例上修改引用类型属性,原型和别的实例也会受到影响。

如下面例子中的age属性,数组受到了影响

        function Person() { }
        Person.prototype = {
            name: "yoimiya",
            age: [1,2],
            sayName() {
                console.log(this.name);
            }
        };
        let friend = new Person();
        let f2 = new Person();

        friend.name = 'eula';
        friend.age.push(3);

        console.log(f2.name , f2.age) //'yoimiya' [1,2,3] 

因此,这种方法的优缺点是:

优点:

  • 父类新增原型属性或方法,子类都能访问到
  • 易于实现

缺点:

  • 原型对象的所有属性被所有实例共享,导致引用类型属性容易受到影响
  • 需要先实现继承才能往子类新增属性或方法
  • 创建子类实例时,无法向父类构造函数传参
  • 无法实现多继承

还有个小问题,这里为什么要修复constructor指向呢?

如果不修复指向的话,obj.constructor就会指向SuperType而非SubType了。

因为用了SubType.prototype = new SuperType()这句话来实现继承,而new SuperType()这个实例的构造函数是SuperType(),那么就会导致SubType.prototype.constructor也指向SuperType()了。

1.2 盗用构造函数继承(利用call/apply)

可以通过call实现继承来改善原型属性被所有实例共享的问题。

通过使用 call()(或 apply())方法,父类构造函数在为子类的实例创建的新对象的上下文中执行了。这相当于新的子类对象上运行了父类函数中的所有初始化代码。结果就是每个实例都会有自己的name和age属性。

        function Person(name , age){
            this.name = name;
            this.age = age;
            this.setName = function(){}
        }
        Person.prototype.setAge = function(){};

        function Student(name , age , price){
            Person.call(this , name , age);
            this.price = price;
        }
        const s = new Student('yoimiya' , 21 , 648)
        console.log(s);
        //age:21 name:yoimiya price:648 setName:f()

这种继承方式的优缺点:

优点:

  • 解决了子类实例共享父类属性的问题
  • 创建子类实例时,可以向父类传参
  • 可以通过call多个父类实现多继承

缺点:

  • 只能继承父类本身的属性和方法,不能继承父类原型上的。(比如上面例子的setAge没继承到)
  • 无法实现函数服用,每个子类都有父类实例函数的副本,影响性能

1.3 组合继承

js中使用最多的继承模式,综合了以上两种继承方式,弥补了各自的缺点。

原型链继承主要缺点是引用属性问题,call/apply继承主要是无法访问prototype上的方法问题。

        function SuperType(name) {
            this.name = name;
            this.colors = ['red', 'blue'];
        }
        SuperType.prototype.sayName = function () {
            console.log(this.name);
        }

        function SubType(name, age) {
            //盗用构造函数继承
            SuperType.call(this, name);
            this.age = age;
        }
        //原型链继承
        SubType.prototype = new SuperType();

	//修复constructor指向
	SubType.prototype.constructor = SubType;

        SubType.prototype.sayAge = function () {
            console.log(this.age);
        }; 

        let obj1 = new SubType('aaa' , 10) , obj2 = new SubType('bbb' , 20);
        obj1.colors.push('green');
        //测试是否避免了引用类型问题
        console.log(obj1.colors , obj2.colors);
        //测试是否继承到原型上的方法
        obj1.sayName();
        obj1.sayAge();

缺点是:
调用了两次父类构造函数,一次是在创建子类型原型的时候(SubType.prototype = new SuperType();),另一次是在子类型构造函数的内部(SuperType.call(this, name);)。

1.4 组合继承优化1

这种方式通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。

和组合继承的区别在第16行

        function SuperType(name) {
            this.name = name;
            this.colors = ['red', 'blue'];
        }
        SuperType.prototype.sayName = function () {
            console.log(this.name);
        }

        function SubType(name, age) {
            //盗用构造函数继承
            SuperType.call(this, name);
            this.age = age;
        }

	//和传统组合继承的区别在这里
        SubType.prototype = SuperType.prototype;

        SubType.prototype.sayAge = function () {
            console.log(this.age);
        };

        let obj1 = new SubType('aaa', 10), obj2 = new SubType('bbb', 20);
        obj1.colors.push('green');
        //测试是否避免了引用类型问题
        console.log(obj1.colors, obj2.colors);
        //测试是否继承到原型上的方法
        obj1.sayName();
        obj1.sayAge();

优点:

  • 不会再初始化两次实例,避免了组合继承的缺点

缺点:

  • 无法辨别实例是子类创造还是父类创造,因为子类父类构造函数指向相同。
        console.log(obj1 instanceof SuperType , obj1 instanceof SubType);//true true
        console.log(obj1.constructor) //SuperType

1.5 组合继承优化2

直接把实现继承核心代码的修改摆上来,别的都和前两种组合继承一样

        SubType.prototype = Object.create(SuperType.prototype);
        SubType.prototype.constructor = SubType;

let B = Object.create(A)以A对象为原型,生成了B对象。B继承了A的所有属性和方法。相对来说是目前最完美的继承方法。

2. es6-类

2.1 基本使用

2.1.1 类的构造

和函数一样,可以用两种方法构造

// 类声明
class Person {} 

// 类表达式
const Animal = class {};

可以在constructor里初始化属性,也可以在class内部顶层直接初始化

        class Father{
            //(1)
            name = 'father';
            //(2)
            constructor(){
                this.name = 'father'
            }
        }

和函数的区别是类被块作用域所限制

        {
            function FunctionDeclaration() { }
            class ClassDeclaration { }
        }
        console.log(FunctionDeclaration); // FunctionDeclaration() {} 
        console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined

2.1.2 get/set关键字

类的内部也可以使用get和set关键字

        class Test{
            constructor(){};
            get fn(){
                console.log(this.val);
            }
            set fn(val){
                this.val = val;
            }
        }

        let obj = new Test();
        //相当于调用set
        obj.fn = 123;
        //相当于调用get
        obj.fn;

2.1.3 静态方法

和别的地方一样,只要在方法前加上static关键字,就表示该方法可以直接通过类来调用。

        class Foo{
            static fn(){
                console.log('hello')
            }
        }
        Foo.fn(); //hello

父类的静态方法,可以被子类继承(通过extends或者super都可以)

extends

        class Father{
            static fn(){
                console.log('hello')
            }
        }
        
        class Son extends Father{}
        Son.fn();

super

        class Father{
            static fn(){
                return 'hello'
            }
        }

        class Son extends Father{
            static fn(){
                console.log(super.fn() + ',world');
            }
        }
        Son.fn(); //hello,world

2.1.4 私有属性

ES2022中,class也引进了自己的私有属性,可以通过属性名前面加'#'来表示。

        class Father{
            #name = 'father'
        }
        console.log(father.#name);//报错

2.1.5 in操作符

如果指定的属性在指定的对象或其原型链中,则in运算符返回true。

        class Father {
            static fn() {
                return 'hello';
            }
            fn1(){}
        }
        let father = new Father();

	//fn1不是静态方法 因此在实例上
        console.log('fn1' in father);
	//fn是静态方法 因此在class上
        console.log('fn' in Father);

2.2 类构造函数

2.2.1 new

constructor 关键字用于在类定义块内部创建类的构造函数。constructor 会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。

使用new实例化时会有以下步骤:

此处用obj表示创建的对象

  1. 在内存中创建一个空对象obj:{}
  2. 将obj内部的__proto__指针指向构造函数construcor的prototype
  3. 将构造函数内部的this指向obj
  4. 执行构造函数内部的代码,由于此时的this指向obj,所以也就是给obj添加属性
  5. 若构造函数原本就返回一个引用类型,则返回该值;否则就返回这个新创建的obj(构造函数默认return this)

如下,不传入true参数则默认返回this值;传入true参数则返回一个对象{ bar: 'bar' }。

        class Person {
            constructor(override) {
                this.foo = 'foo';
                if (override) {
                    return {
                        bar: 'bar'
                    };
                }
            }
        }
        let p1 = new Person(),
            p2 = new Person(true);

        console.log(p1); // Person{ foo: 'foo' } 
        console.log(p1 instanceof Person); // true 

        console.log(p2); // { bar: 'bar' } 
        console.log(p2 instanceof Person); // false 

2.2.2 和普通构造函数的区别

类构造函数和函数构造函数的区别是必须用new。

函数构造函数不用new那么就会以全局的this(通常是window)作为内部对象。调用类构造函数时如果忘了使用new则会抛出错误。

        function Person() { }
        class Animal { }
        // 把 window 作为 this 来构建实例
        let p = Person();
        let a = Animal(); // TypeError: class constructor Animal cannot be invoked without 'new' 

window中新加入Person属性

然而其实类本质上其实也是一种特殊的函数

class Person {} 
console.log(Person); // class Person {} 
console.log(typeof Person); // function

因此立即执行类也是可以实现的

        let p = new class classNameTest{
            constructor(x) {
                console.log(x);
            }
        }('bar'); // bar 
        console.log(p); // classNameTest{} 

2.3 类的实例

为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法,把通过static关键字修饰的方法作为类上的静态方法

        class Person {
            constructor() {
                // 添加到 this 的所有内容都会存在于不同的实例上
                this.locate = () => console.log('instance', this);
            }
            // 定义在类的原型对象上
            locate() {
                console.log('prototype', this);
            }
            // 定义在类本身上
            static locate() {
                console.log('class', this);
            }
        }

        let p = new Person();
        p.locate(); 
        Person.prototype.locate(); 
        Person.locate();

可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据

class Person { 
 name: 'Jake' 
} 
// Uncaught SyntaxError: Unexpected token

虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加:

        class Person {
            // constructor(){
            //     this.name = 'asd'
            // }
            sayName() {
                console.log(`${Person.greeting} ${this.name}`);
            }
        }
        Person.greeting = 'asfawf'
        Person.prototype.name = 'dfg'
        let p = new Person();
        p.sayName()

3. es6-继承

3.1 extends

可以通过extends继承类或者普通函数

        // 继承类
        class Vehicle { }        
        class Bus extends Vehicle { }
        let b = new Bus();
        console.log(b instanceof Bus); // true 
        console.log(b instanceof Vehicle); // true 

        // 继承普通构造函数
        function Person() { }
        class Engineer extends Person { }
        let e = new Engineer();
        console.log(e instanceof Engineer); // true 
        console.log(e instanceof Person); // true

this的值会反映调用相应方法的实例或者类

        class Vehicle {
            identifyPrototype(id) {
                this.test = ''
                console.log(id, this);
            }
            static identifyClass(id) {               
                console.log(id, this);
            }
        }

        class Bus extends Vehicle { }

        let v = new Vehicle();
        let b = new Bus();

        b.identifyPrototype('bus');
        v.identifyPrototype('vehicle');

        Bus.identifyClass('bus');
        Vehicle.identifyClass('vehicle');

3.2 super

子类方法可以通过super关键字引用它们的原型,这个关键字只能在派生类中使用,而且只能用于类构造函数和实例方法,静态方法内部

箭头函数不能使用arguments、super和new.target,也不能用作构造函数。此外,箭头函数也没有prototype属性。

类构造函数调用super()

        class Vehicle {
            constructor(name) {
                this.name = name;
            }
        };
        class Bus extends Vehicle {
            constructor(name) {
                super(name); //相当于super.constructor(name)
                console.log(this instanceof Vehicle); //true
                console.log(this); //Bus {name: 'good bus'}
            }
        };
        let bus = new Bus('good bus');

另一方面,super也可以作为对象使用。

比如下面的例子,在静态方法中,super指向父类;在普通方法中,super指向父类的原型对象)
普通方法调用super()

        class A{
            p(){
                return 1;
            }
        }
        class B extends A{
            constructor(){
                super();
                console.log(super.p());
            }
        }
        let b = new B();

静态方法调用super()

        class Vehicle {
            static identify() {
                console.log('vehicle');
            }
        }
        class Bus extends Vehicle {
            static identify() {
                super.identify();
            }
        }
        Bus.identify(); // vehicle

同时要注意super作为对象时,只能取到父类的原型对象上的属性,不能取到父类本身的属性

        class A {
            constructor() {
                this.x = 2;
            }
        }
        A.prototype.y = 3;

        class B extends A {
            constructor(){
                super();
                console.log(super.x);
                console.log(super.y);
            }
        }

        let b = new B();//undefined 3

3.3 抽象基类

new.target保存通过new关键字调用的类或函数。因此可以通过它实现一个类:可以被继承,但本身不能被实例化

        class Vehicle {
            constructor() {
                console.log(new.target);
                if (new.target === Vehicle) {
                    throw new Error('Vehicle cannot be directly instantiated');
                }
            }
        }
        // 派生类
        class Bus extends Vehicle { }
        new Bus(); // class Bus {} 
        new Vehicle(); // class Vehicle {} 
        // Error: Vehicle cannot be directly instantiated

也可以在抽象基类构造函数中进行一些检查操作,比如要求子类必须实现某个方法:

        // 抽象基类
        class Vehicle {
            constructor() {
                if (new.target === Vehicle) {
                    throw new Error('Vehicle cannot be directly instantiated');
                }
                if (!this.foo) {
                    throw new Error('Inheriting class must define foo()');
                }
                console.log('success!');
            }
        }

        // 派生类
        class Bus extends Vehicle {
            foo() { }
        }
        // 派生类
        class Van extends Vehicle { }

        new Bus(); // success! 
        new Van(); // Error: Inheriting class must define foo()

3.4 类的注意点

(1)严格模式

类和模块的内部,默认就是严格模式,所以不需要使用'use strict'指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。

(2)不存在变量提升

class不像用var定义的变量或者函数,不会变量提升。

	let f = new Foo()//报错
	class Foo{}

(3)需要注意的this指向

        class Logger{
            printName(name = 'world'){
                this.print(`hello ${name}`)
            }
            print(text){
                console.log(text);
            }
        }

        const logger = new Logger();
        //logger.printName();

        //对象的解构赋值
        const {printName} = logger;
        printName();//报错

printName()中的this默认指向Logger类的实例,因此如果按被注释掉那句写是可以正常输出的。但是现在将printName()单独提取出来使用,this本该指向window,但是class内部是严格模式,因此相当于指向undefined,导致找不到print()而报错。

解决方法是在constructor中用bind绑定this

        class Logger{
            constructor(){
                this.printName = this.printName.bind(this);
            }
            printName(name = 'world'){
                this.print(`hello ${name}`)
            }
            print(text){
                console.log(text);
            }
        }

(4) class的proto和prototype
es5时,每个对象有自己的proto属性指向构造函数的prototype。

而class作为构造函数的语法糖,同时具有proto和prototype属性,因此存在两条链:

  • 子类的proto总是指向父类,表示构造函数的继承
  • 子类的prototype的proto总是指向父类的prototype,表示方法的继承。
        class A{};

        class B extends A{};

        console.log(B.__proto__ === A); //true
        console.log(B.prototype.__proto__ === A.prototype) //true

3.5 类混入

js可以通过混入模式实现多继承,实现起来比较复杂

混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。

        class Vehicle {}
        let FooMixin = (Superclass) => class extends Superclass {
            foo() {
                console.log('foo');
            }
        };
        let BarMixin = (Superclass) => class extends Superclass {
            bar() {
                console.log('bar');
            }
        };
        let BazMixin = (Superclass) => class extends Superclass {
            baz() {
                console.log('baz');
            }
        };
	
	//解嵌套函数
        function mix(BaseClass, ...Mixins) {
            return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
        }
        
        class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) { }
        let b = new Bus();
        b.foo(); // foo 
        b.bar(); // bar 
        b.baz(); // baz