深入理解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
是用來定義返回函式的一個靜態訪問器屬性,這個返回的函式是每當需要在例項方法內建立例項的時候使用到的建構函式(而不是直接使用建構函式)。
以下的內建型別定義了Symbol.species
:
- Array
- ArrayBuffer
- Map
- Promise
- Set
- RegExp
- Typed Arrays
上面的每一個都有預設的Symbol.species
,他返回this
,意味著該屬性始終返回建構函式。
我們來定義一個帶有Symbol.species
的類
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
複製程式碼
可以發現上面這段程式碼,有個靜態的訪問器屬性,而且也可以看到上面只有getter
,並沒有setter
,因為要修改內建的型別,這是不可能的。 所有呼叫this.constructor[Symbol.species]
的都會返回派生類 MyClass
. 如clone
呼叫了,並且返回了一個新的例項。 再看下面的例子:
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {
// empty
}
class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1("foo"),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2("bar"),
clone2 = instance2.clone();
console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false
複製程式碼
在上面的程式碼中:
MyDerivedClass1
繼承自MyClass
並且沒有改變Symbol.species
屬性, 返回了MyDerivedClass1
的例項。MyDerivedClass2
繼承自MyClass
並且改變了Symbol.species
屬性返回MyClass
.當MyDerivedClass2
例項呼叫clone
方法的時候,返回的是MyClass
的例項. 使用Symbol.species
,任何派生類都可以確定方法返回例項時返回的值的型別。
例如,Array
使用Symbol.species
指定用於返回陣列的方法的類。在從Array
派生的類中,可以確定從繼承方法返回的物件型別。如下:
class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof Array); // true
console.log(subitems instanceof MyArray); // false
複製程式碼
上面的程式碼是重寫了Symbol.species
,他繼承自Array
.所有繼承的陣列的方法,這樣使用的就是Array
的例項,而不是MyArray
的例項.
通常情況下,要想在類方法中使用this.constructor
方法,就應該使用Symbol.species
屬性.
類的建構函式中使用new.target
你可以在類的建構函式中使用new.target
去確定class
是如何被呼叫的。一些簡單的情況之下,new.target
等於方法或者類的建構函式.
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
// new.target is Rectangle
var obj = new Rectangle(3, 4); // outputs true
複製程式碼
因為class
呼叫必須使用new
,所以這種情況下就等於Rectangle(constructor name)
. 但是值卻不總是一樣,如下:
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
// new.target is Square
var obj = new Square(3); // outputs false
複製程式碼
可以發現,這裡就不是Rectangle
了,而是Square
.這個很重要,他可以根據呼叫方式來判斷當前的target
. 基於上面這點,我們就可以定義一個不可以被例項化的基類。例如:
// abstract base class
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("This class cannot be instantiated directly.")
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
var x = new Shape(); // throws error
var y = new Rectangle(3, 4); // no error
console.log(y instanceof Shape); // true
複製程式碼
注意: 因為
class
必須使用new
呼叫,因此new.target
在建構函式中永遠不可能是undefined
作者:xiaohesong
連結:https://juejin.im/post/5b9b068ae51d450e6c749f32
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。