zoukankan      html  css  js  c++  java
  • [ES6]ES6语法中的class、extends与super的原理

    class

    首先, 在JavaScript中, class类是一种函数

    class User {
        constructor(name) { this.name = name; }
        sayHi() {alert(this.name);}
    }

    alert(typeof User); // function

    class User {…} 构造器内部干了啥?

    1. 创建一个以User为名称的函数, 这是类声明的结果(函数代码来自constructor中)
    2. 储存所有方法, 例如User.prototype中的sayHi
    alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

    class并不是JavaScript中的语法糖, 虽然我们可以在没有 class 的情况下声明同样的内容:

    // 以纯函数的重写 User 类

    // 1. 创建构造器函数
    function User(name{
         this.name = name;
    }
    // * 任何函数原型默认具有构造器属性,
    // 所以,我们不需要创建它

    // 2. 向原型中添加方法
    User.prototype.sayHi = function() {
        alert(this.name);
    };

    // 使用方法:
    let user = new User("John");
    user.sayHi();
    两者存在重大差异
    1. 首先,通过 class 创建的函数是由特殊内部属性标记的 [[FunctionKind]]:"classConstructor"。不像普通函数,调用类构造器时必须要用 new 关键词:

      class User {
         constructor() {}
      }

      alert(typeof User); // function
      User(); // Error: 没有 ‘new’ 关键词,类构造器 User 无法调用

      此外,大多数 JavaScript 引擎中的类构造函数的字符串表示形式都以 “class” 开头

      class User {
       constructor() {}
      }

      alert(User); // class User { ... }
    2. 方法不可枚举。 对于 "prototype" 中的所有方法,类定义将 enumerable 标记为false

      这很好,因为如果我们对一个对象调用 for..in 方法,我们通常不希望 class 方法出现。

      枚举实例属性时, 不会出现class方法; 而普通创建的构造函数, 枚举实例属性时会出现prototype上的方法。

    3. 类默认使用 use strict。 在类构造函数中的所有方法自动使用严格模式。

    Getters/setters 及其他 shorthands

    就像对象字面量,类可能包括 getters/setters,generators,计算属性(computed properties)等。

    使用 get/set 实现 user.name 的示例:

    class User {

      constructor(name) {
        // 调用 setter
        this.name = name;
      }

      get name() {
        return this._name;
      }

      set name(value) {
        if (value.length < 4) {
          alert("Name is too short.");
          return;
        }
        this._name = value;
      }

    }

    let user = new User("John");
    alert(user.name); // John

    user = new User(""); // Name too short.

    除了使用getter/setter语法,大多数时候我们首选 get…/set… 函数

    class CoffeeMachine {
      _waterAmount = 0;

      set waterAmount(value) {
        if (value < 0throw new Error("Negative water");
        this._waterAmount = value;
      }

      get waterAmount() {
        return this._waterAmount;
      }
    }

    new CoffeeMachine().waterAmount = 100// setter 赋值函数
    class CoffeeMachine {
      _waterAmount = 0;

      setWaterAmount(value) {
        if (value < 0throw new Error("Negative water");
        this._waterAmount = value;
      }

      getWaterAmount() {
        return this._waterAmount;
      }
    }

    new CoffeeMachine().setWaterAmount(100);

    虽然这看起来有点长,但函数更灵活。他们可以接受多个参数(即使我们现在不需要它们)// 更加灵活,原来getter中不能加参数,setter中只可以加一个参数,newVal,但是使用了函数后可以自定义加任意的参数

    类声明在 User.prototype 中创建 getterssetters,示例:

    Object.defineProperties(User.prototype, {
      name: {
        get() {
          return this._name
        },
        set(name) {
          // ...
        }
      }
    });
    class属性
    class User {
      name = "Anonymous";

      sayHi() {
        alert(`Hello, ${this.name}!`);
      }
    }

    new User().sayHi();

    属性不在 User.prototype 内。相反它是通过 new 分别为每个对象创建的。所以,该属性永远不会在同一个类的不同对象之间共享。

    总结

    基本的类语法:

    class MyClass {
        prop = value;  // filed 公有字段声明(通过new分别为每个对象创建)
        #prop = value; // field 私有字段声明(从类外部引用私有字段是错误的。它们只能在类里面中读取或写入。)

        static prop = value; // 静态属性(存储类级别的数据,MyClass本身的属性, 而不是定义在实例对象this上的属性, 只能通过 MyClass.prop 访问);静态属性是继承的。

        constructor(...) { // 构造器
            // ...
        }

        method(...) {} // 方法

        static method(...) {} // 静态方法被用来实现属于整个类的功能,不涉及到某个具体的类实例的功能;静态方法是继承的;

        get something(...) {} // getter 方法
        set something(...) {} // setter 方法

        [Symbol.iterator]() {} // 计算 name/symbol 名方法 // 变量做属性
    }

    由于extends创建了两个[[prototype]]的引用

    1. Rabbit方法原型继承自Animal方法
    2. Rabbit.prototype 原型继承自Animal.prototype

    Rabbit.__proto__ === Animal,因此对于class B extends A,类B的prototype指向了A,所以如果一个字段在B中没有找到,会继续在A中查找。故而静态属性和方法都是被继承的

    技术上来说,静态声明等同于直接给类本身赋值:

    class MyClass {
      static property = ...;

      static method() {
        ...
      }
    }
    // 等同于
    MyClass.property = ...
    MyClass.method = ...

    实例属性的新写法:

    实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层

    class IncreasingCounter {
      constructor() {
        this._count = 0// (*)
      }
      get value() {
        console.log('Getting the current value!');
        return this._count;
      }
      increment() {
        this._count++;
      }
    }

    上面代码中,实例属性this._count定义在constructor()方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。

    class IncreasingCounter {
      _count = 0// (**)
      get value() {
        console.log('Getting the current value!');
        return this._count;
      }
      increment() {
        this._count++;
      }
    }

    上面代码中,实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this

    这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性

    class foo {
      bar = 'hello';
      baz = 'world';

      constructor() {
        // ...
      }
    }

    上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。

    extends

    根据规范,如果一个类继承了另一个类并且没有constructor,那么将生成以下"空" constructor:

    class Rabbit extends Animal {
        // 为没有构造函数的继承类生成以下的构造函数
        constructor(...ars) {
            super(...args);
        }
    }
    class Animal {
      constructor(name) {
        this.speed = 0;
        this.name = name;
      }

      run(speed) {
        this.speed += speed;
        alert(`${this.name} runs with speed ${this.speed}.`);
      }
      stop() {
        this.speed = 0;
        alert(`${this.name} stopped.`);
      }
    }

    class Rabbit extends Animal {
      hide() {
        alert(`${this.name} hides!`);
      }
    }

    let rabbit = new Rabbit("White Rabbit");
    console.log(rabbit); // console: Rabbit {speed: 0, name: "White Rabbit"}
    rabbit.run(5); // White Rabbit runs with speed 5.
    rabbit.hide(); // White Rabbit hides!

    extends干了啥?

    通过指定"extends Animal"让 Rabbit继承自 Animal

    Rabbit内部,extends关键字添加了[[Prototype]]引用: 从 Rabbit.prototypeAnimal.prototype

    `extends`允许后接任何表达式(高级编程模式中用到)

    类语法不仅可以指定一个类,还可以指定extends之后的任何表达式

    ex.一个生成父类的函数调用

    function f(phrase{
        return class {
            sayHi() { alert(phrase) }
        }
    }

    class User extends f("Hello") {}

    new User().sayHi(); // Hello

    这里是 class User继承自f("Hello")的结果

    我们可以根据多种状况使用函数生成类,并继承它们,这对于高级编程模式来说可能很有用。

    super

    通常来说,我们不希望完全替换父类的方法,而是希望基于它做一些调整或者功能性的扩展。我们在我们的方法中做一些事情,但是在它之前/之后或在执行过程中调用父类方法。

    super关键字提供了上述功能

    1. 执行 super.method(…)调用父类方法; (借用并改造父类方法, 生成自己的方法)
    2. 执行super(…)调用父类构造函数(只能在子类的构造函数中运行) (继承父类属性)
    重写原型方法
    class Animal {

      constructor(name) {
        this.speed = 0;
        this.name = name;
      }

      run(speed) {
        this.speed += speed;
        alert(`${this.name} runs with speed ${this.speed}.`);
      }

      stop() {
        this.speed = 0;
        alert(`${this.name} stopped.`);
      }

    }

    class Rabbit extends Animal {
      hide() {
        alert(`${this.name} hides!`);
      }

      stop() { // (*)
        super.stop(); // 调用父类的 stop 函数
        this.hide();  // 然后隐藏
      }
    }

    let rabbit = new Rabbit("White Rabbit");

    rabbit.run(5); // White Rabbit runs with speed 5.
    rabbit.stop(); // White Rabbit stopped. White rabbit hides!

    箭头函数没有super

    如果箭头函数中,super被访问,那么则会从外部函数中获取(类似this)

    class Rabbit extends Animal {
        stop() {
            setTimtout(() => super.stop(), 1000); // 1 秒后调用父类 stop 方法
        }
    }

    因此,箭头函数中的superstop()中的是相同的,所以它能按预期工作。但如果我们在这里指定一个"普通"函数,那么将会抛出错误: (找不到super)

    class Rabbit extends Animal {
      stop() {
        setTimeout(function () super.stop() }, 1000); // Unexpected super
      }
    }

    代码解析会出错,报Uncaught SyntaxError: 'super' keyword unexpected here

    重写构造函数

    根据 规范,如果一个类继承了另一个类并且没有 constructor,那么将生成以下“空” constructor

    class Rabbit extends Animal {
        // 为没有构造函数的继承类生成以下的构造函数
        constructor(...args) {
            super(...args);
        }
    }

    可以看到,它调用了父类的constructor, 并传递了所有的参数。

    如果给继承类添加一个自定义的额构造函数

    class Animal {
      constructor(name) {
        this.speed = 0;
        this.name = name;
      }
      // ...
    }

    class Rabbit extends Animal {

      constructor(name, earLength) {
        this.speed = 0;
        this.name = name;
        this.earLength = earLength;
      }

      // ...
    }

    // 不生效!
    let rabbit = new Rabbit("White Rabbit"10); 

    报错: Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

    解释下就是: 继承类的构造函数必须调用 super(...), 并且一定要在this之前调用

    这是因为, 在JavaScript中,“继承类的构造函数" 与所有其他的构造函数之间存在区别。在继承类中,相应的构造函数会被标记为特殊的的内部属性[[ConstructorKind]]:"derived"

    不同点在于:

    • 当一个普通构造函数执行时,它会创建一个空对象作为this并继续执行。
    • 但是当继承的构造函数执行时,它并不会做这件事。它期望父类的构造函数来完成这项工作。

    因此,如果我们在继承类中构建了自己的构造函数,我们必须调用super,因为如果不这样的话this指向的对象不会被创建。并且会收到一个报错。

    正确的写法;需要在使用this之前调用super()

    class Rabbit extends Animal {
        constructor(name, earLength) {
            super(name);
            this.earLength = earLength;
        }
    }
    super内部探究: [[HomeObject]]

    当一个对象方法运行时,它会将当前对象作为this,如果调用super.method(),它需要从当前的原型中调用method

    super技术上的实现,首先会想到,引擎知道当前对象的this,因此它可以获取父method作为this.__proto__.method。但这个解决方法是行不通的。

    让我们来说明一下这个问题。没有类,为简单起见,使用普通对象。

    let animal = {
        name'Animal',
        eat() {
            alert(`${this.name} eats.`);
        }
    };

    let rabbit = {
        __proto__: animal,
        name'Rabbit',
        eat() {
            // 这是 super.eat() 可能运行的原因
            this.__proto__.eat.call(this); // (*)
        }
    };

    rabbit.eat(); // Rabbit eats

    在(*)这一行,我们从原型animal,我们从原型animal上获取eat方法,并在当前对象的上下文中调用它。注意, .call(this)在这里非常重要,因为简单的调用this.__proto__.eat()将在原型的上下文中执行eat,而非当前对象。

    上述代码中,我们获得了正确的父类方法。但如果在原型链上再添加一个额外的对象。这就不成立了

    let animal = {
        name'Animal',
        eat() {
            alert(`${this.name} eats`);
        }
    };

    let rabbit = {
        __proto__: animal,
        eat() {
            this.__proto__.eat.call(this); // (*)
        }
    };

    let longEar = {
        __proto__: rabbit,
        eat() {
            this.__proto__.eat.call(this); // (**)
        }
    };

    longEar.eat(); // Error: Maxium call stack size exceeded
    // InternalError: too much recursion

    代码无法运行;这是由于在()和(*)这两行中,this的值都是当前对象(longEar)。

    在()和(*)这两行中,this.__proto__的值是完全相同的: 都是rabbit。在这个无限循环中,它们都调用了rabbit.eat,而并没有在原型链上向上寻找方法。

    1. longEar.eat()中,(**)这一行调用rabbit.eat并且此时this=longEar

      // 在 longEar.eat() 中 this 指向 longEar
      this.__proto__.eat.call(this// (**)
      // 变成了
      longEar.__proto__.eat.call(this)
      // 即等同于
      rabbit.eat.call(this);
    2. 之后在rabbit.eat的(*)行中,我们希望将函数调用再原型链上向更高层传递,但是因为this=longEar,因此this.__proto__.eat又是rabbit.eat

      // 在 rabbit.eat() 中 this 依旧等于 longEar
      this.__proto__.eat.call(this// (*)
      // 变成了
      longEar.__proto__.eat.call(this)
      // 再次等同于
      rabbit.eat.call(this);
    3. …所以 rabbit.eat 不停地循环调用自己,因此它无法进一步地往原型链的更高层调用。

    因此,super无法单独使用this来解决

    [[HomeObject]]

    为了提供super的解决方法,javascript为函数额外添加了一个特殊的内部属性: [[HomeObjext]]

    当一个函数被定义为类或者对象方法时, 它的[[HomeObject]]属性就成为那个对象。

    然后super使用它来解析父类原型和它自己的方法。

    let animal = {
        name'Animal',
        eat() { // animal.eat.[[HomeObject]] == animal // (3)
            alert(`${this.name} eats.`);
        }
    };

    let rabbit = {
        __proto__: animal,
        name'Rabbit',
        eat() {
            super.eat(); // rabbit.eat.[[HomeObject]] == rabbit
            // rabbit.eat.[[HomeObject]].__proto__.eat.call(this); // (2)
        }
    };

    let longEar = {
        __proto__: rabbit,
        name'Lonet Ear',
        eat() { // longEar.eat.[[HomeObject]] == longEar
            super.eat();
            // longEar.eat.[[HomeObject]].__proto__.eat.call(this); // (1)
        }
    };

    // 正常运行
    longEar.eat(); // alert: Lonet Ear eats.

    上述代码按照预期运行,基于[[HomeObject]]运行机制。 像longEar.eat这样的方法,知道[[HomeObejct]],并且从它的原型中获取父类方法, 并没有使用 this。( 调用顺序(1) -> (2) -> (3) )

    方法并不是"自由"的

    通常函数都是"自由"的,并没有绑定到javascript中的对象。因此,它们可以在对象之间赋值,并且用另外一个this调用它。
    [[HomeObject]]的存在违反了这个原则,因为方法记住了它们的对象[[HomeObject]]不能被修改,所以这个绑定是永久的。

    在javascript语言中[[HomeObject]]仅被用于super所以,如果一个方法不使用super,那么仍然可以被视为自由且可在对象之间复制。但在super中可能出错。

    let animal = {
      sayHi() {
        console.log(`I'm an animal`);
      }
    };

    let rabbit = {
      __proto__: animal,
      sayHi() {
        super.sayHi();
      }
    };

    let plant = {
      sayHi() {
        console.log("I'm a plant");
      }
    };

    let tree = {
      __proto__: plant,
      sayHi: rabbit.sayHi // (*)
    };

    tree.sayHi();  // I'm an animal (?!?)

    原因很简单:

    • 在(*)行,tree.sayHi方法从rabbit复制而来。(可能是为了避免重复代码)
    • 所以它的[[HomeObject]]rabbit,因为它是在rabbit中创建的。无法修改[[HomeObject]]
    • tree.sayHi()内具有super.sayHi()。它从rabbit中上溯,然后从animal中获取方法。
    方法, 不是函数属性

    [[HomeObject]] 是为类和普通对象中的方法定义的。但是对于对象来说,方法必须确切指定为 method(),而不是 "method: function()"

    这个差别对我们来说可能不重要,但是对 JavaScript 来说却是非常重要的。

    下面的例子中,使用非方法(non-method)语句进行比较。[[HomeObject]] 属性未设置,并且继承不起作用:

    let animal = {
        eatfunction() // eat() {...}
            // ...
        }
    };

    let rabbit = {
        __proto__: animal,
        eatfunction() {
            super.eat();
        }
    }

    rabbit.eat(); // 错误调用 super(因为这里并没有 [[HomeObject]])

    总结

    1.扩展类: class Child extends Parent:

    • 这就意味着Child.prototype.proto将是Parent.prototype,所以方法被继承

    2.重写构造函数:

    • 在使用this之前,我们必须在Child构造函数中将父构造函数调用为super()。( super(…)用来初始化继承类构造函数里的 this值,相当于手动执行了 this = Reflect.construct(super.constructor, args, new.target))

    3.重写方法:

    • 我们可以在Child方法中使用super.method()来调用Parent方法;(通过方法的内部属性[[HomeObject]]实现往原型链的更高层调用)

    4.内部工作:

    • 方法在内部[[HomeObject]]属性中记住它们的类/对象。这就是super如何解析父类方法的。
    • 因此,将一个带有super的方法从一个对象复制到另一个对象是不安全的。

    补充:

    • 箭头函数没有自己的thissuper,所以它们能融入到就近的上下文,像透明似的。

    class Rabbitclass Rabbit extends Object的区别

    extends语法会设置两个原型: (结果就是,继承对于常规的和静态的方法都生效)

    1.在构造函数的prototype之间设置原型(为了获取实例方法)

    2.在构造函数之间会设置原型(为了获取静态方法)

    class Rabbit extends Object {}

    alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
    alert( Rabbit.__proto__ === Object ); // (2) true

    // 所以现在 Rabbit 对象可以通过 Rabbit 访问 Object 的静态方法,如下所示:
    class Rabbit extends Object {}

    // 通常我们调用 Object.getOwnPropertyNames
    alert ( Rabbit.getOwnPropertyNames({a1b2}) ); // a,b (*)

    但是如果我们没有声明 extends Object,那么 Rabbit.__proto__ 将不会被设置为 Object。

    class Rabbit {}

    alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
    alert( Rabbit.__proto__ === Object ); // (2) false (!)
    alert( Rabbit.__proto__ === Function.prototype ); // 所有函数都是默认如此

    // 报错,Rabbit 上没有对应的函数
    alert ( Rabbit.getOwnPropertyNames({a1b2})); // Error

    顺便说一下,Function.prototype 也有一些函数的通用方法,比如 callbind 等等。在上述的两种情况下他们都是可用的,因为对于内置的 Object 构造函数来说,Object.__proto__ === Function.prototype。(所有函数都是默认如此)

    因此class Rabbitclass Rabbit extends Object有两点区别

    class Rabbitclass Rabbit extends Object
    - needs to call super() in constructor
    Rabbit.__proto__ === Function.prototype Rabbit.__proto__ === Object
  • 相关阅读:
    1142
    dbms_monitor开启/关闭会话跟踪
    mysql密码过期问题
    zabbix监控mysql
    12C -- ORA-65048 ORA-65048
    idea的快捷键
    IntelliJ IDEA的配置优化
    IDEA环境设置
    Java 中int、String的类型转换
    js数组去重
  • 原文地址:https://www.cnblogs.com/rencoo/p/11879150.html
Copyright © 2011-2022 走看看