JavaScript calss语法糖

JavaScript calss语法糖

基础知识

  严格意义上来讲,在Js中是没有类这一概念的。

  我们可以运用前面章节提到的构造函数来模拟出类这一概念,并且可以通过原型对象的继承来完美的实现实例对象方法复用。

  但是这样十分的麻烦,我们需要将实例对象需要用到的公共方法来存放到构造函数的原型对象中,而使用class语法糖整个过程就变得非常简单。

 

声明定义


  使用class来定义类,类中定义的函数称为方法,不需要用关键字function也不需要用逗号进行分割。

 

<script>"use strict";
​
    class User {
​
        f1() {
            console.log("运行了f1...");
        }
        // 不需要逗号分隔
        f2() {
            console.log("运行了f2...");
        }
​
    }
​
    let u1 = new User();
​
    u1.f1();  // 方法调用
    u1.f2();  // 方法调用
</script>

 

构造函数


  使用 constructor 构造函数传递参数,该函数会在new时自动执行。

 

<script>"use strict";
​
    class User {
​
        constructor(name, age, gender) {
            this.name = name;
            this.age = age;
            this.gender = gender;
        }
​
        show(){
            console.log(`姓名:${this.name},年龄:${this.age},性别:${this.gender}`);
        }
​
    }
​
    let u1 = new User("云崖",18,"男");
​
    u1.show();  // 执行方法
</script>

 

  构造函数不是必须定义的,当没有定义构造函数时。

  它会自动去查找原型链,相当于如下代码所示。

constructor(...args) {
  super(...args);
}

 

原理分析


  不管是使用构造函数还是class语法糖,其原理都是一样的。

  但是构造函数中的方法应该存放进原型对象,这一步需要我们手动去做,使用class语法糖的结构后则会自动将方法放入原型对象。

 

<script>"use strict";
​
    class U1 {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log("U1 show");
        }
    }
    
    console.log(typeof U1);  // function  class只是语法糖,内部还是函数。
</script>

 

<script>"use strict";
​
    class U1 {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log("U1 show");
        }
    }
​
    let u1 = new U1("class语法糖");
​
    console.dir(u1);
​
    // ============  两种操作一模一样 class自动将方法写入原型对象中
function U2(name) {
        this.name = name;
    }
​
    U2.prototype.show = function () {
        console.log("U2 show");
    }
​
    let u2 = new U2("构造函数");
​
    console.dir(u2);
​
</script>

 

image-20200806104822239

 

遍历差异


  虽说class定义的类归根结底还是函数,但是与我们手动创建构造函数还是有一些优化措施的。

  class 中定义的方法不能被遍历出来,而构造函数原型对象中的方法却可以被遍历出来。

 

<script>"use strict";
​
    class U1 {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log("U1 show");
        }
    }
​
    let u1 = new U1("class语法糖");
​
    for (let key in u1) {
        console.log(key);  // name  Ps:show不会被遍历出来
    }
​
​
    // ============  两种操作一模一样 class自动将方法写入原型对象中
function U2(name) {
        this.name = name;
    }
​
    U2.prototype.show = function () {
        console.log("U2 show");
    }
​
    let u2 = new U2("构造函数");
​
    for (let key in u2) {
        console.log(key);  // name  show  
    }
​
​
</script>

 

严格模式


  class中, 默认使用strict 严格模式执行

 

<script>// "use strict";  取消严格模式
​
    class U1 {
        constructor(name) {
            this.name = name;
        }
        show() {
​
            (function () {
                console.log(this);  // class中用严格模式执行代码,所以this指向为undefined
            }())
​
        }
    }
​
    let u1 = new U1("class语法糖");
​
    u1.show();
​
​
    // ============  两种操作一模一样 class自动将方法写入原型对象中
function U2(name) {
        this.name = name;
    }
​
    U2.prototype.show = function () {
​
        (function () {
            console.log(this);  // 构造函数中方法中的函数this非严格模式下指向为window
        }())
​
    }
​
    let u2 = new U2("构造函数");
​
    u2.show();
​
</script>

 

静态访问

静态属性


  静态属性即为类自己单独设置属性,而不是为生成的对象设置,请使用static进行声明。

<script>"use strict";
​
    class User {
​
        static username = "用户类";  // 如果不加 static,将会变成实例属性
​
        constructor(username) {
​
            this.username = username;
        }
​
    }
​
    let u1 = new User("云崖");
​
​
    console.log(u1.username);  // 云崖
    console.log(User.username);  // 用户类
</script>

 

  实现原理也非常简单。

<script>

    "use strict";


    function User(username) {

    
        this.username = username;

    }

    User.username = "用户类";

    let u1 = new User("云崖");


    console.log(u1.username);  // 云崖
    console.log(User.username);  // 用户类


</script>

 

静态方法


  静态方法即为类自己单独设置方法,而不是为生成的对象设置,请使用static进行声明。

 

<script>

    "use strict";

    class User {

        static show(){
            console.log("类的方法...");
        }

    }

    let u1 = new User();

    User.show(); 

    u1.show();  // 抛出异常

</script>

 

  实现原理也非常简单。

<script>

    "use strict";

    function User() { }  // 构造函数

    User.show = function () { console.log( "类的方法..."); };

    let u1 = new User();

    User.show();

    u1.show();  // 抛出异常

</script>

 

访问器

  使用访问器可以对对象的属性进行访问控制,下面是使用访问器对私有属性进行管理。

 

语法介绍


 

  使用访问器可以管控属性,有效的防止属性随意修改

  访问器就是在函数前加上 get/set修饰,操作属性时不需要加函数的扩号,直接用函数名

 

<script>

    "use strict";

    class User {

        constructor(username){
            this.username = username;
        }

        get name(){  // 访问name时返回username

            return this.username;
        }

        set name(value){  // 设置name其实是给username做设置
            this.username = value;
        }

    }

    let u1 = new User("云崖");
  
    console.log(u1.name);  

</script>

 

属性保护


  当外部尝试修改某一属性时,可以使用访问器来进行验证。

 

<script>

    "use strict";

    class User {

        constructor(username){
            this.username = username;
        }

        get name(){  // 访问name时返回username

            return this.username;
        }

        set name(value){  // 设置name其实是给username做设置

            if(typeof value == String){
                this.username = value;
            }
            
            throw Error("value type error,must string");
        }

    }

    let u1 = new User("云崖");

    u1.name = 18;  // value type error,must string

    console.log(u1.name);  

</script>

 

私有封装

  私有封装是指内部可以任意调用,外部只能通过访问的接口才能进行调用。

 

public


  public 指不受保护的属性,在类的内部与外部都可以访问到

 

<script>

    "use strict";

    class User {

        constructor(username){
            this.username = username;
        }

        show(){
            return this.username; // 内部可以访问
        }

    }

    let u1 = new User("云崖");
  
    console.log(u1.username); // 外部也可以访问

    console.log(u1.show());  

</script>

 

protected


  protected是受保护的属性修释,不允许外部直接操作,但可以继承后在类内部访问,下面将介绍protected三种封装方法。

 

命名保护

  将属性定义为以 _ 开始,来告诉使用者这是一个私有属性,请不要在外部使用。

 

  外部修改私有属性时可以使用访问器 setter 操作

  但这只是提示,就像吸烟时烟盒上的吸烟有害健康,但还是可以抽

 

<script>

    "use strict";

    class User {

        constructor(username){
            this._username = username;
        }

        show(){
            return this._username; // 内部可以访问
        }

    }

    let u1 = new User("云崖");
  
    // console.log(u1._username); // 外部也可以访问,但是如果你是专业人员看到 _开头就知道不该在外部拿他。

    console.log(u1.show());  

</script>

 

Symbol

  由于我们的代码都是在一个模块中进行封装的,所以使用Symbol()来进行私有封装非常方便。

  除非使用者打开源代码找到变量名key,否则他只能通过提供的接口来拿到数据。

 

<script>

    "use strict";

    let key = Symbol();

    class User {

        constructor(username){
            this[key] ={username};
        }

        show(){
            return this[key].username; // 内部可以访问
        }

    }

    let u1 = new User("云崖");
  
    console.log(u1);  //  { Symbol(): {username: "云崖"}} 外部拿不到。只能通过接口来拿
    console.log(u1.show());  

</script>

 

WeakMap

  WeakMap是一组键/值对的集,下面利用WeakMap类型特性定义私有属性

 

<script>

    "use strict";

    let key = new WeakMap();

    class User {

        constructor(username) {
            key.set(this, {  // 以当前对象作为键来存储。
                username,
            })
        }

        show() {
            return key.get(this).username; // 内部可以访问
        }

    }

    let u1 = new User("云崖");

    console.log(u1);  //  {}  干干净净,啥都拿不到。
    console.log(u1.show());  // 云崖

</script>

 

private


  private 指绝对私有属性,只在当前类可以访问到,并且不允许继承的子类使用

 

  为属性或方法名前加 # 为声明为私有属性

  私有属性只能在声明的类中使用

 

<script>

    "use strict";



    class User {

        #username;  // 对于实例的私有属性来说,必须先定义一下。方法则不用。

        constructor(username) {
            this.#check(username);
            this.#username = username;
        }

        show() {
            return this.#username; // 内部可以访问
        }

        #check = (username) => {  // 验证类的私有方法。 私有方法格式必须如此。
            if (typeof username != "string") {
                throw Error("type error,value type must string.")
            }
        }

    }

    let u1 = new User("云崖");
    
    console.log(u1);  // {#username: "云崖", #check: ƒ}

    console.log(u1.show());   // 只能通过接口访问

    // console.log(u1.#check);  // 外部不能访问,抛出异常。

    // console.log(u1.#username); // 外部不可以访问,抛出异常 

</script>

 

继承特性

  class内部也是采用原型继承,这与构造函数如出一辙。

  如果你不了解原型继承,那么可以看一下我前面的一篇文章,解释的非常清楚了。

  这里就是介绍一些class语法糖如何使用继承。

 

extends


  以下示例将展示使用extends语法实现原型继承。

 

  需要注意的是,只要继承的子类中有构造函数,就一定要使用super方法。

 

  没使用super引发异常

<script>

    "use strict";

    class A {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log(`${this.name}运行A类的show...`);
        }
    }

    class B extends A {  // 继承A的原型对象
        constructor(name) {

            this.name = name; 

            // Uncaught ReferenceError: 
            // Must call super constructor in derived 
            // class before accessing 'this' or returning from derived constructor
            
            // 意思是必须用super

        }
    }

    let b1 = new B("实例b1");

    b1.show();

</script>

 

  正确示范

<script>

    "use strict";

    class A {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log(`${this.name}运行A类的show...`);
        }
    }

    class B extends A {  // 继承A的原型对象
        constructor(name) {

            super(name);  // 必须使用super,这里用父类的构造函数进行构造。

        }
    }

    let b1 = new B("实例b1");

    b1.show();  // 实例b1运行A类的show...

</script>

 

  原理如下

<script>

    "use strict";


    function A(name) {
        this.name = name;
    }

    A.prototype = {

        constructor: A,

        show() {
            console.log(`${this.name}运行A类的show...`);
        },
    };

    function B(name){
        A.call(this,name);
    }

    Object.setPrototypeOf(B.prototype,A.prototype); // B原型对象继承与A原型对象

    let b1 = new B("实例b1");

    b1.show();  // 实例b1运行A类的show...

</script>

 

super


  表示从当前原型中执行方法。

 

  super 一直指向当前对象

  在构造函数中,super一定要放在this声明的前面

  super 只能在类或对象的方法中使用,而不能在函数中使用,换而言之,不要使用function来表示这是一个函数!

 

 

  正常调用构造函数示范

<script>

    "use strict";

    class User {

        constructor(name) {
            this.name = name;
        }
        
        show() {
            console.log(`姓名:${this.name},年龄:${this.age},性别:${this.gender}`); // this指向始终为当前对象
        }
    }

    class Admin extends User {  // 继承User的原型对象

        constructor(name,age,gender) {

            super(name);  // 使用父类的构造函数进行构造name,其他参数由自身构造。必须放在this上面

            this.age = age; 
            this.gender = gender; 

        }

        func() {
            super.show(); // 使用super调用的同时会将当前对象this进行传递。
        }
    }

    let a1 = new Admin("云崖",18,"男");

    a1.func();  // 姓名:云崖,年龄:18,性别:男

</script>

 

  super调用其他方法的示范

<script>

    "use strict";

    let obj_1 = {
        show(){
            console.log("执行了show..."); 
        },
    };

    let obj_2 = {

        __proto__:obj_1,

        run(){
        
            super.show();  // 正常执行
           
        },

    };

    obj_2.run(); // 执行了show...

</script>

 

  错误示范,不能用function进行声明

<script>

    "use strict";

    let obj_1 = {
        show() {
            console.log("执行了show...");
        },
    };

    let obj_2 = {

        __proto__: obj_1,

        run = function () {

            super.show();  // 'super' keyword unexpected here
        
        },

    };

    obj_2.run(); // 异常,super不能在function中执行

</script>

 

静态继承


  类自身的属性以及方法都能被继承下来。

 

<script>

    "use strict";

    class A {

        static description = "类A";

        static show(){
            return ("类A的方法");
        }

    }

    class B extends A {  // 继承A的原型对象

    }

    console.log(B.description);  // 类A
    console.log(B.show());  // 类A的方法

</script>

 

方法覆写


  当父类原型对象中有一个方法与子类原型对象中方法同名,子类的实例对象将调用子类的原型对象中的方法。

  这是由原型链属性顺序查找引起的。

 

<script>

    "use strict";

    class A {

        show() {
            console.log("A-->show");
        }

    }

    class B extends A {  // 继承A的原型对象

        show() {
            console.log("B-->show");
        }

    }

    let b1 = new B();

    b1.show()  // B-->show

</script>

 

MixIn机制


  由于Js不支持多继承,所以想添加功能必须在某一个原型对象上不断的增加功能,这势必会让其本来的原型显得混乱不堪。

  这种时候就可以使用Mixin机制来实现。

 

  注意:Minin类应该当做工具箱来使用,而不应该作为其他类的父类被继承下来。

 

img

 

image-20200805160602907

 

 

<script>

    "use strict";

    class Vehicle {
        // 交通工具类
        constructor(name) {
            this.name = name;
        }

        whistle() {
            console.log(`${this.name}在鸣笛`);  // 公用方法放父类中
        }
    }


    class Aircraft extends Vehicle {
        constructor(name) {
            super(name);
        }
    }

    class Car extends Vehicle {
        constructor(name) {
            super(name);
        }
    }


    let Flyable_Mixin = {
        // 飞行器的功能
        fly() {
            console.log(`${this.name}在飞`);
        },
        outer() {
            console.log("其他功能...");
        },
    };

    Object.assign(Aircraft.prototype, Flyable_Mixin); //给飞机添加上飞机Mixin的功能


    let Car_Mixin = {
        // 汽车的功能

        reversing() {
            console.log(`${this.name}正在倒车入库`);
        },
        outer() {
            console.log("其他功能...");
        },
    };


    Object.assign(Car.prototype, Car_Mixin); //给汽车添加汽车Mixin的功能
    

    let c1 = new Car("法拉利");
    let a1 = new Aircraft("波音747");


    c1.whistle();  //  法拉利在鸣笛
    c1.reversing();  // 法拉利正在倒车入库

    a1.whistle();  // 波音747在鸣笛
    a1.fly();  // 波音747在飞

</script>

代码示例

 

原型检测

检测方法


  使用instanceof检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

  使用isPrototypeOf检测一个对象是否是另一个对象的原型链中

 

<script>

    "use strict";

    class User {

    } 


    let u1 = new User();

    console.log(u1 instanceof User);  // true  u1的原型链中包含User的原型对象吗?

    console.log(User.prototype.isPrototypeOf(u1)); // true User的原型对象在u1的原型链中吗?

</script>

 

instanceof原理


  递归不断向上检测原型对象。

 

    function checkPrototype(obj, constructor) {
        if (!obj.__proto__) return false;
        if (obj.__proto__ == constructor.prototype) return true;
        return checkPrototype(obj.__proto__, constructor);
    }