为了解决ES5 中原型链继承实现给我们造成的麻烦,ES6 又给我们提供了一颗语法糖:Class。
本文将通过以下几个关键字:class、constructor、static、extends、super 来具体了解一下 Class。
一、class
class,顾名思义,就是“类”。在ES6 之前并没有像java、C#等语言有具体的“类”的概念,这对于面向对象开发是一件很难受的体验。于是乎,ES6 为了减少 JavaScript 开发的痛苦,就提供了这么一个让对象原型写法更加清晰的语法糖:Class。
让我们对比一下传统构造函数写法,来看 class 关键字写法的优势:
// 传统写法
function Animal(type, name) {
this.type = type;
this.name = name;
}
Animal.prototype.toString = function () {
return '(' + this.type + ',' + this.name + ')';
};
var m = new Animal('monkey', 'yuan');
// class 写法
class Animal {
constructor (type, name) {
this.type = type;
this.name = name;
}
toString() {
return '(' + this.type + ',' + this.name + ')';
}
}
var m = new Animal('monkey', 'yuan');
m.toString(); // (monkey,yuan)
1、通过 class 关键字可以定义类,提供了更接近传统语言的写法,引入了 Class (类)这个概念作为对象的模板。
类的所有方法都是定义在类的 prototype 属性上。
class Animal {
constructor() { ... };
toString() { ... };
getName() { ... };
}
// 等价于
Animal.prototype = {
constructor() {},
toString() {},
getName() {}
}
2、在类的实例上调用方法,其实就是调用原型上的方法。
class A {};
let a = new b();
a.constructor === a.prototype.constructor; // true
3、由于类的方法(除 constructor 之外)都定义在 prototype 对象上,所以类的新方法可以添加在 prototype 对象上。Object.assgn() 方法可以很方便的一次向类添加多个方法。
class Animal {
constructor () { ... };
}
Object.assign(Animal.prototype, {
toString() { ... },
getName() { ... }
});
4、类的内部定义的所有方法都是不可枚举。
5、类的调用必须要使用 new 命令,否则会报错。
6、Class 表达式
与函数一样,Class 也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
7、采用 Class 表达式,可以写出立即执行的 class。
let animal = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("monkey");
animal.sayName(); // monkey
8、与 ES5 不同,类不存在变量提升
new Foo(); // ReferenceError
class Foo {};
9、this 的指向
类的方法内部如果含有 this,它将默认指向类的实例,如果将该方法提出出来单独使用,this 指向的是该方法运行时所在的环境,找不到该方法,会报错:
class Animal {
printName (name = "monkey") {
this.print(`Hello ${name}`);
}
print(name) {
console.log(name);
}
}
const animal = new Animal();
const { printName } = animal;
printName(); // 报错
如何解决类方法中的 this 指向问题呢?
方法一:在构造方法中绑定 this:
class Animal {
constructor () {
this.printName = this.printName.bind(this);
}
// ...
}
方法二:使用箭头函数:
class Animal {
constructor () {
this.printName = ( name = "monkey" ) => {
this.print(`Hello ${ name }`);
};
}
// ....
}
方法三:使用 Proxy,在获取方法的时候自动绑定 this:
function selfish (target) {
const cache = new WeakMap();
const handle = {
get (target, key) {
const value = Reflect.get(target, key) {
if (typeof value !== 'function') return value;
if (!cache.has(value)) cache.set(value, value.bind(target));
retrun cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
}
const animal = selfish(new Animal());
二、constructor 关键字
上面第一段代码中的 constructor 方法,是构造方法,this 关键字则代表实例对象。
一个类必须要有 constructor 方法,如果没有显示定义,一个空的 constructor 方法会被默认添加
class Animal { }
// 等同于
class Animal {
constructor() {}
}
实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上),并且,类的所有实例共享一个原型对象。
class Person {
//自身属性
constructor( name , age ) {
this.name = name;
this.age = age;
}
//原型对象的属性
say() {
return 'My name is ' + this.name + ', I am ' + this.age + ' years old';
}
}
var person = new Person( 'Jack' , 23);
person.hasOwnProperty('name') // true
person.hasOwnProperty('age') // true
person.hasOwnProperty('say') // false
person.__proto__.hasOwnProperty('say') // true
上述代码中,name 和 age 实例对象person自身的属性(因为定义在this变量上) ,所以hasOwnProperty方法返回true,而say是原型对象的属性(因为定义在Person类上),所以hasOwnProperty方法返回false。
三、static 关键字
如果在一个方法钱加上 static 关键字,就表示该方法不会被实例继承,而是通过类调用,成为静态方法。
class Foo {
static calssMethod() {
return 'hello';
}
}
Foo.classMethod(); // hello
var foo = new Foo();
foo.classMethod(); // TypeError: foo.calssMethod is not function
父类的静态方法可以被子类继承:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo { }
Bar.classMethod(); // hello
静态方法也可以从 super 对象上调用:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod(); // hello, too
四、extends 关键字
在之前的 ES5 中,处理原型的继承非常麻烦,我们先看一下 ES5 是如何处理原型继承的:
function Monkey(type, name) {
Animal.apply(this, [type, name]);
}
Monkey.prototype = Object.create(Animal.prototype, {
toSting: function() {
return "monkey is" + tjhis.type + ",name is " + this.name;
}
};
Monkey.prototype.constructor = Monkey;
在 ES6 中使用 extends 关键字实现原型继承:
class Monkeyi extends Animal {
constructor(type, name) {
super(type, name);
}
toString() {
return "monkey is" `${this.type}` ",name is "`{ this.name}`;
}
}
五、super 关键字
当你想在子类中调用父类 的函数时,super 关键字就很有作用了,使用这个关键字时应该注意:
使用 super 关键字的时候,必须显示指定是作为函数还是作为对象使用,否则会报错。
1、super 作为函数调用时,代表父类的构造函数。ES6 中要求,子类的构造函数必须执行一次 super 函数。
class A { }
class B extends A {
f() {
super(); // 报错
}
}
2、super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class Parent {
static myMethod(msg) {
console.log('static');
}
myMethod(msg) {
console.log('instance');
}
}
class Child extends Parent {
static myMethod() {
super.myMethod();
}
myMethod(msg) {
super.myMethod();
}
}
Child.myMethod(); // static
var child = new Child();
child.myMethod(); // instance
3、通过 super 调用父类的方法时,super 会绑定子类的 this:
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
4、由于绑定子类的 this,因此通过 super 对某个属性赋值,这是 super 就是 this,赋值的属性会变成子类实例的属性:
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
六、其它补充
除了以上几个关键字的介绍,如要了解如 new.target 属性、私有属性等,请参看阮一峰的《ES6 标准入门(第三版)》书籍,或参考 class 的基本语法。
七、令人遐想的 Mixin 模式的实现
所谓 Mixin 模式:将多个类的接口“混入”(mix in)另一个类。如下:
funtion mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc):
}
}
}
上述代码中的 Mix 函数可以将多个对象合成一个类。使用方法如下:
class DistributeEdit extends mix(Loggable, Serializable) {
...
}
结语
本章主要是以ES6 类的五个关键字为主,粗略的学习了,在 ES6 中是如何定义类,以及其用法,再则就是类的继承。对于类还需进一步的理解,本文比较粗糙,后续会进行相关专题的深入学习。如若有错,欢迎拍砖。
章节目录
1、ES6中啥是块级作用域?运用在哪些地方?
2、ES6中使用解构赋值能带给我们什么?
3、ES6字符串扩展增加了哪些?
4、ES6对正则做了哪些扩展?
5、ES6数值多了哪些扩展?
6、ES6函数扩展(箭头函数)
7、ES6 数组给我们带来哪些操作便利?
8、ES6 对象扩展
9、Symbol 数据类型在 ES6 中起什么作用?
10、Map 和 Set 两数据结构在ES6的作用
11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
12、从 Promise 开始踏入异步操作之旅
13、ES6 迭代器(Iterator)和 for…of循环使用方法
14、ES6 异步进阶第二步:Generator 函数
15、JavaScript 异步操作进阶第三步:async 函数
16、ES6 构造函数语法糖:class 类