前面的话
es6之前js都不支持类和类的继承,直到es6引入了类的特性。
es5的语法
js语言的传统方法通过构造函数定义并生成新对象。
function Person (name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
let person = new Person('xiaoqi');
person.sayName();// 'xiaoqi'
console.log(person instanceof Person);// true
console.log(person instanceof Object); // true
这段代码Person是一个构造函数,给其创建一个name属性,给Person的原型添加一个sayName()方法, 所有Person实例共享这个方法。使用new操作符创建person实例,由于存在原型继承的特性,所以person也是Object的实例。
Class 基本形式
es6引入了Class这个概念作为对象的模板,es6中的class可以看做只是一个语法糖,它的绝大部分的功能,ES5都能做到,新的class写法只是让对象原型的写法更清晰。
上面的例子改写为“类”的形式:
class Person {
// 等价于Person 构造器
constructor (name) {
this.name = name;
}
// 等价于Person.prototype.sayName
sayName() {
console.log(this.name);
}
}
let person = new Person('xiaoqi');
person.sayName();// 'xiaoqi'
console.log(person instanceof Person);// true
console.log(person instanceof Object);// true
console.log(typeof Person);// function
console.log(person.constructor === Person);// true
console.log(typeof Person.prototype.sayName);// function
console.log(Person === Person.prototype.constructor);// true
- 通过类声明的Person与创建构造函数Person的过程相似,只是在类中通过特殊的constructor方法名来定义构造函数,不需要加function这个保留字。
- 方法之间不需要逗号分隔,加了会报错。
- 私有属性是实例中的属性,不会出现在原型上,且只能在类的构造函数或方法中创建,上例中name就是一个私有属性。
- es6的类完全可以看作构造函数的另一种写法,类本身就指向构造函数。并且构造函数的prototype属性在es6上继续存在,类的所有方法(除constructor以外)都定义在类的prototype属性上。
[注意]: 与函数不同的是,类属性不可被赋予新值,Person.prototype就是一个只读类属性
差异
es6的类与es5的构造函数之间的差异:
- 类的内部定义的原型上的所有方法都是不可枚举的
- 类和模块内部默认使用严格模式,不需要使用use strict指定运行模式
- constructor方法是类的默认方法,通过new命令生成的对象实例时自动调用该方法。一个类必须有constructor方法,如果没有显示的定义,一个空的constructor方法会被默认添加。
class Person {
}
// 等同于
class Point {
constructor() {}
}
constructor方法默认返回实例对象(即this),不过完全可以像es5那样,返回一个新的对象
class Foo{
constructor() {
return Object.create(null)
}
}
var foo = new Foo();
console.log(foo.constructor === Foo);// false
console.log(foo.constructor);// undefined
constructor函数返回了一个新的对象,导致实例对象不是Foo的实例
-
类必须使用new操作符来调用,否则会报错
-
类不存在变量提升,这与es5完全不同
new Foo() // ReferenceError class Foo {}
-
es6的类只是es5构造函数的一层包装,所以函数的许多特性都被从class继承,例如name属性
class Point {}
Point.name // Point
name属性总是返回紧跟在class关键字后面的类名
Class表达式
与函数一样,class也可以使用表达式的形式定义:
const MyClass = class Me {
getClassName () {
return Me.name
}
}
let inst = new MyClass();
console.log(inst.getClassName());// Me
console.log(MyClass.name);// Me
这个类的名字是MyClass,而不是Me,Me只是在class的内部代码可用,指代当前类。
如果class内部没有用到,那么可以省略Me,也可以写成下面的形式:
const MyClass = class{ }
采用Class表达式,可以得到立即执行的class实例:
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('xiaoqi');
person.sayName();// 'xiaoqi'
私有方法
私有方法是常见的需求,但es6不提供,只能通过变通的方法来模拟实现
方法1: 在命名上加以区别
class Widget {
// 共有方法
foo(baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
}
_bar方法前面的下划线表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部还是可以调用这个方法
方法2:将私有方法移除模块,因为模块内的所有方法都是对外可见的
class Widget {
foo (baz) {
bar.call(this, baz);
}
}
function bar (baz) {
return this.snaf = baz;
}
let instance = new Widget();
instance.foo('xiao');
console.log(instance.snaf); // 'xiao'
foo是共有的方法,内部调用bar.call(this, baz),使得bar实际上成为当前模块的私有方法
方法3: 利用Symbol值的唯一性将私有方法的名字命名为一个Symbol值
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass {
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
}
上面的代码中,bar和sanf都是Symbol值,导致第三方无法获取它们,因此达到私有方法与私有属性的效果。
私有属性
与私有方法一样,es6不支持私有属性。目前,有一个提案为给class加私有属性。方法是在属性名前面使用#来表示
class Point {
#x;
constructor (x = 0) {
#x = +x;
}
get x() {
return #x;
}
set x(value) {
#x = +value
}
}
#x表示私有属性x,在Point类之外是读取不到这个属性的。
该提案只规定了私有属性的写法。但是很自然的,它也可以用来编写私有方法
class Foo{
#a;
#b;
#sum() {
return #a + #b;
}
printSum() {
console.log(#sum());
}
constructor(a,b) {
#a = a;
#b = b;
}
}
访问器属性
与es5一样,在类的内部可以使用get和set关键字对某个属性设置存值函数和取值函数
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, 'html');
console.log('get' in descriptor); // true
console.log('set' in descriptor); // true
console.log(descriptor.enumerable);// false
这段代码中的CustomHTMLElement类是一个针对现有DOM元素的包装器,并通过getter和setter方法将这个元素的innerHTML方法委托给HTML属性,这个访问器属性是在CustomHTMLElement.prototypr上创建的。与其他方法一样,这两个方法都是不可枚举的。
Generator生成器方法
在某个方法之前加上星号(*),就表示该方法是一个Generator函数
class MyClass {
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}
let instance = new MyClass();
let iterator = instance.createIterator();
console.log( iterator.next());// {value: 1, done: false}
这段代码创建一个MyClass的类,它有一个生成器方法createIterator(),其返回值为一个迭代器。
尽管生成器方法很实用,但如果类是用来表示值的集合的,那么为它定义一个默认迭代器会更有用。
通过Symbol.iterator定义生成器方法即可为类定义默认的迭代器。
class Collection {
constructor() {
this.items = [];
}
*[Symbol.iterator]() {
yield *this.items.values();
}
}
var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for(let x of collection) {
// 1
// 2
// 3
console.log(x);
}
上面的代码 Collection类的Symbol.iterator方法返回一个默认的遍历器。任何管理一系列值的类都应该引入默认的迭代器,因为一些与特定集合有关的操作需要所操作的集合含有一个迭代器。现在可以将collection实例直接用于for-of循环中或用展开运算符操作它。
静态成员
[es5中,模拟静态方法]
function Person(name) {
this.name = name;
}
// 静态方法
Person.create = function (name) {
console.log('xiao');
}
// 实例方法
Person.prototype.sayName = function() {
console.log(this.name);
}
Person.create();// 'xiao'
[class的静态方法]
所有在类中定义的方法都会被实例继承。如果在一个方法前加上static关键字,就表示方法不会被实例继承,而是通过类调用,称为“静态方法”
class PersonClass {
// 等价于构造器
constructor (name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create('xiaoqi');
console.log(person.name);// 'xiaoqi'
静态方法在类上直接调用,不能再实例上调用
[class静态属性和实例属性]
实例属性:可以用等式直接写入类的定义中.
静态属性: 在实例属性上加static关键字.
class MyClass {
myProp = 42;
static myStaticProp = 43;
constructor() {
console.log(this.myProp);// 42
console.log(MyClass.myStaticProp);//43
}
}
let instance = new MyClass();
}
new.target属性
new.target这个属性可以用来确定构造函数是怎么调用的。
在构造函数中返回new命令所作用的构造函数。如果构造函数不是通过new命令调用的,那么new.target会返回undefined。
function Person(name) {
if(new.target !== undefined) { // 或者 new.target === Person
this.name = name
}else {
throw new Error('必须使用new生成实例')
}
}
var person = new Person('xiaoqi');
//var notPerson = Person.call(person, 'xiaoqi');// Uncaught Error: 必须使用new生成实例
子类继承父类时new.target会返回子类:
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle) ;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
let obj = new Square(3);// false
利用这个特点写出不能独立使用而必须继承之后才能使用的类:
class Shape {
constructor () {
if(new.target === Shape){
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor (length, width){
super();
}
}
let x = new Shape();// Uncaught Error: 本类不能实例化
let y = new Rectangle(4,4);