class基本聲明
在說(shuō)class
之前宵喂,想必大家肯定會(huì)想到constructor function
. 看下面代碼:
function Foo(name) {
this.name = name
}
class Bar {
constructor(name){
this.name = name
}
}
f = new Foo('xhs')
b = new Bar('xhs')
兩個(gè)差不多吧缸血,foo function
是在new
的時(shí)候,把this
指向當(dāng)前的新創(chuàng)建的空對(duì)象,并且會(huì)把進(jìn)行屬性分配岩调。bar class
是在constructor
里進(jìn)行接收參數(shù)炒俱。
但是兩個(gè)還是 有些不同
-
class
聲明并不像function
聲明盐肃,他不存在提升。他類似let
聲明权悟,存在TDZ(temporal dead zone)
砸王。 -
class
中的代碼都會(huì)自動(dòng)的使用嚴(yán)格模式,沒(méi)辦法選擇峦阁。 - 所有的方法都是不可枚舉的(
non-enumerable
), 注:非綁定當(dāng)前對(duì)象的方法谦铃。 -
class
內(nèi)所有方法內(nèi)部都缺少[[Construct]]
方法,所以如果對(duì)這些方法進(jìn)行new
會(huì)出錯(cuò)榔昔。 - 不攜帶
new
操作符調(diào)用class
會(huì)報(bào)錯(cuò)驹闰。 - 嘗試在類的方法中改變類名會(huì)出錯(cuò)。
考慮到上面這幾點(diǎn)撒会,下面來(lái)看一個(gè)等價(jià)的例子:
class PersonClass {
// equivalent of the PersonType constructor
constructor(name) {
this.name = name;
}
// equivalent of PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}
上面的代碼將等價(jià)下面無(wú)class
的語(yǔ)法
// 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;
}());
我們來(lái)分析上面這個(gè)無(wú)class
語(yǔ)法的代碼段嘹朗。
首先注意到這里有兩個(gè)PersonType2
的聲明(let
聲明在作用域外面,const
在IIFE
里)诵肛,這個(gè)就是禁止類方法覆蓋類名屹培。
在構(gòu)造方法里有new.target
來(lái)檢測(cè)確保通過(guò)new
調(diào)用,與之相對(duì)的是對(duì)方法的檢測(cè)怔檩,排除new
方法調(diào)用的可能褪秀,否則拋錯(cuò)。在下面就是enumerable: false
薛训,最后返回這個(gè)構(gòu)造函數(shù).
雖然上面的代碼可以實(shí)現(xiàn)class
的效果媒吗,但是明顯,class
更加簡(jiǎn)潔方便许蓖。
類的常量名稱蝴猪。
常量是不可被改變的调衰,否則就會(huì)報(bào)錯(cuò)。類的名稱只是在內(nèi)部使用const
,這就意味著在內(nèi)部不可以改變名稱自阱,外面卻可以嚎莉。
class Foo {
constructor() {
Foo = "bar"; // 執(zhí)行的時(shí)候報(bào)錯(cuò)。
}
}
// 這里不會(huì)報(bào)錯(cuò)沛豌。
Foo = "baz";
class表達(dá)式
class
和function
類似趋箩,也可以使用表達(dá)式。
let PersonClass = class {
// equivalent of the FunctionName constructor
constructor(name) {
this.name = name;
}
// equivalent of FunctionName.prototype.sayName
sayName() {
console.log(this.name);
}
};
可以發(fā)現(xiàn)加派,表達(dá)式語(yǔ)法類似叫确,使用class
的表達(dá)式還是聲明都只是風(fēng)格的不同,不像構(gòu)造函數(shù)的聲明和表達(dá)式有著提升的區(qū)別芍锦。
當(dāng)然竹勉,上面的表達(dá)式是一個(gè)匿名表達(dá)式,我們可以創(chuàng)建一個(gè)攜帶名稱的表達(dá)式娄琉。
let PersonClass = class PersonClass2 {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
};
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass2); // undefined
可以發(fā)現(xiàn)上面輸出PersonClass2
是未定義次乓,因?yàn)樗挥写嬖陬惗x中, 如需了解,我們做下面的一個(gè)轉(zhuǎn)變:
// 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;
}());
這個(gè)轉(zhuǎn)變與上面的class
聲明略有不同孽水,class
聲明的時(shí)候票腰,內(nèi)部與外部的名稱相同,但是在class
表達(dá)式 中女气,卻不同杏慰。
Classes
第一等公民
在編程世界中,當(dāng)某個(gè)東西可以作為一個(gè)值使用時(shí)炼鞠,這意味著它可以被傳遞到函數(shù)中缘滥,從函數(shù)返回,可以分配給變量簇搅,它被認(rèn)為是一等的公民完域。所以在javascript
中,function
是第一等公民.
ES6
中使用class
沿用了這一傳統(tǒng)瘩将,所以class
有很多方式去使用它吟税,下面來(lái)看將他作為一個(gè)參數(shù):
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
});
obj.sayHi(); // "Hi!"
class
有一個(gè)有意思的是使用立即執(zhí)行來(lái)創(chuàng)建單例
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("xhs");
person.sayName(); // "xhs"
這樣就創(chuàng)建了一個(gè)單例。
訪問(wèn)的屬性
雖說(shuō)應(yīng)該是在class constructor
中定義自己的一些屬性姿现,但是class
允許你在原型上通過(guò)set&get
來(lái)定義獲取屬性肠仪。
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
他類似下面這種無(wú)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;
}());
可以發(fā)現(xiàn),最終都是在Object.defineProperty
中處理备典。
Generator 方法
class
內(nèi)部的方法是支持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);
}
對(duì)于generator
和iterator
不了解的,可在此了解
Static 成員
在es6
之前提佣,使用靜態(tài)方法需要像下面這般處理:
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");
現(xiàn)在在es6
中只需要添加關(guān)鍵字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
之前吮蛹,實(shí)現(xiàn)一個(gè)繼承是有多種方式荤崇,適當(dāng)?shù)睦^承有以下步驟:
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
,并且調(diào)用到new Rectangle
(Rectangle.call(this, length, length)
),這經(jīng)常會(huì)迷惑一些新手。
所以出現(xiàn)了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
直接通過(guò)extends
來(lái)繼承术荤,子類中通過(guò)super
來(lái)調(diào)用父類的構(gòu)造函數(shù),并傳遞參數(shù)每篷。
這樣從其他類繼承的類稱為派生類瓣戚,派生類在出現(xiàn)的constructor
中需要指定super()
,否則會(huì)出錯(cuò)。如果不出現(xiàn) constructor
,則默認(rèn)會(huì)添加constructor
.
使用
super()
的時(shí)候焦读,需要記住下面這幾點(diǎn)
1. 你只可以在派生類(extends
)中使用super()
,否則會(huì)出錯(cuò)子库。
2.constructor
中的super()
使用必須在this
之前使用,因?yàn)樗?fù)責(zé)一些初始化矗晃,所以在此之前使用this
會(huì)出錯(cuò)仑嗅。
3. 派生類中避免使用super()
的唯一方法是在constructor
返回一個(gè)對(duì)象(非原始類型)。
class
的影子方法
這個(gè)類似于原型鏈的property
,因?yàn)榕缮愂抢^承的喧兄,所以可能存在同名的方法无畔。
具體的關(guān)于shadowing property
繼承靜態(tài)成員
這個(gè)就類似派生繼承里的方法,也可以被繼承吠冤。
表達(dá)式派生的類
只要一個(gè)表達(dá)式內(nèi)部存在[[Constructor]]
并且有prototype
,那就可以被extends
.
看下面這個(gè)例子:
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}"
他仍然可以工作,因?yàn)?code>mixin方法返回的是一個(gè)function
.滿足[[Constructor]]
和prototype
的要求恭理≌蓿可以發(fā)現(xiàn)這里例子中,雖然基類是空的颜价,但是仍然使用了super()
涯保,否則報(bào)錯(cuò). 如果mixin
中有多個(gè)相同的prototype
,則以最后一個(gè)為準(zhǔn)。
extends
后面可以使用任何的表達(dá)式周伦,但是并不是所有的表達(dá)式都會(huì)生成有效的類夕春。有這些情況是不可以的。
- null
- generator function
在這些情況下专挪,嘗試使用new
去實(shí)例化一個(gè)對(duì)象及志,會(huì)報(bào)錯(cuò),因?yàn)檫@些內(nèi)部不存在[[Constructor]]
繼承內(nèi)部的屬性
自從數(shù)組存在寨腔,開(kāi)發(fā)者幾乎都想通過(guò)繼承定制自己的數(shù)組類型速侈,在es5
及更早之前的版本,這幾乎是不可能的迫卢。使用經(jīng)典繼承并不會(huì)使代碼正常運(yùn)行倚搬。
例如:
// 內(nèi)置的數(shù)組行為
var colors = [];
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
// es5中嘗試數(shù)組繼承
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"
可以發(fā)現(xiàn),這個(gè)是不能繼承內(nèi)部的屬性乾蛤。es6
的一個(gè)目標(biāo)就是繼承內(nèi)部的屬性方法每界。因此es6 class
的繼承和es5
的經(jīng)典繼承略有不同:
ES5
的經(jīng)典繼承首先調(diào)用的是派生類中的this
,然后基類的構(gòu)造函數(shù)再被調(diào)用捅僵,這就意味著this
是作為派生類的第一個(gè)實(shí)例開(kāi)始≌2悖基類的其他屬性進(jìn)行修飾
ES6
的class
卻是恰恰相反:
ES6
的class
繼承命咐,this
首先是由基類來(lái)創(chuàng)建,后面通過(guò)派生類的構(gòu)造函數(shù)來(lái)改變谐岁。這樣才會(huì)導(dǎo)致開(kāi)始就是由基類內(nèi)置的功能來(lái)接收所有的功能
再來(lái)看看下面的例子:
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
這樣就會(huì)完全繼承Array
的內(nèi)置功能醋奠。
Symbol.species屬性
extends
一個(gè)有趣的事情就是任何繼承了內(nèi)置的功能,最終返回伊佃。內(nèi)置的實(shí)例都會(huì)自動(dòng)返回到派生類的實(shí)例窜司。例如上面的MyArray
繼承自Array
,像slice
這樣返回的是MyArray
這個(gè)派生類的實(shí)例航揉。
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
實(shí)例返回slice()
方法.正常情況下, slice()
方法繼承自Array
并且返回Array
的實(shí)例。實(shí)際上是Symbol.species
在幕后進(jìn)行改變帅涂。
Symbol.species
是用來(lái)定義返回函數(shù)的一個(gè)靜態(tài)訪問(wèn)器屬性议薪,這個(gè)返回的函數(shù)是每當(dāng)需要在實(shí)例方法內(nèi)創(chuàng)建實(shí)例的時(shí)候使用到的構(gòu)造函數(shù)(而不是直接使用構(gòu)造函數(shù))。
以下的內(nèi)置類型定義了Symbol.species
:
- Array
- ArrayBuffer
- Map
- Promise
- Set
- RegExp
- Typed Arrays
上面的每一個(gè)都有默認(rèn)的Symbol.species
,他返回this
媳友,意味著該屬性始終返回構(gòu)造函數(shù)斯议。
我們來(lái)定義一個(gè)帶有Symbol.species
的類
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
可以發(fā)現(xiàn)上面這段代碼,有個(gè)靜態(tài)的訪問(wèn)器屬性醇锚,而且也可以看到上面只有getter
,并沒(méi)有setter
,因?yàn)橐薷膬?nèi)置的類型哼御,這是不可能的。
所有調(diào)用this.constructor[Symbol.species]
的都會(huì)返回派生類 MyClass
. 如clone
調(diào)用了焊唬,并且返回了一個(gè)新的實(shí)例恋昼。
再看下面的例子:
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
并且沒(méi)有改變Symbol.species
屬性, 返回了MyDerivedClass1
的實(shí)例。 -
MyDerivedClass2
繼承自MyClass
并且改變了Symbol.species
屬性返回MyClass
.當(dāng)MyDerivedClass2
實(shí)例調(diào)用clone
方法的時(shí)候赶促,返回的是MyClass
的實(shí)例.
使用Symbol.species
液肌,任何派生類都可以確定方法返回實(shí)例時(shí)返回的值的類型。
例如鸥滨,Array
使用Symbol.species
指定用于返回?cái)?shù)組的方法的類嗦哆。在從Array
派生的類中,可以確定從繼承方法返回的對(duì)象類型爵赵。如下:
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
上面的代碼是重寫(xiě)了Symbol.species
,他繼承自Array
.所有繼承的數(shù)組的方法吝秕,這樣使用的就是Array
的實(shí)例,而不是MyArray
的實(shí)例.
通常情況下空幻,要想在類方法中使用this.constructor
方法烁峭,就應(yīng)該使用Symbol.species
屬性.
類的構(gòu)造函數(shù)中使用new.target
你可以在類的構(gòu)造函數(shù)中使用new.target
去確定class
是如何被調(diào)用的。一些簡(jiǎn)單的情況之下,new.target
等于方法或者類的構(gòu)造函數(shù).
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
因?yàn)?code>class調(diào)用必須使用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
可以發(fā)現(xiàn)缩挑,這里就不是Rectangle
了,而是Square
.這個(gè)很重要鬓梅,他可以根據(jù)調(diào)用方式來(lái)判斷當(dāng)前的target
.
基于上面這點(diǎn)供置,我們就可以定義一個(gè)不可以被實(shí)例化的基類。例如:
// 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
注意: 因?yàn)?code>class必須使用
new
調(diào)用绽快,因此new.target
在構(gòu)造函數(shù)中永遠(yuǎn)不可能是undefined