Classes
我们知道javascript是没有类的概念的, ES6新添加了 class 关键词,为书写类提供了很大的便利,下面我们通过ES5和ES6对比来看一下ES6这种新的语法糖。
- class 与 function ;
- class 与 对象字面量 ;
- 静态方法 "static" 关键词 ;
- 继承 "extends", "super" ;
- 抽象类 new.target
一. class和函数的关系
1.ES5类的表达方式 和 ES6 class
ES5通过函数构造器和原型的方法来模拟类的概念
function Person(name, age) {
this.name = name;
this.age = age;
}
// 将方法写在原型链上的原因是:函数是对象
// 如果每次实例化一个对象都创建一个相同的函数,这样会占用很多内存
Person.prototype.sayName = function() {
console.log(this.name);
}
// 实例化一个对象
var person = new Person("james", 26);
person.sayName(); // "james"
console.log(person instanceof Person); // true
ES6 通过 class关键词, 还有constructor()方法, 将实例属性写在构造器函数中, 原型上的属性使用简写语法,上面的例子相当于:
class Person {
// 构造器
constructor(name, age) {
this.name = name;
this.age = age;
}
// 简写语法,原型链上的属性
sayName() {
console.log(this.name);
}
}
// 实例化 new操作符
var person = new Person("kobe", 37);
person.sayName(); // "kobe"
// 注意下面Person的实质类型
console.log(typeof Person); // "function"
console.log(typeof Person.prototype.sayName); // "function"
2.class语法和自定义函数表示类的区别
- class declarations 不提升(hoisting),就像用let声明一样,函数声明会自动提升;
- 在class declarations中,所有的代码自动在严格模式下;
- class中所有的方法都是不可枚举的(nonenumerable).函数声明要想方法不可枚举,需要使用Object.defineProperty方法;
- class中的方法内部都不存在[[Construct]]属性,因此不能使用new;
- 调用class构造器必须使用new,否则抛出异常;
- 尝试用class中的方法改写 class name会抛出错误,在外部改写,则像使用let声明之后重新赋值一样,不会抛出错误;
class语法相当于下面代码:
// 使用let声明,避免变量提升
let PersonType = (function() {
"use strict";
// 使用const, 内部不能改变class 名称
const PersonType = function(name) {
// 确保构造器使用new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new");
}
this.name = name;
}
// 方法不可枚举
Object.defineProperty(PersonType.prototype, "sayName", {
value: function() {
// 确保方法不使用new操作符
if (typeof new.target !== "undefined") {
console.log(this.name)
}
},
enumerable: false,
writable: true,
configurable: true
});
return PersonType;
}());
3.Class Expressions
class 和 function 的相似之处都有两种形式: 声明式 和 表达式, class表达式一般用作变量声明或者当作参数传入函数中。
表达式形式:
let Person = class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
};
功能上与class声明形式一样,就是"name"属性不一样(因为class本质上是function,ES6函数新添加了name 属性)。上面的匿名class表达式 Person.name 为空字符串""
4.Named Class expressions
这个和Named Function expressions一样,就是在class后面添加一个标识符, 该标识符只能在class定义内部使用
let Person = class PersonType {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
console.log(typeof Person); // "function"
console.log(typeof PersonType); // undefined PersonType只能在class内部使用
相当于:
let Person = (function() {
"use strict";
// 使用const, 内部不能改变class 名称 此处和 Person不一样
const PersonType = function(name) {
// 确保构造器使用new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new");
}
this.name = name;
}
// 方法不可枚举
Object.defineProperty(PersonType.prototype, "sayName", {
value: function() {
// 确保方法不适用new操作符
if (typeof new.target !== "undefined") {
console.log(this.name)
}
},
enumerable: false,
writable: true,
configurable: true
});
return PersonType;
}());
二.class和对象字面量关系
1.访问器get,set属性
class能够在构造器中定义实例属性,也可以在原型上定义访问器属性(accessor properties), 定义方法和在对象字面量中的方式一致:
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
熟悉jquery的应该知道html()方法,原理和这个差不多,相当于:
let CustomHTMLElement = (function() {
function CustomHTMLElement(element) {
// 确保使用new调用构造器
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new");
}
this.element = element;
}
Object.defineProperty(CustomHTMLElement.prototype, "html", {
enumerable: false,
configurable: true,
get: function() {
return this.element.innerHTML;
},
set: function(value) {
this.element.innerHTML = value;
}
});
return CustomHTMLElement;
}());
2.Computed Member Names
对象字面量中有个计算变量属性,ES6新添加的语法, 同样class 方法和访问器 也可以采用类似的计算命名
let methodName = "sayName";
class Person {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
}
访问器:
let propertyName = "html";
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get [propertyName]() {
return this.element.innerHTML;
}
set [propertyName](value) {
this.element.innerHTML = value;
}
}
3.产生器方法(generator method)
在方法名前添加 "*",可以将任意方法变为一个产生器, Generator method对对象是集合类型值,进行迭代十分的方便, 定义一个默认的迭代器
可以通过 Symbol.iterator 来定义一个默认产生器方法:
class Collection {
constructor() {
// 属性为集合类型
this.items = [];
}
// 定义一个默认的产生器方法, 使用computed name
*[Symbol.iterator]() {
// 利用数组默认的迭代器values()
yield *this.items.values();
}
}
var c = new Collection();
c.items.push(1);
c.items.push(2);
c.items.push(3);
for (let n of c) {
console.log(n);
}
// 1
// 2
// 3
// 使用spread操作符
var arr = [...c];
arr; // [1, 2, 3]
4.first-citizen
class的本质是function,意味着可以当作对象一样当作参数或者作为返回值。
1.当作参数
function createObj(classDef) {
return new classDef();
}
let obj = createObj(class {
sayHi() {
console.log("hi");
}
});
obj.sayHi(); // "hi"
2.创建单利(singletons),利用IIEF
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("Nicholas");
person.sayName(); // "Nicholas"
三.静态方法 static
ES6之前可以通过直接添加方法到构造器来模拟静态方法(区别于实例方法)
function PersonType(name) {
this.name = name;
}
// 模拟静态方法 static method
PersonType.create = function(name) {
// 调用构造器
return new PersonType(name);
}
// instance method
PersonType.prototype.sayName = function() {
console.log(this.name);
}
// 直接对类PersonType调用方法
var person = PersonType.create("Mike");
ES6 引入 static 关键词
class PersonType {
constructor(name) {
this.name = name;
}
// instance method
sayName() {
console.log(this.name);
}
// static method
static create(name) {
return new PersonType(name);
}
}
我们可以对任何方法添加 static 关键词 使之变为静态方法,唯一不能对constructor构造器添加static; 另外,静态成员只能直接通过class,而不能使用实例去访问
四.继承
ES6之前,实现继承是比较繁琐的步骤,我们需要使用构造器窃取,将子类指向一个父类的实例(或者使用Object.create()方法),下面我们来看一下
1.ES6之前模拟类继承
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
Rectangle.prototype.getArea = function() {
return this.width * this.height;
}
function Square(width) {
// 构造器窃取
Rectangle.call(this, width, width);
}
// 实现继承
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
// 将原型重新指向自身
value: Square,
enumerable: true,
wriable: true,
configurable: true
}
});
var square = new Square(5);
square.getArea(); // 25
square instanceof Square; // true
square instanceof Rectangle; // true
2.ES6通过 super() 和 extends 实现继承
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(width) {
// 使用super()
super(width, width);
}
}
var square = new Square(5);
square.getArea(); // 25
square instanceof Square; // true
square instanceof Rectangle; // true
可以看出这种写法十分的简洁,同时也十分的清晰
使用super()注意事项:
- 只能在派生类(即子类)构造器中使用super(),在非派生类中或者一个function中使用super,会抛出异常;
- 在访问this之前必须先调用super(),因为super()是负责实例化this的;
- 唯一能避免调用super()的方式是从一个对象构造器中返回一个对象
3.子类override父类方法
学过其他编程语言的肯定知道override,可以用来重写父类中的属性,ES6也提供了这样的选择,可以直接重写,也可以使用super.property方式
class Square extends Rectangle {
constructor(width) {
super(width, width);
}
// 重写或者shadow父类原型上的方法getArea
getArea() {
return this.width * this.width;
}
}
或使用super
class Square extends Rectangle {
constructor(width) {
super(width, width);
}
// override or shadow 并调用父类原型上的方法
getArea() {
return super.getArea();
}
}
4.继承父类静态属性
父类上的静态属性同样可以被继承,这是ES6新概念
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
// 静态方法
static create(width, height) {
return new Rectangle(width, height);
}
}
class Square extends Rectangle {
constructor(width) {
super(width, width);
}
}
// 子类调用父类静态方法
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
rect.getArea(); // 12
console.log(rect instanceof Square); // false 注意!
5.派生类继承自表达式
1.当一个表达式拥有[[Construct]]属性 和 原型 就能够被其他类继承,这种方式给动态的继承提供了很大的便利,后面会谈到
比如:
// 构造器, 拥有[[Construct]]属性
function Rectangel(width, height) {
this.width = width;
this.height = height;
}
// 拥有原型
Rectangle.prototype.getArea = function() {
return this.widht * this,height;
}
// 实现继承
class Square extends Rectangle {
constructor(width) {
super(width, width);
}
}
var x = new Square(4);
x.getArea(); // 16
x instanceof Rectangle; // true
2.动态的决定继承自谁
// 构造器, 拥有[[Construct]]属性
function Rectangel(width, height) {
this.width = width;
this.height = height;
}
// 拥有原型
Rectangle.prototype.getArea = function() {
return this.widht * this,height;
}
// 利用这个函数,返回函数对象Rectangle
function getBase() {
return Rectangle;
}
// 动态实现继承, 调用getBase()
class Square extends getBase() {
constructor(width) {
super(width, width);
}
}
var x = new Square(4);
x.getArea(); // 16
x instanceof Rectangle; // true
3.上面的方法可以用来创建mixins, 将多个属性添加到同一个类中,然后子类继承该mixins
let serializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.width * this.height;
}
};
// 创建mixins,利用Object.assign()
function Mixins(...mixins) {
// 创建一个空的base函数
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
// 继承Mixins
class Square extends Mixins(serializableMixin, AreaMixin) {
constructor(width) {
super(width, width);
// 两个实例属性
this.width = width;
this.height = width;
}
}
var s = new Square(3);
s.getArea(); // 9
s.serilize(); // {"width": 3, "height: 3}
4.继承内置类(built-in)
ES6之前对继承内置类会出现问题,但是ES6允许继承内置类
比如继承Array类,则拥有Array上的属性:
class MyArray extends Array {
// empty
}
var myArray = new MyArray();
myArray[0] = "red";
myArray.length; // 1
5.Symbol.species
Symbol.species: 一个静态访问器属性,用于返回一个function,这个function是一个构造器,当实例必须使用内部方法创建时,下列内置类型拥有 Symbol.species 属性:
- Array
- ArrayBuffer
- Map
- Promise
- RegExp
- Set
- TypeArray
eg:
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
// 使用内置方法创建实例, 允许派生类改写这个值
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
// 派生类
class DerivedClass1 extends MyClass {
// empty
}
class DerivedClass2 extends MyClass {
// 改写
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new DerivedClass1("foo"),
clone1 = instance1.clone();
let instance2 = new DerivedClass2("bar"),
clone2 = instance2.clone();
instance1 intanceof MyClass; // true
clone1 instanceof DerivedClass1; // true
instance2 instanceof MyClass; // true
clone2 instanceof DerivedClass2; // false
比如上面的例子:
class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
var arr = new MyArray(1,2,3,4);
var sub = arr.slice(1, 3);
sub instanceof Array; // true
sub instanceof MyArray; // false
五.创建抽象类
抽象类的本质就是当父类不能实例化,当父类使用new操作符时抛出异常,这我们可以相当使用new.target来控制
class Sharp {
contructor(width, height) {
// 如果new 的目标是 类 本身 则抛出异常
if (new.target === Sharp) {
throw new Error("Abstract class cannot be instantiated");
}
}
}
class Rectangle extends Sharp {
contructor(width, height) {
super();
this.width = width;
this.height = height;
}
}
var sharp = new Sharp(); // ERROR
var rec = new Rectangel(10, 5);
rec instanceof Sharp; // true
总结
class的出现为类的创建提供了极大的便利,这种语法糖其本质还是function,拥有函数该有的特性,比如几种表达方式,作为first-citizen,能够作为参数或者返回值;另外书写方面和对象字面量又十分的相识,提供各种简写。另外最重要的是对实现继承是非常的友好,同时mixins,对扩展类的功能提供了很大方便,还有抽象类的出现,这使得javascript更加的OOP了。