2021-06-19 ES6 Class关键字的使用

2021-06-19  本文已影响0人  走花鹿

ES6 Class关键字的使用

此文总结和参考自链接

概述

JavaScript语言的传统方法是通过构造函数,定义并生成新对象

function People (name, age) {
 this.name = name
 this.age = age
}
People.prototype.speak = function () {
 console.log('讲话')
}
const person1 = new People('走花鹿', 26)
person1.speak() //讲话

而现在ES6提供了一种更加接近传统编程语言的写法,引入了class这个概念,通过class关键字,我们可以定义类。class关键字让对象原型的写法更加清晰,更像面向对象编程的语法。

上面的构造函数的写法可以写成这样:

class People {
 constructor (name, age) {
 this.name = name
 this.age = age
 }

 speak() {
 console.log('讲话')
 }
}

var person1 = new People('走花鹿', 26)
person1.speak() //讲话

constructor方法就是构造方法this关键字指的就是实例对象(此处就是person1)。

在People类中还自定义了一个speak方法,此处为固定写法,声明方法不需要使用function关键字,方法之间也不需要用逗号隔开。

ES6的类可以看作构造函数的另外一种写法

class People {
 constructor (name, age) {
 this.name = name
 this.age = age
 }

 speak() {
 console.log('讲话')
 }
}

People === People.prototype.constructor //true

typeof People  //'function'

可以看出People类的数据类型是function,类的本身指向构造函数。

构造函数有prototype属性,该属性上定义的方法都可以被实例化对象拿到,class关键字也有prototype属性,事实上,类的方法都是定义在prototype属性上的。

class People {
 constructor (name, age) {
 this.name = name
 this.age = age
 }

 speak() {
 console.log('讲话')
 }

 laugh() {
 console.log('哈哈')
 }
}

//等同于
People.prototype.speak = function() {
 console.log('讲话')
}

People.prototype.laugh = function() {
 console.log('哈哈')
}

类的实例调用方法就是调用的原型上面的方法

const person2 = new People('走花鹿', 26)
person2.constructor === People.prototype.constructor //true
person2.speak === People.prototype.speak //true

上面的person2是People的实例化对象,它的constructor方法就是继承自People原型对象(即prototype)上的方法

Object.assign方法可以实现一次向类添加多个方法

class People {
 constructor(){
 //...
 }
}
//Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
Object.assign(People.prototype,{
 speak(){},
 laugh(){}
})

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

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

class类的内部所有的方法都是不可枚举的:

class People {
 constructor (name, age) {
 this.name = name
 this.age = age
 }

 speak() {
 console.log('讲话')
 }

 laugh() {
 console.log('哈哈')
 }
}
//Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
Object.keys(People.prototype) //[]
//Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
Object.getOwnPropertyNames(People.prototype) //['constructor','speak','laugh']

这一点与ES5是不一样的

function People (name, age) {
    this.name = name
    this.age = age
}
People.prototype.speak = function () {
    console.log('讲话')
}

Object.keys(People.prototype) // ['speak']
Object.getOwnPropertyNames(People.prototype)  //['constructor','speak']

constructor方法

constructor方法是类的默认方法,通过new关键字生成实例对象的时候会自动调用该方法,一个类必须要有constructor方法,如果没有显示定义,一个空的constructor方法会默认添加

constructor方法默认返回实例对象(即this),也可以自定义指定返回其它对象

class Foo {
  constructor() {
    //Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
    return Object.create(null); //表示this.__proto__ = null
  }
}
//instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
new Foo() instanceof Foo  //this__proto__ != Foo.prototype
// false

使用class声明的类,必须要用new关键字调用,否则会报错。普通构造函数可以直接调用

class Foo {
  constructor() {
    return Object.create(null);
  }
}

Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'

类的实例对象

生成类的实例对象需要new关键字调用,否则会报错

var person1 = new People('走花鹿',26) //正确
var person1 = People('走花鹿',26) //错误

与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

//定义类
class People {

  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log('说话')
  }

}

var person = new People(2, 3);

person.speak() // '说话'
//hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
//这个方法只会在自身上找,不会去原型对象上找
person.hasOwnProperty('name') // true
person.hasOwnProperty('age') // true
person.hasOwnProperty('speak') // false
person.__proto__.hasOwnProperty('speak') // true

上面代码中,nameage都是实例对象person自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而speak是原型对象的属性(因为定义在People类上),所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。

类共享一个原型对象

var person1 = new People('走花鹿',26)
var person2 = new People('大熊猫',28)
person1.__proto__ === person2.__proto__ === People.prototype   //true

上面代码中,person1person2都是People的实例,他们的原型都是People.prototype

这也就表示可以通过实例的__proto__属性为Class类添加方法

var person1 = new People('走花鹿',26)
var person2 = new People('大熊猫',28)
person1.__proto__.laugh = function(){
    return '哈哈'
}
person1.laugh() //'哈哈'
person2.laugh() //'哈哈'

var person3 = new People('老鼠',27)
person3.laugh() //'哈哈'

我们在实例对象person1上的原型上添加了一个laugh方法,因为person1和person2有同一个原型(即People.prototype),所以person2也可以调用laugh方法。

class关键字不存在变量提升

new People() 
class People{
    // ...
}
// 会报错

ES6不会把类的声明提升到代码的头部

this的指向

类的方法内部如果出现this关键字,this默认指向类的实例

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}

另一种解决方法是使用箭头函数。

class Logger {
  constructor() {
    this.printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    };
  }

  // ...
}

class的继承

基本用法

class类之间可以用extends关键字实现继承

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this

如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

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

另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}

上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

类的prototype性和__proto__属性

大多数浏览器的ES5实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代码中,子类B__proto__属性指向父类A,子类Bprototype属性的__proto__属性指向父类Aprototype属性。

这样的结果是因为,类的继承是按照下面的模式实现的。

class A {
}

class B {
}
// Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]也就是__proto__属性)到另一个对象或  null。
// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B继承A的静态属性
Object.setPrototypeOf(B, A);

《对象的扩展》一章给出过Object.setPrototypeOf方法的实现。

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point
// true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

super关键字

super关键字,既可以当作函数使用,也可以当作对象使用

第一种情况:super作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)

第二种情况:super作为对象的时候,指向父类的原型对象

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super指向A.prototype,所以super.p()就相当于A.prototype.p()

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

class的取值函数(getter)和存值函数(setter)

与ES5一样,在Class内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。

存值函数和取值函数是设置在属性的descriptor对象上的。

class CustomHTMLElement {
 constructor(element) {
 this.element = element;
 }

 get html() {
 return this.element.innerHTML;
 }

 set html(value) {
 this.element.innerHTML = value;
 }
}
// Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
var descriptor = Object.getOwnPropertyDescriptor(
 CustomHTMLElement.prototype, "html");
"get" in descriptor  // true
"set" in descriptor  // true

上面代码中,存值函数和取值函数是定义在html属性的描述对象上面,这与ES5完全一致。

class的静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例对象继承。如果在一个方法前加上static关键字,就表示该方法只能通过类来调用,该方法不会被实例对象继承,就这是静态方法

classclass Foo {
 static classMethod() {
 return 'hello';
 }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

父类的静态方法,可以被子类继承。

class Foo {
 static classMethod() {
 return 'hello';
 }
}

class Bar extends Foo {
}

Bar.classMethod(); // 'hello'

上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

静态方法也是可以从super对象上调用的。

class Foo {
 static classMethod() {
 return 'hello';
 }
}

class Bar extends Foo {
 static classMethod() {
 return super.classMethod() + ', too';
 }
}

Bar.classMethod();

完~

上一篇 下一篇

猜你喜欢

热点阅读