Classes
我們知道javascript是沒有類的概念的细卧, ES6新添加了 class 關(guān)鍵詞茴扁,為書寫類提供了很大的便利,下面我們通過ES5和ES6對比來看一下ES6這種新的語法糖石洗。
- class 與 function ;
- class 與 對象字面量 ;
- 靜態(tài)方法 "static" 關(guān)鍵詞 ;
- 繼承 "extends", "super" ;
- 抽象類 new.target
一. class和函數(shù)的關(guān)系
1.ES5類的表達方式 和 ES6 class
ES5通過函數(shù)構(gòu)造器和原型的方法來模擬類的概念
function Person(name, age) {
this.name = name;
this.age = age;
}
// 將方法寫在原型鏈上的原因是:函數(shù)是對象
// 如果每次實例化一個對象都創(chuàng)建一個相同的函數(shù)缀雳,這樣會占用很多內(nèi)存
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關(guān)鍵詞羞延, 還有constructor()方法渣淳, 將實例屬性寫在構(gòu)造器函數(shù)中, 原型上的屬性使用簡寫語法伴箩,上面的例子相當(dāng)于:
class Person {
// 構(gòu)造器
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的實質(zhì)類型
console.log(typeof Person); // "function"
console.log(typeof Person.prototype.sayName); // "function"
2.class語法和自定義函數(shù)表示類的區(qū)別
- class declarations 不提升(hoisting),就像用let聲明一樣,函數(shù)聲明會自動提升;
- 在class declarations中嗤谚,所有的代碼自動在嚴格模式下;
- class中所有的方法都是不可枚舉的(nonenumerable).函數(shù)聲明要想方法不可枚舉棺蛛,需要使用Object.defineProperty方法;
- class中的方法內(nèi)部都不存在[[Construct]]屬性,因此不能使用new;
- 調(diào)用class構(gòu)造器必須使用new,否則拋出異常;
- 嘗試用class中的方法改寫 class name會拋出錯誤巩步,在外部改寫鞠值,則像使用let聲明之后重新賦值一樣,不會拋出錯誤;
class語法相當(dāng)于下面代碼:
// 使用let聲明渗钉,避免變量提升
let PersonType = (function() {
"use strict";
// 使用const, 內(nèi)部不能改變class 名稱
const PersonType = function(name) {
// 確保構(gòu)造器使用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表達式一般用作變量聲明或者當(dāng)作參數(shù)傳入函數(shù)中。
表達式形式:
let Person = class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
};
功能上與class聲明形式一樣鳄橘,就是"name"屬性不一樣(因為class本質(zhì)上是function声离,ES6函數(shù)新添加了name 屬性)。上面的匿名class表達式 Person.name 為空字符串""
4.Named Class expressions
這個和Named Function expressions一樣瘫怜,就是在class后面添加一個標(biāo)識符术徊, 該標(biāo)識符只能在class定義內(nèi)部使用
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內(nèi)部使用
相當(dāng)于:
let Person = (function() {
"use strict";
// 使用const, 內(nèi)部不能改變class 名稱 此處和 Person不一樣
const PersonType = function(name) {
// 確保構(gòu)造器使用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和對象字面量關(guān)系
1.訪問器get,set屬性
class能夠在構(gòu)造器中定義實例屬性,也可以在原型上定義訪問器屬性(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的應(yīng)該知道html()方法鲸湃,原理和這個差不多赠涮,相當(dāng)于:
let CustomHTMLElement = (function() {
function CustomHTMLElement(element) {
// 確保使用new調(diào)用構(gòu)造器
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.產(chǎn)生器方法(generator method)
在方法名前添加 "*"笋除,可以將任意方法變?yōu)橐粋€產(chǎn)生器丁侄, Generator method對對象是集合類型值硅堆,進行迭代十分的方便议谷, 定義一個默認的迭代器
可以通過 Symbol.iterator 來定義一個默認產(chǎn)生器方法:
class Collection {
constructor() {
// 屬性為集合類型
this.items = [];
}
// 定義一個默認的產(chǎn)生器方法, 使用computed name
*[Symbol.iterator]() {
// 利用數(shù)組默認的迭代器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的本質(zhì)是function,意味著可以當(dāng)作對象一樣當(dāng)作參數(shù)或者作為返回值裙盾。
1.當(dāng)作參數(shù)
function createObj(classDef) {
return new classDef();
}
let obj = createObj(class {
sayHi() {
console.log("hi");
}
});
obj.sayHi(); // "hi"
2.創(chuàng)建單利(singletons),利用IIEF
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("Nicholas");
person.sayName(); // "Nicholas"
三.靜態(tài)方法 static
ES6之前可以通過直接添加方法到構(gòu)造器來模擬靜態(tài)方法(區(qū)別于實例方法)
function PersonType(name) {
this.name = name;
}
// 模擬靜態(tài)方法 static method
PersonType.create = function(name) {
// 調(diào)用構(gòu)造器
return new PersonType(name);
}
// instance method
PersonType.prototype.sayName = function() {
console.log(this.name);
}
// 直接對類PersonType調(diào)用方法
var person = PersonType.create("Mike");
ES6 引入 static 關(guān)鍵詞
class PersonType {
constructor(name) {
this.name = name;
}
// instance method
sayName() {
console.log(this.name);
}
// static method
static create(name) {
return new PersonType(name);
}
}
我們可以對任何方法添加 static 關(guān)鍵詞 使之變?yōu)殪o態(tài)方法往史,唯一不能對constructor構(gòu)造器添加static咨跌; 另外考廉,靜態(tài)成員只能直接通過class,而不能使用實例去訪問
四.繼承
ES6之前,實現(xiàn)繼承是比較繁瑣的步驟上忍,我們需要使用構(gòu)造器竊取惯殊,將子類指向一個父類的實例(或者使用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) {
// 構(gòu)造器竊取
Rectangle.call(this, width, width);
}
// 實現(xiàn)繼承
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 實現(xiàn)繼承
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()注意事項:
- 只能在派生類(即子類)構(gòu)造器中使用super(),在非派生類中或者一個function中使用super,會拋出異常;
- 在訪問this之前必須先調(diào)用super(),因為super()是負責(zé)實例化this的;
- 唯一能避免調(diào)用super()的方式是從一個對象構(gòu)造器中返回一個對象
3.子類override父類方法
學(xué)過其他編程語言的肯定知道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 并調(diào)用父類原型上的方法
getArea() {
return super.getArea();
}
}
4.繼承父類靜態(tài)屬性
父類上的靜態(tài)屬性同樣可以被繼承陕习,這是ES6新概念
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
// 靜態(tài)方法
static create(width, height) {
return new Rectangle(width, height);
}
}
class Square extends Rectangle {
constructor(width) {
super(width, width);
}
}
// 子類調(diào)用父類靜態(tài)方法
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
rect.getArea(); // 12
console.log(rect instanceof Square); // false 注意霎褐!
5.派生類繼承自表達式
1.當(dāng)一個表達式擁有[[Construct]]屬性 和 原型 就能夠被其他類繼承,這種方式給動態(tài)的繼承提供了很大的便利该镣,后面會談到
比如:
// 構(gòu)造器冻璃, 擁有[[Construct]]屬性
function Rectangel(width, height) {
this.width = width;
this.height = height;
}
// 擁有原型
Rectangle.prototype.getArea = function() {
return this.widht * this,height;
}
// 實現(xiàn)繼承
class Square extends Rectangle {
constructor(width) {
super(width, width);
}
}
var x = new Square(4);
x.getArea(); // 16
x instanceof Rectangle; // true
2.動態(tài)的決定繼承自誰
// 構(gòu)造器, 擁有[[Construct]]屬性
function Rectangel(width, height) {
this.width = width;
this.height = height;
}
// 擁有原型
Rectangle.prototype.getArea = function() {
return this.widht * this,height;
}
// 利用這個函數(shù)损合,返回函數(shù)對象Rectangle
function getBase() {
return Rectangle;
}
// 動態(tài)實現(xiàn)繼承, 調(diào)用getBase()
class Square extends getBase() {
constructor(width) {
super(width, width);
}
}
var x = new Square(4);
x.getArea(); // 16
x instanceof Rectangle; // true
3.上面的方法可以用來創(chuàng)建mixins, 將多個屬性添加到同一個類中省艳,然后子類繼承該mixins
let serializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.width * this.height;
}
};
// 創(chuàng)建mixins,利用Object.assign()
function Mixins(...mixins) {
// 創(chuàng)建一個空的base函數(shù)
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.繼承內(nèi)置類(built-in)
ES6之前對繼承內(nèi)置類會出現(xiàn)問題嫁审,但是ES6允許繼承內(nèi)置類
比如繼承Array類跋炕,則擁有Array上的屬性:
class MyArray extends Array {
// empty
}
var myArray = new MyArray();
myArray[0] = "red";
myArray.length; // 1
5.Symbol.species
Symbol.species: 一個靜態(tài)訪問器屬性,用于返回一個function,這個function是一個構(gòu)造器律适,當(dāng)實例必須使用內(nèi)部方法創(chuàng)建時辐烂,下列內(nèi)置類型擁有 Symbol.species 屬性:
- Array
- ArrayBuffer
- Map
- Promise
- RegExp
- Set
- TypeArray
eg:
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
// 使用內(nèi)置方法創(chuàng)建實例, 允許派生類改寫這個值
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
五.創(chuàng)建抽象類
抽象類的本質(zhì)就是當(dāng)父類不能實例化厂僧,當(dāng)父類使用new操作符時拋出異常扣草,這我們可以相當(dāng)使用new.target來控制
class Sharp {
contructor(width, height) {
// 如果new 的目標(biāo)是 類 本身 則拋出異常
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
總結(jié)
class的出現(xiàn)為類的創(chuàng)建提供了極大的便利,這種語法糖其本質(zhì)還是function颜屠,擁有函數(shù)該有的特性辰妙,比如幾種表達方式,作為first-citizen甫窟,能夠作為參數(shù)或者返回值密浑;另外書寫方面和對象字面量又十分的相識,提供各種簡寫蕴坪。另外最重要的是對實現(xiàn)繼承是非常的友好肴掷,同時mixins,對擴展類的功能提供了很大方便敬锐,還有抽象類的出現(xiàn)背传,這使得javascript更加的OOP了。