你理解ES5的构造函数和ES6的Class类的关系吗

226 阅读10分钟

你理解ES5的构造函数和ES6的Class类吗

ES5的构造函数

1.构造函数创建对象

示例:

        function Person(name, age) {
            this.name = name;
            this.age = age;
            this.sayName = function() {
                console.log('my name:', this.name);
            }
        }
        Person.prototype.sayAge = function() {
            console.log('my age:', this.age);
        }
        const tom = new Person('Tom', 25);
        tom.sayName();
        tom.sayAge();
        console.log('tom', tom.name, tom.age);

我们定义了一个构造函数Person,通过Person实例了对象tom,并初始化两个属性name、age和一个方法sayName,并让tom继承原型上的sayAge方法。

image.png

2.构造函数实例化对象时的隐式操作

    function Person(name, age) {
      // 1.函数体最前端隐式创建空对象this={}
      this.name = name;
      // this = {
      //		name : name
      // }
      this.age = age;
      // this = {
      //	 name : name,
      //	 age : age
      // }
      // 2.将name属性添加进去
      return this;
      // 3.隐式抛出this
    }

当我们new Person时,会将this对象抛出,注释中的步骤都是在构造函数中隐式执行的;如果我们自己在这里加个return {},无论是在this上挂载多少属性都会返回一个空对象。

但是自己写return 123等无效,原始值和undefined以及null无属性方法,最后系统强制返回对象类型

    function Person(name, age, sex) {
      this.name = name;
      this.age = age;
      return {}
    }
    const person = new Person('小明', 18);
    console.log('person:', person);

正如前面所讲的,我们可以控制自定义构造函数反馈自己设计的对象(绝大多数情况没啥用)。

image.png

理解构造函数如何创建一个对象,接下来思考在构造函数中定义的sayName和原型上sayAge的方法,在实例化的对象调用时this为什么指向它自身?

3.构造函数中的方法和原型上的方法this指向

一个对象是由构造函数实例化的,该实例对象的原型就是构造函数的prototype属性所指向的原型对象。

当在构造函数中使用函数声明方法时,this 指向的是当前创建的实例对象。而在构造函数的原型中定义函数声明的方法,最终被实例化对象所继承,this 指向的是调用该方法的对象

所以在示示例中,调用sayNamesayAge方法中this所指向的都是调用者本身。

在解析中我们都提到了用函数声明的方法,那么尝试用箭头函数会怎么样?先了解下函数声明和箭头函数的区别。

4.函数声明和箭头函数调用时this指向

在 JavaScript 中,箭头函数和函数声明函数在各个环境调用时其内部 this 指向不同。具体而言:

  1. 函数声明函数的 this 指向与调用方式有关。
  • 当以函数形式调用时,this 指向全局对象(浏览器中为 window 对象)。
  • 当以方法调用方式调用时,this 指向调用该方法的对象。
  • 当使用 apply() 或 call() 方法调用时,this 指向 apply() 或 call() 的第一个参数所指定的对象。
  • 当使用 bind() 方法创建新函数并调用时,this 指向 bind() 的第一个参数所指定的对象。

例如:

function sayHello() {
  console.log('Hello, my name is ' + this.name);
}

let person = {
  name: 'John',
  sayHello: sayHello
};

person.sayHello(); // 输出:Hello, my name is John

let obj = { name: 'Tom' };
sayHello.call(obj); // 输出:Hello, my name is Tom

let boundFunc = sayHello.bind(person);
boundFunc(); // 输出:Hello, my name is John
  1. 箭头函数的 this 指向定义箭头函数时的上下文环境。

箭头函数没有自己的 this 值,会继承父级作用域的 this 值。箭头函数无法通过 call、apply、bind 方法来改变 this 指向。

例如:

let person = {
  name: 'John',
  sayHello: () => {
    console.log('Hello, my name is ' + this.name);
  }
};

person.sayHello(); // 输出:Hello, my name is undefined

let obj = { name: 'Tom' };
person.sayHello.call(obj); // 输出:Hello, my name is undefined

在这个例子中,箭头函数 sayHello 继承了定义时的上下文环境,即全局作用域,因此输出的结果为 undefined。

总之,在 JavaScript 中,函数声明函数和箭头函数在各个环境调用时其内部 this 指向不同。通常情况下,我们需要根据实际需求选择使用哪种方式来定义函数。

5.构造函数中和原型上的方法用箭头函数

    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.sayName = () => {
          console.log('my name:', this.name);
      }
    }
    Person.prototype.sayAge = () => {
      console.log('my age:', this.age);
    }
    const tom = new Person('Tom', 25);
    tom.sayName();
    tom.sayAge();

经过我们实践,发现构造函数中的箭头函数方法的仍然能够拿到实例化对象中的属性,而原型中的方法则不能。这里可以结合前面的内容进行思考。

image.png

因为在构造函数中定义的箭头函数方法,this指向继承构造函数作用域的this,此时this就是我们实例化抛出的对象。而原型上使用箭头函数,this指向继承了全局作用域。

6.构造函数总结

Class类

Class是 ES6 新增的一个语法糖,它可以让我们更方便地创建对象和继承。我们可以使用 class 关键字来定义一个类。

示例:

    class Person {
      constructor(name, age) { // 构造函数,在创建对象时被调用,用于初始化对象的状态
        this.name = name;
        this.age = age;
      }
      sayName() { // 类中的方法
        console.log('my name:' + this.name);
      }
    }
    let person = new Person('John', 30);
    person.sayName(); // my name:John

Class类中无论是构造函数还是自定义方法,都使用 this 关键字来引用Class类创建的对象。

1.类关键字

  • extends:继承一个类。
  • static:定义一个静态方法或静态属性。
  • super:调用父类构造函数。

extends

extends关键字用于在子类中引用其父类,并在子类的构造函数中调用父类的构造函数以初始化父类的属性(多与super合)

        class Person {
            constructor(name) {
                this.name = name;
            }
            sayHello() {
                console.log('hello');
            }
        }

        class Student extends Person {}

        const student = new Student();
        student.sayHello(); // hello

static

static关键字用于定义一个静态方法或静态属性。静态方法和静态属性不属于任何实例对象,而是属于类本身,可以通过类名直接调用,而不需要创建实例对象。

因为静态方法是属于类级别的,而不属于实例对象,它们只能访问和操作属于类本身的静态属性和静态方法。

        class Person {
            static type = 'human'; // 静态属性
            constructor(name, age) {
                this.name = name;
                this.age = age;
            }
            
            sayHello() {
                console.log(`Hello, I am ${this.name}, ${this.age} years old.`);
            }
            
            static introduce() { // 静态方法
                console.log('This is a person class.');
            }
        }

        const person = new Person('Tom', 25);
        person.sayHello(); // "Hello, I am Tom, 25 years old."

        Person.introduce(); // "This is a person class."
        console.log(Person.type); // "human"

super

super关键字,我们可以在子类中访问和调用父类的属性和方法,并将其作为自己的一部分。

        class Person {
            constructor(name) {
                this.name = name;
            }
            
            sayHello() {
                console.log(`Hello, I am ${this.name}`);
            }
        }

        class Student extends Person {
            constructor(name, age) {
                super(name); // 调用父类的构造函数初始化属性
                this.age = age;
            }
            
            study() {
                super.sayHello(); // 等效于 this.sayHello();
                console.log(`my age:${this.age}.`);
            }
        }

        const student = new Student('Tom', 18);
        student.sayHello(); // Hello, I am Tom
        student.study(); // Hello, I am Tom | my age:18

super关键字来调用父类的构造函数和方法,我们可以在子类中访问和重用父类的代码,并获得更大的灵活性和可扩展性。

2.访问修饰符

在 JavaScript 中,可以使用访问修饰符来限制属性和方法的访问权限。目前,JavaScript 支持三种访问修饰符:

  • public(默认):默认的访问修饰符,表示公开的属性或方法,可以在任何地方访问。
  • private:使用#作为前缀来声明一个私有的属性或方法,只能在类内部访问。
  • protected:使用_作为前缀来声明一个受保护的属性或方法,只能在类及其子类内部访问,无法在外部访问。

public

public 是默认的访问修饰符,表示该属性或方法是公共的,所有地方都可以访问。

    class Person {
      age = 21; // public age = 21 默认值,可在构造函数中修改,public一般直接在构造函数中声明
      constructor(name) {
        this.name = name; // public,构造函数也可以给默认参数
      }
      sayName() { // 类中的方法
        console.log('my name:' + this.name);
        console.log('my age:' + this.age);
      }
    }
    let person = new Person('Tom');
    person.sayName(); // Tom 21
    console.log('person:', person);

name、age属性和sayName方法都是公共的,所有地方都可以访问。 我们使用了一个未指定访问修饰符的属性name、age和方法sayName,因此它们默认为 public。

image.png

private

使用#作为前缀来声明一个私有的属性或方法,只能在类内部访问。

    class Person {
      #name; // private name,# 符号表示属性是私有的,需要在构造函数前用修饰符声明。
      constructor(name) {
        this.#name = name;
      }
      #sayName() { // private sayName,# 符号来表示方法是私有的。
        console.log('my name:' + this.#name);
      }
      sayHello() { // 构造函数内部方法调用私有方法
        this.#sayName();
      }
    }
    let person = new Person('Tom');
    person.sayHello(); // Tom
    person.#sayName(); // 报错,无法访问私有方法#sayName

在构造函数中和 sayName 方法中,我们使用了 this.#name 和 this.#sayName() 来访问私有属性和私有方法。

protected

使用_作为前缀来声明一个受保护的属性或方法,只能在类及其子类内部访问,无法在外部访问。

        class Person {
            _name; // protected name,_ 符号表示属性是受保护属性,需要在构造函数前用修饰符声明。
  
            constructor(name) {
                this._name = name;
            }
  
            _sayName() { // protected sayName,_ 符号表示属性是受保护方法
                console.log('my name:' + this._name);
            }
        }

        class Student extends Person {
            constructor(name, age) {
                super(name);
                this.age = age;
            }
        
            sayAge() {
                console.log('my age:' + this.age);
                this._sayName(); // 访问受保护方法
            }
        }

        const student = new Student('Lucy', 5);
        console.log(student._name); // 报错,无法访问受保护属性
        student._sayName(); // 报错,无法调用受保护方法
        student.sayAge(); // my age:5 | my name:Lucy

注意: 虽然JavaScript支持使用不同的访问修饰符来控制类成员的可见性和访问权限,但实际上这只是基于约定的机制,而不是强制执行的。在某些情况下,我们仍然可以通过一些技巧来访问或修改私有或受保护成员。因此,访问修饰符不能完全保证代码的安全性,我们还需要结合其他的安全性措施来确保代码的安全性和正确性。

构造函数和Class类对比

  • 构造函数可以直接调用,Class类无法直接调用
  • Class类创建的实例原型上自定义的方法不可被枚举
  • Class类中的代码全部在严格模式下执行
  • Class类创建的实例原型上的方法不可通过new关键字调用

手写new

new关键字用于创建一个对象实例,并调用构造函数来初始化该对象的属性和方法。实际上,我们也可以通过手动模拟new的过程来创建一个对象实例。

具体实现过程如下:

  • 创建一个空对象,作为新对象实例;
  • 将这个空对象的原型指向构造函数的prototype属性;
  • 执行构造函数,将空对象作为this上下文执行,并传递构造函数的参数;
  • 如果构造函数返回一个非空对象,则直接返回该对象,否则返回第一步创建的新对象实例。
        const copyNew = (constructor, ...args) => {
            // 创建一个空对象obj,作为新对象实例,obj的原型指向构造函数的prototype属性
            const obj = Object.create(constructor.prototype);
            // 将构造函数的this指向空对象obj
            const res = constructor.apply(obj, args);
            // 若构造函数返回非空对象,则返回该对象,否则直接返回obj即可。
            return (typeof result === 'object' && result !== null) ? res : obj;
        }
        // 构造函数
        function Person(name) {
            this.name = name;
        }
        Person.prototype.sayName = function() {
            console.log('sayName:', this.name);
        }
        const person = copyNew(Person, 'Tom');
        person.sayName();

Babel中Class类的转义

在实现上,ES6 的 class 实际上是基于原型(prototype)机制实现的,它本质上是使用函数来模拟类,并通过 prototype 和 constructor 等属性实现继承和成员访问等功能。

在Babel中是如何将Class转义成构造函数且附加class新的特性呢?

示例构造函数:

        class Example {
            constructor(name) {
                this.name = name;
            }
            // 原型方法
            sayName()  {
                console.log('my name:', this.name);
            }
            // 静态方法
            static description = () => {
                console.log('class Example:');
            }
        }

Babel转义:

  • 思考如何判断当前class是直接调用或者通过new关键字执行(可参考上述手写new)
        "use strict" // 严格模式下执行
        
        // 检查class是直接调用还是new执行
        function _classCallCheck(instance, Constructor) {
            // 通过实例和构造函数的关系:new会将实例的原型指向构造函数的原型,直接调用不会
            if (!(instance instanceof Constructor)) {
                throw new TypeError('Cannot call a class as a function');
            }
        }
        
        // 对原型和静态方法做特殊处理,添加其不可被特性
        //(原型不可被枚举,静态方法只有class可访问)
        function _defineProperties(target, props) {
            // 遍历原型方法和静态方法,
            for (let i = 0;i < props.length;i ++) {
                let descriptor = props[i];
                descriptor.enumerable = descriptor.enumerable || false;
                descriptor.configurable = true;
                if ('value' in descriptor) {
                    descriptor.writable = true;
                }
                Object.defineProperty(target, descriptor.key, descriptor);
            }
        }
        
        // 调用特殊处理方法
        function _createClass(Constructor, protoProps, staticProps) {
            if (protoProps) {
                _defineProperties(Constructor.prototype, protoProps);
            }
            if (staticProps) {
                _defineProperties(Constructor, staticProps);
            }
            return Constructor;
        }
        
        
        const Example = /*#__PURE__*/function () {
            function Example(name) {
                // 1.检查如何调用
                _classCallCheck(this, Example);
                this.name = name;
            }
            _createClass(Example, [{
                key: 'sayName',
                value: function sayName() { // 原型方法
                    console.log('my name:', this.name);
                }
            }, {
                key: 'description',
                value: function description() { // 静态方法
                    console.log('class Example');
                }
            }]);
            return Example;
        }();
        const example = new Computer('Tom');