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
上面代码中,name
和age
都是实例对象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
上面代码中,person1
和person2
都是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
,子类B
的prototype
属性的__proto__
属性指向父类A
的prototype
属性。
这样的结果是因为,类的继承是按照下面的模式实现的。
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内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
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();
网友评论