完整詳細的es6 class
class基本宣告
在說class
之前,想必大家肯定會想到constructor function
. 看下面程式碼:
function Foo(name) {
this.name = name
}
class Bar {
constructor(name){
this.name = name
}
}
f = new Foo('xhs')
b = new Bar('xhs')
兩個差不多吧,foo function
是在new
的時候,把this
指向當前的新建立的空物件,並且會把進行屬性分配。bar class
constructor
裡進行接收引數。
但是兩個還是 有些不同
class
宣告並不像function
宣告,他不存在提升。他類似let
宣告,存在TDZ(temporal dead zone)
。class
中的程式碼都會自動的使用嚴格模式,沒辦法選擇。- 所有的方法都是不可列舉的(
non-enumerable
), 注:非綁定當前物件的方法。 class
內所有方法內部都缺少[[Construct]]
方法,所以如果對這些方法進行new
會出錯。- 不攜帶
new
操作符呼叫class
會報錯。 - 嘗試在類的方法中改變類名會出錯。
考慮到上面這幾點,下面來看一個等價的例子:
class PersonClass {
// equivalent of the PersonType constructor
constructor(name) {
this.name = name;
}
// equivalent of PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}
上面的程式碼將等價下面無class
的語法
// direct equivalent of PersonClass
let PersonType2 = (function() {
"use strict";
const PersonType2 = function(name) {
// make sure the function was called with new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(PersonType2.prototype, "sayName", {
value: function() {
// make sure the method wasn't called with new
if (typeof new.target !== "undefined") {
throw new Error("Method cannot be called with new.");
}
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});
return PersonType2;
}());
我們來分析上面這個無class
語法的程式碼段。
首先注意到這裡有兩個PersonType2
的宣告(let
宣告在作用域外面,const
在IIFE
裡),這個就是禁止類方法覆蓋類名。
在構造方法裡有new.target
來檢測確保通過new
呼叫,與之相對的是對方法的檢測,排除new
方法呼叫的可能,否則拋錯。在下面就是enumerable: false
,最後返回這個建構函式.
雖然上面的程式碼可以實現class
的效果,但是明顯,class
更加簡潔方便。
類的常量名稱。
常量是不可被改變的,否則就會報錯。類的名稱只是在內部使用const
,這就意味著在內部不可以改變名稱,外面卻可以。
class Foo {
constructor() {
Foo = "bar"; // 執行的時候報錯。
}
}
// 這裡不會報錯。
Foo = "baz";
class表示式
class
和function
類似,也可以使用表示式。
let PersonClass = class {
// equivalent of the FunctionName constructor
constructor(name) {
this.name = name;
}
// equivalent of FunctionName.prototype.sayName
sayName() {
console.log(this.name);
}
};
可以發現,表示式語法類似,使用class
的表示式還是宣告都只是風格的不同,不像建構函式的宣告和表示式有著提升的區別。
當然,上面的表示式是一個匿名錶達式,我們可以建立一個攜帶名稱的表示式。
let PersonClass = class PersonClass2 {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
};
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass2); // undefined
可以發現上面輸出PersonClass2
是未定義,因為他只有存在類定義中, 如需瞭解,我們做下面的一個轉變:
// direct equivalent of PersonClass named class expression
let PersonClass = (function() {
"use strict";
const PersonClass2 = function(name) {
// make sure the function was called with new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(PersonClass2.prototype, "sayName", {
value: function() {
// make sure the method wasn't called with new
if (typeof new.target !== "undefined") {
throw new Error("Method cannot be called with new.");
}
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});
return PersonClass2;
}());
這個轉變與上面的**class
宣告略有不同,class
宣告的時候,內部與外部的名稱相同,但是在class
表示式** 中,卻不同。
Classes
第一等公民
在程式設計世界中,當某個東西可以作為一個值使用時,這意味著它可以被傳遞到函式中,從函式返回,可以分配給變數,它被認為是一等的公民。所以在javascript
中,function
是第一等公民.
ES6
中使用class
沿用了這一傳統,所以class
有很多方式去使用它,下面來看將他作為一個引數:
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
});
obj.sayHi(); // "Hi!"
class
有一個有意思的是使用立即執行來建立單例
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("xhs");
person.sayName(); // "xhs"
這樣就建立了一個單例。
訪問的屬性
雖說應該是在class constructor
中定義自己的一些屬性,但是class
允許你在原型上通過set&get
來定義獲取屬性。
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
他類似下面這種無class的情況:
// direct equivalent to previous example
let CustomHTMLElement = (function() {
"use strict";
const CustomHTMLElement = function(element) {
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;
}());
可以發現,最終都是在Object.defineProperty
中處理。
Generator 方法
class
內部的方法是支援generator
方法的。
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) {
console.log(x);
}
對於generator
和iterator
不瞭解的,可在此瞭解
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);
};
var person = PersonType.create("xhs");
現在在es6
中只需要新增關鍵字static
即可:
class PersonClass {
// equivalent of the PersonType constructor
constructor(name) {
this.name = name;
}
// equivalent of PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// equivalent of PersonType.create
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create("xhs");
派生繼承
在es6
之前,實現一個繼承是有多種方式,適當的繼承有以下步驟:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value:Square,
enumerable: true,
writable: true,
configurable: true
}
});
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
Square
繼承自Rectangle
,這使得Square.prototype
需繼承自Rectangle.prototype
,並且呼叫到new Rectangle
(Rectangle.call(this, length, length)
),這經常會迷惑一些新手。
所以出現了es6
的繼承,他使得更加容易瞭解.
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// same as Rectangle.call(this, length, length)
super(length, length);
}
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
直接通過extends
來繼承,子類中通過super
來呼叫父類的建構函式,並傳遞引數。
這樣從其他類繼承的類稱為派生類,派生類在出現的constructor
中需要指定super()
,否則會出錯。如果不出現 constructor
,則預設會新增constructor
.
使用
super()
的時候,需要記住下面這幾點
1. 你只可以在派生類(extends
)中使用super()
,否則會出錯。
2.constructor
中的super()
使用必須在this
之前使用,因為他負責一些初始化,所以在此之前使用this
會出錯。
3. 派生類中避免使用super()
的唯一方法是在constructor
返回一個物件(非原始型別)。
class
的影子方法
這個類似於原型鏈的property
,因為派生類是繼承的,所以可能存在同名的方法。
具體的關於shadowing property
繼承靜態成員
這個就類似派生繼承裡的方法,也可以被繼承。
表示式派生的類
只要一個表示式內部存在[[Constructor]]
並且有prototype
,那就可以被extends
.
看下面這個例子:
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"
他仍然可以工作,因為mixin
方法返回的是一個function
.滿足[[Constructor]]
和prototype
的要求。可以發現這裡例子中,雖然基類是空的,但是仍然使用了super()
,否則報錯. 如果mixin
中有多個相同的prototype
,則以最後一個為準。
extends
後面可以使用任何的表示式,但是並不是所有的表示式都會生成有效的類。有這些情況是不可以的。
- null
- generator function
在這些情況下,嘗試使用new
去例項化一個物件,會報錯,因為這些內部不存在[[Constructor]]
繼承內部的屬性
自從陣列存在,開發者幾乎都想通過繼承定製自己的陣列型別,在es5
及更早之前的版本,這幾乎是不可能的。使用經典繼承並不會使程式碼正常執行。
例如:
// 內建的陣列行為
var colors = [];
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
// es5中嘗試陣列繼承
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0
colors.length = 0;
console.log(colors[0]); // "red"
可以發現,這個是不能繼承內部的屬性。es6
的一個目標就是繼承內部的屬性方法。因此es6 class
的繼承和es5
的經典繼承略有不同:
ES5
的經典繼承首先呼叫的是派生類中的this
,然後基類的建構函式再被呼叫,這就意味著this
是作為派生類的第一個例項開始。基類的其他屬性進行修飾
ES6
的class
卻是恰恰相反:
ES6
的class
繼承,this
首先是由基類來建立,後面通過派生類的建構函式來改變。這樣才會導致開始就是由基類內建的功能來接收所有的功能
再來看看下面的例子:
class MyArray extends Array {
// empty
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
這樣就會完全繼承Array
的內建功能。
Symbol.species屬性
extends
一個有趣的事情就是任何繼承了內建的功能,最終返回。內建的例項都會自動返回到派生類的例項。例如上面的MyArray
繼承自Array
,像slice
這樣返回的是MyArray
這個派生類的例項。
class MyArray extends Array {
// empty
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof MyArray); // true
在上面的程式碼中,MyArray
例項返回slice()
方法.正常情況下, slice()
方法繼承自Array
並且返回Array
的例項。實際上是Symbol.species
在幕後進行改變。
Symbol.species