jannahuang / blog

MIT License
0 stars 0 forks source link

构造函数,class 类是什么 #15

Open jannahuang opened 2 years ago

jannahuang commented 2 years ago

构造函数

构造函数的主要目的是实现可重用的对象创建代码,能更方便地创建类似的对象。 构造函数在技术上是常规函数,从技术上讲,除了箭头函数(它没有 this)之外,任何函数都可以用作构造器。 所以我们用两个约定来规范构造函数的使用:

  1. 它们的命名以大写字母开头。
  2. 它们只能由 "new" 操作符来执行。

举例:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

当用 new 执行一个函数时,函数发生以下步骤:

  1. 创建一个空对象并分配给 this
  2. 执行函数体。一般会修改 this,为其添加新属性。
  3. 返回 this 举例,也就是当 new User() 时:

    function User(name) {
    // this = {};(隐式创建)
    
    // 添加属性到 this
    this.name = name;
    this.isAdmin = false;
    
    // return this;(隐式返回)
    }

Class 基本语法

ES6引入新的关键字class,它提供了一种更为优雅的创建对象和实现继承的方式,底层仍然是基于原型的实现。

class 语法

除了构造函数(构造器)之外,还有一个更高级的“类”构建方式。 在 JavaScript 中,类是一种函数。 类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。空的类定义照样有效。默认情况下,类定义中的代码都在严格模式下执行。 基本语法是:

class MyClass {
  prop = value; // 属性
  constructor(...) { // 构造器
    // ...
  }
  method(...) {} // method
  get something(...) {} // getter 方法
  set something(...) {} // setter 方法
  ...
  // 注意方法之间不加逗号  
}

定义上述的类之后,使用 new MyClass() 来创建具有上述列出的所有方法的新对象。 new 会自动调用 constructor() 方法,因此我们可以在 constructor() 中初始化对象。 举例:

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

// 用法:
let user = new User("John");
user.sayHi();

定义 class User {...} 时实际做了以下事情:

  1. 创建一个名为 User 的函数,该函数的代码来自于 constructor(类字段的属性也会加进来)。
  2. 将类中的方法添加到原型,例如 User.prototype.sayHi = function() {}。 当用 new User 创建对象之后,该对象调用方法时,会从 User.prototype 中获取。

class 是语法糖吗?

从上述定义 class 和 new class 的过程可见,可以将 class 视为一种定义构造器及其原型方法的语法糖。 但其实它们之间存在重大差异,比如:

  1. 通过 class 创建的函数具有特殊的内部属性标记 [[ IsClassConstructor]]: true。编程语言会在许多地方检查该属性。例如,与普通函数不同,必须使用 new 来调用 class。
  2. 类方法不可枚举。类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false。
  3. 类总是使用 use strict。 在类构造中的所有代码都将自动进入严格模式。

类继承

类继承是一个类扩展另一个类的一种方式。可以在现有功能之上创建新功能。 扩展另一个类的语法是:class Child extends Parent。 举例:

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} stands still.`);
  }
}
// 类 Rabbit 继承类 Animal
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

在内部,关键字 extends 使用了很好的旧的原型机制进行工作。它将 Rabbit.prototype.[[ Prototype]] 设置为 Animal.prototype。所以,如果在 Rabbit.prototype 中找不到一个方法,JavaScript 就会从 Animal.prototype 中获取该方法。 extends

另外,类语法不仅允许指定一个类,在 extends 后可以指定任意表达式。 例如,一个生成父类的函数调用:

function f(phrase) {
  return class {
    sayHi() { alert(phrase); }
  };
}
class User extends f("Hello") {}

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

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

重写父类方法

有时,我们不希望完全替换父类的方法,而是想在沿用父类方法的基础上,拓展其他功能。 Class 为此提供了 "super" 关键字。

class Rabbit extends Animal { hide() { alert(${this.name} hides!); } stop() { super.stop(); // 调用父类的 stop this.hide(); // 然后 hide } }

## 重写父类 constructor
根据规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor:
```javascript
class Rabbit extends Animal {
  // 为没有自己的 constructor 的扩展类生成
  constructor(...args) {
    super(...args);
  }
}

假如给 Rabbit 添加一个自定义的 constructor。除了 name 之外,它还会指定 earLength。 继承类的 constructor 必须调用 super(...),并且一定要在使用 this 之前调用

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

class Rabbit extends Animal {
  constructor(name, earLength) {
    super(name);
    this.earLength = earLength;
  }
  // ...
}

这是因为,在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [[ ConstructorKind]]:"derived"。这是一个特殊的内部标签。

该标签会影响它的 new 行为:

与普通构造函数一样,可以使用 instanceof 操作符检查构造函数原型是否存在于实例的原型链中:

class Animal {}
let a = new Animal();
console.log(a instanceof Animal); // true

由此可知,可以使用instanceof 操作符检查一个对象与类构造函数,以确定这个对象是不是类的实例。

类本身具有与普通构造函数一样的行为。在类的上下文中,类本身在使用new 调用时就会被当成构造函数。重点在于,类中定义的 constructor 方法不会被当成构造函数

class Animal {}
let a1 = new Animal()
let a2 = new Animal.constructor()

console.log(a1 instanceof Animal) // true
console.log(a2 instanceof Animal.constructor) // true

静态属性和静态方法

在一个类的声明中,以 static 关键字开头把一个方法作为一个整体赋值给类,这样的方法被称为静态的(static)。 注意:它不是某个实例对象的方法,而是整个 class 的方法。

class User {
  static staticMethod() {
    console.log(this === User);
  }
}
// 上述写法跟下述直接将方法作为属性赋值的写法作用相同
class User { }
User.staticMethod = function() {
  console.log(this === User);
};

私有的和受保护的属性和方法

受保护的属性 在类中,受保护的属性通常以下划线 _ 作为前缀。这是程序员之间有一个众所周知的约定,即不应该从外部访问此类型的属性和方法。

只读的属性 有时候一个属性必须只能被在创建时进行设置,之后不能再被修改。在类中只设置该属性的 getter,不设置 setter 即可。

扩展内建类

内建的类,例如 Array,Map 等也都是可以扩展的。 比如,从原生 Array 类继承一个类 PowerArray():

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let a = new Array(1, 2)
let b = new PowerArray(1, 3, 8, 10)
a.isEmpty() // 报错
b.isEmpty() // false
b.constructor === PowerArray // true
b.constructor === Array // false

let filterb = b.filter(item=>item >=20) // filterb 为子类 PowerArray 的新对象, PowerArray []
filterb.isEmpty() // true

使用 filter 等内建方法,返回的是子类 PowerArray 的新对象。 当 b.filter() 被调用时,它的内部使用的是 b.constructor 来创建新的结果数组,而不是使用原生的 Array。

如果希望使用原生的 Array,则可以使用 Symbol.species 返回 Array。species 访问器属性允许子类覆盖对象的默认构造函数。 以下是 MDN[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/species] 中关于 Symbol.species 的举例:

class MyArray extends Array {
  // 覆盖 species 到父级的 Array 构造函数上
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

instanceof

instanceof 操作符用于检查一个对象是否属于某个特定的 class。

obj instanceof Class

如果 obj 隶属于 Class 类(或 Class 类的衍生类),则返回 true。构造函数也可用。

class Rabbit {}
let rabbit = new Rabbit()

function Rabbit2() {}
let rabbit2 = new Rabbit2()

rabbit instanceof Rabbit // true
rabbit2 instanceof Rabbit2 // true

通常,instanceof 在检查中会将原型链考虑在内。

以上笔记参考《现代 JavaScript 教程》,《JavaScript高级程序设计(第4版)》及 MDN 文档