JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

  1. function Point(x, y) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. Point.prototype.toString = function () {
  6. return '(' + this.x + ', ' + this.y + ')';
  7. };
  8. var p = new Point(1, 2);

上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

ES6 的class可以看作只是一个语法糖

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。

  1. //定义类
  2. class Point {
  3. constructor(x, y) {
  4. this.x = x;
  5. this.y = y;
  6. }
  7. toString() {
  8. return '(' + this.x + ', ' + this.y + ')';
  9. }
  10. }

上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而**this**关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。

Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

ES6 的类,完全可以看作构造函数的另一种写法。

  1. class Point {
  2. // ...
  3. }
  4. typeof Point // "function"
  5. Point === Point.prototype.constructor // true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

  1. class Bar {
  2. doStuff() {
  3. console.log('stuff');
  4. }
  5. }
  6. var b = new Bar();
  7. b.doStuff() // "stuff"

类的方法定义在类的prototype属性上

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的**prototype**属性上面。

  1. class Point {
  2. constructor() {
  3. // ...
  4. }
  5. toString() {
  6. // ...
  7. }
  8. toValue() {
  9. // ...
  10. }
  11. }
  12. // 等同于
  13. Point.prototype = {
  14. constructor() {},
  15. toString() {},
  16. toValue() {},
  17. };

在类的实例上面调用方法,其实就是调用原型上的方法。

  1. class B {}
  2. let b = new B();
  3. b.constructor === B.prototype.constructor // true

上面代码中,bB类的实例,实例bconstructor方法就是B类原型的constructor方法。

Object.assign 向类添加多个方法

由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。**Object.assign**方法可以很方便地一次向类添加多个方法。

  1. class Point {
  2. constructor(){
  3. // ...
  4. }
  5. }
  6. Object.assign(Point.prototype, {
  7. toString(){},
  8. toValue(){}
  9. });

constructor 属性指向“类”的本身

**prototype**对象的**constructor**属性,直接指向“类”的本身,这与 ES5 的行为是一致的。

  1. Point.prototype.constructor === Point // true

内部所有定义的方法都是不可枚举的

另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

  1. class Point {
  2. constructor(x, y) {
  3. // ...
  4. }
  5. toString() {
  6. // ...
  7. }
  8. }
  9. Object.keys(Point.prototype)
  10. // []
  11. Object.getOwnPropertyNames(Point.prototype)
  12. // ["constructor","toString"]

上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。

类的属性名,可以采用表达式。

  1. let methodName = 'getArea';
  2. class Square {
  3. constructor(length) {
  4. // ...
  5. }
  6. [methodName]() {
  7. // ...
  8. }
  9. }

上面代码中,Square类的方法名getArea,是从表达式得到的。

严格模式

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

考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

定义 class 类

类声明和类表达式

在ES6标准中,JavaSCript 也支持了 class 这种创建对象的语法。

与函数类型相似,定义类也有两种主要方式:类声明和类表达式。这两种方式都使用 class 关键字加大括号:

  1. // 类声明
  2. class Person {}
  3. // 类表达式
  4. const Animal = class {};
  1. class Animal {
  2. // 构造方法
  3. constructor(name) {
  4. this.name = name
  5. }
  6. sleep() {
  7. return 'zzZZ~'
  8. }
  9. }
  10. let cat = new Animal('cat')
  11. let dog = new Animal('dog')
  12. console.log(cat.name) // cat
  13. console.log(dog.name) // dog
  14. console.log(cat.sleep === dog.sleep) // true

constructor 类的构造函数

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

一个类必定有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

  1. class Point {
  2. }
  3. // 等同于
  4. class Point {
  5. constructor() {}
  6. }

上面代码中,定义了一个空的类Point,JavaScript 引擎会自动为它添加一个空的constructor方法。

constructor方法默认返回实例对象(即**this**,完全可以指定返回另外一个对象。

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. new Foo() instanceof Foo
  7. // false

上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。

类必须使用new调用

类必须使用**new**调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用**new**也可以执行。

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. Foo()
  7. // TypeError: Class constructor Foo cannot be invoked without 'new'

属性

static 静态属性

先来看一下构造函数的静态属性

  1. function User(url) {
  2. this.url = url
  3. }
  4. const user = new User('https://123.com')
  5. // 给构造函数创建静态属性,会保留在原型中
  6. User.newUrl = 'https://baidu.com'
  7. console.dir(user)

打印结果

  1. User
  2. url: "https://123.com"
  3. [[Prototype]]: Object
  4. constructor: ƒ User(url)
  5. newUrl: "https://baidu.com"
  6. arguments: null
  7. caller: null
  8. length: 1
  9. name: "User"
  10. prototype: {constructor: ƒ}
  11. [[FunctionLocation]]: 2.html:14
  12. [[Prototype]]: ƒ ()
  13. [[Scopes]]: Scopes[2]
  14. [[Prototype]]: Object

那么在类中定义静态属性仅需要关键字 static 就可以实现了

  1. class User {
  2. static url = 'https://www.baidu.com'
  3. api() {
  4. return `${User.url}/sayName`
  5. }
  6. }
  7. const user = new User()
  8. console.log(user.api()) // https://www.baidu.com/sayName

如果一个属性是为所有实例对象共用的,不是为某一个对象来使用的,这时候就可以将其定义为静态属性,这样也会节省内存的占用,仅仅只会保存一份,定义到类里面

私有属性

正常的构造函数创建出来的对象都不是进行属性保护的,在外部都可以随意的进行修改,这些属性被称之为 非保护属性,例如下面

  1. class User {
  2. constructor(age) {
  3. this.age = age
  4. }
  5. }
  6. const user = new User(18)
  7. user.age = 12313
  8. console.log(user)
  9. // User {age: 12313}

通过在属性名前面加入 # 来设定私有属性进行更严格的保护,在外部是修改不了的

  1. class User {
  2. // 定义私有属性
  3. #url = 'www.baidu.com'
  4. }
  5. const user = new User()
  6. console.log(user)

方法

static 静态方法

在构造函数中定义静态方法

因为函数也是对象,所以构造函数可以通过下面放方式定义静态方法

  1. function User() {}
  2. User.sayName = function () {
  3. console.log('我是静态方法')
  4. }
  5. User.sayName('张三')

也可以定义到原型上

  1. function User() {}
  2. User.__proto__.sayName = function () {
  3. console.log('我是静态方法')
  4. }
  5. User.sayName('张三')
  6. console.dir(User)

那么里面的 this 指向的也是当前的对象

  1. function User() {}
  2. User.__proto__.sayName = function () {
  3. console.log(this === User) // true
  4. console.log(this === User.prototype.constructor) // true
  5. }
  6. User.sayName('张三')

在类中定义静态方法

  1. class User {
  2. sayName() {
  3. console.log('你好')
  4. }
  5. }
  6. User.__proto__.sayName = function () {
  7. console.log('我在原型上')
  8. }
  9. console.dir(User)

上面代码中,看似在类中定义了两个函数名一样的函数,可是这两个函数却是没有任何关系的,因为第一个在类中定义的函数,在 new 出来的对象中才可以使用,而后者是类的静态方法。

上述打印结果:

  1. class User
  2. length: 0
  3. name: "User"
  4. prototype:
  5. constructor: class User
  6. sayName: ƒ sayName()
  7. [[Prototype]]: Object
  8. arguments: (...)
  9. caller: (...)
  10. [[FunctionLocation]]: 1.html:13
  11. [[Prototype]]: ƒ ()
  12. [[Scopes]]: Scopes[2]

打印的 sayName 实际上是函数的静态方法,下面分别打印一下

  1. class User {
  2. sayName() {
  3. console.log('你好')
  4. }
  5. }
  6. User.__proto__.sayName = function () {
  7. console.log('我在原型上')
  8. }
  9. User.sayName() // 我在原型上
  10. const user = new User()
  11. user.sayName() // 你好

了解上述关系之后,那么就可以直接使用类的语法糖的形式进行定义了,需要通过关键字 static 来定义静态方法

  1. class User {
  2. sayName() {
  3. console.log('你好')
  4. }
  5. static sayName() {
  6. console.log('hello')
  7. }
  8. }
  9. User.sayName() // hello
  10. new User().sayName() // 你好

下面是通过调用静态方法创建出构造函数的例子

  1. class User {
  2. constructor(name, age) {
  3. this.name = name
  4. this.age = age
  5. }
  6. static create(...args) {
  7. // 这里是 this 指向的就是当前对象
  8. // 所以可以 new this 创建构造函数
  9. return new this(...args)
  10. }
  11. }
  12. // 通过调用静态方法创建出构造函数
  13. const user = User.create('张三', 19)
  14. console.log(user)

私有方法

同私有属性一样,使用 # 可以定义私有方法

  1. class User {
  2. #url = 'www.baidu.com'
  3. #sayName() {
  4. console.log('你好')
  5. }
  6. }
  7. const user = new User()
  8. user.#sayName()

直接调用私有属性的话会提示错误

  1. Uncaught SyntaxError: Private field '#sayName' must be declared in an enclosing class
  2. 必须在封闭类中声明私有字段 #sayName

私有属性必须是在类的内部调用才可以,例如下面,通过定义一个非私有的函数,让它去调用私有函数是可以正常工作的

  1. class User {
  2. #url = 'www.baidu.com'
  3. #sayName() {
  4. console.log('你好')
  5. }
  6. changeSayName() {
  7. this.#sayName()
  8. }
  9. }
  10. const user = new User()
  11. user.changeSayName() // 你好

访问器

在正常对象中,对象中的属性我们是可以随意设置和更改的,但是有些时候并不希望某些值被设置了不可控的值,比如:

  1. const user = {
  2. name: '张同学',
  3. age: 12
  4. }
  5. user.age = 19999
  6. console.log(user) // {name: '张同学', age: 19999}

如果需要加以限制,可以在对象中新建两个获取函数,分别使用 set 和 get 声明,那么每次获取和修改都会经过这里,来进行判断

  1. const user = {
  2. data: {
  3. name: '张同学',
  4. age: 12
  5. },
  6. set age(val) {
  7. if (typeof val !== 'number' || val < 1 || val > 100) {
  8. throw new Error('年龄格式错误')
  9. }
  10. this.data.age = val
  11. },
  12. get age() {
  13. return this.data.age
  14. }
  15. }

访问器的作用是限制用户对对象中的值进行随意的更改,在 class 中也可以通过关键字 set 和 get 针对修改和获取进行限制处理

  1. class User {
  2. constructor() {
  3. this._name = '李四'
  4. }
  5. // 通过 setName 函数来修改属性值
  6. set name(name) {
  7. // 限制逻辑
  8. if (typeof name !== 'string') {
  9. throw new Error('参数错误')
  10. }
  11. // 通过才可以进行修改
  12. this._name = name
  13. }
  14. }
  15. const user = new User()
  16. user.name = '张三'

或者定义一个对象来存储数据

  1. class User {
  2. constructor(age) {
  3. this.data = {
  4. age
  5. }
  6. }
  7. // 通过函数来修改属性值
  8. set name(name) {
  9. // 限制逻辑
  10. if (typeof name !== 'string') {
  11. throw new Error('参数错误')
  12. }
  13. // 通过才可以进行修改
  14. this.data.name = name
  15. }
  16. // 访问器返回用户所有的信息
  17. get name() {
  18. return `${this.data.name}今年${this.data.age}岁`
  19. }
  20. }
  21. const user = new User(18)
  22. user.name = '张三'
  23. console.log(user.name)

extends 继承

类的继承使用 extend 关键字进行继承。在类中调用父类的构造函数传递参数的写法就需要使用 super 关键字进行调用父类的构造函数

  1. class Animal {
  2. constructor(name) { // 构造方法
  3. this.name = name
  4. }
  5. sleep() {
  6. return 'zzZZ~'
  7. }
  8. }
  9. class Flyable extends Animal {
  10. constructor(name) {
  11. super(name) // 执行父类构造方法
  12. }
  13. fly() {
  14. return 'flying...'
  15. }
  16. }
  17. var brid = new Flyable('brid')
  18. console.log(brid.name) // bire
  19. console.log(brid.sleep()) // zzZZ~
  20. console.log(brid.fly()) // flying...

this类型

链式调用

类的成员方法可以直接返回一个 this,这样就可以很方便地实现链式调用。

  1. class StudyStep {
  2. step1() {
  3. console.log('listen')
  4. return this
  5. }
  6. step2() {
  7. console.log('write')
  8. return this
  9. }
  10. }
  11. const s = new StudyStep()
  12. s.step1().step2() // 链式调用