JS6-Class
====Class(類)
Js中,生成實例對象的傳統方法是通過構造函數
function Point(x, y) {
this.x = x;
this.y = y;
}
var p = new Point(1, 2);
上面這種寫法跟傳統的面向對象語言(比如 C++ 和 Java)差異很大
ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為對象的模板。通過class關鍵字,可以定義類。
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘;
}
}
//創建對象
var p=new Point(1,2)
console.log(p.x)
上面代碼定義了一個“類”,可以看到裏面有一個constructor方法,這就是構造方法,而this關鍵字則代表實例對象。也就是說,ES5 的構造函數Point,對應 ES6 的Point類的構造方法。
Point類除了構造方法,還定義了一個toString方法。註意,定義“類”的方法的時候,前面不需要加上function這個關鍵字,直接把函數定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。
ES6 的類,完全可以看作構造函數的另一種寫法。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代碼表明,類的數據類型就是函數,類本身就指向構造函數。
使用的時候,也是直接對類使用new命令,跟構造函數的用法完全一致。
class Bar {
doStuff() {
console.log(‘stuff‘);
}
}
var b = new Bar();
b.doStuff() // "stuff"
構造函數的prototype屬性,在 ES6 的“類”上面繼續存在。事實上,類的所有方法都定義在類的prototype屬性上面。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同於
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
在類的實例上面調用方法,其實就是調用原型上的方法。
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true
上面代碼中,b是B類的實例,它的constructor方法就是B類原型的constructor方法。
由於類的方法都定義在prototype對象上面,所以類的新方法可以添加在prototype對象上面。Object.assign方法可以很方便地一次向類添加多個方法。
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
類必須使用new調用,否則會報錯。這是它跟普通構造函數的一個主要區別,ES5不用new也可以執行。
class Foo {
constructor() {
return 666;
}
}
Foo()// TypeError: Class constructor Foo cannot be invoked without ‘new‘
類的實例對象
生成類的實例對象的寫法,與 ES5 完全一樣,也是使用new命令。如果忘記加上new,像函數那樣調用Class,將會報錯。
class Point {
// ...
}
// 報錯
var point = Point(2, 3);
// 正確
var point = new Point(2, 3);
與 ES5 一樣,類的所有實例共享一個原型對象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__//true
上面代碼中,p1和p2都是Point的實例,它們的原型都是Point.prototype,所以__proto__屬性是相等的。
這也意味著,可以通過實例的__proto__屬性為“類”添加方法。
__proto__ 並不是語言本身的特性,這是各大廠商具體實現時添加的私有屬性,雖然目前很多現代瀏覽器的JS引擎中都提供了這個屬性,但不建議在生產中使用該屬性:
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return ‘Oops‘ };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
上面代碼在p1的原型上添加了一個printName方法,由於p1的原型就是p2的原型,因此p2也可以調用這個方法。而且,此後新建的實例p3也可以調用這個方法。這意味著,使用實例的__proto__屬性改寫原型,必須相當謹慎,不推薦使用,因為這會改變“類”的原始定義,影響到所有實例。
Class 表達式
與函數一樣,類也可以使用表達式的形式定義。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代碼使用表達式定義了一個類。需要註意的是,這個類的名字是MyClass而不是Me,Me只在 Class 的內部代碼可用,指代當前類。
let inst = new MyClass();
Me.name // ReferenceError: Me is not defined
上面代碼表示,Me只在 Class 內部有定義。
類也可以寫成下面的表達式形式。
const MyClass = class { };
采用 Class 表達式,可以寫出立即執行的 Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}(‘張三‘);
person.sayName(); // "張三"
上面代碼中,person是一個立即執行的類的實例。
不存在變量提升
類不存在變量提升,與 ES5 完全不同。
new Foo(); // ReferenceError
class Foo {}
上面代碼中,Foo類使用在前,定義在後,這樣會報錯,因為 ES6 不會把類的聲明提升到代碼頭部。這種規定的原因與繼承有關,必須保證子類在父類之後定義。
{
let Foo = class {};
class Bar extends Foo {
}
}
上面的代碼不會報錯,因為Bar繼承Foo的時候,Foo已經有定義了。但是,如果存在class的提升,上面代碼就會報錯,因為class會被提升到代碼頭部,而let命令是不提升的,所以導致Bar繼承Foo的時候,Foo還沒有定義。
this 的指向
類的方法內部如果含有this,它默認指向類的實例。但是,必須非常小心,一旦單獨使用該方法,很可能報錯。
class Logger {
printName(name = ‘there‘) {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property ‘print‘ of undefined
上面代碼中,printName方法中的this,默認指向Logger類的實例。但是,如果將這個方法提取出來單獨使用,this會指向該方法運行時所在的環境,因為找不到print方法而導致報錯。
解決方法:寫在構造函數constructor裏。
class Logger {
constructor() {
this.printName = (name = ‘there‘) => {
this.print(`Hello ${name}`);
};
}
}
Class 的取值函數(getter)和存值函數(setter)
在“類”的內部可以使用get和set關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行為。
class MyClass {
constructor() {
// ...
}
get prop() {
return ‘getter‘;
}
set prop(value) {
console.log(‘setter: ‘+value);
}
}
let inst = new MyClass();
inst.prop = 123;// setter: 123
inst.prop// ‘getter‘
上面代碼中,prop屬性有對應的存值函數和取值函數,因此賦值和讀取行為都被自定義了。
Class 的靜態方法
類相當於實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接通過類來調用,這就稱為“靜態方法”。
class Foo {
static classMethod() {
return ‘hello‘;
}
}
Foo.classMethod() // ‘hello‘
var foo = new Foo();
foo.classMethod()// TypeError: foo.classMethod is not a function
上面代碼中,Foo類的classMethod方法前有static關鍵字,表明該方法是一個靜態方法,可以直接在Foo類上調用(Foo.classMethod()),而不是在Foo類的實例上調用。如果在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。
父類的靜態方法,可以被子類繼承。
class Foo {
static classMethod() {
return ‘hello‘;
}
}
class Bar extends Foo {
}
Bar.classMethod() // ‘hello‘
上面代碼中,父類Foo有一個靜態方法,子類Bar可以調用這個方法。
Class 的靜態屬性和實例屬性
靜態屬性指的是 Class 本身的屬性,即Class.propName,而不是定義在實例對象(this)上的屬性。
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
上面的寫法為Foo類定義了一個靜態屬性prop。
目前,只有這種寫法可行,因為 ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。
// 以下兩種寫法都無效
class Foo {
// 寫法一
prop: 2
// 寫法二
static prop: 2
}
Foo.prop // undefined
目前有一個靜態屬性的提案,對實例屬性和靜態屬性都規定了新的寫法:
類的實例屬性
類的實例屬性可以用等式,寫入類的定義之中。
以前,我們定義實例屬性,只能寫在類的constructor方法裏面。有了新的寫法以後,可以不在constructor方法裏面定義。
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
上面代碼中,myProp就是MyClass的實例屬性。在MyClass的實例上,可以讀取這個屬性。
這種寫法比以前更清晰:
class Person{
it= {
count: 3
};
}
為了可讀性的目的,對於那些在constructor裏面已經定義的實例屬性,新寫法允許直接列出:
class Person{
state;
constructor() {
this.state = {
count: 0
};
}
}
類的靜態屬性
類的靜態屬性只要在上面的實例屬性寫法前面,加上static關鍵字就可以了。這個新寫法大大方便了靜態屬性的表達。
// 老寫法
class Foo {
// ...
}
Foo.prop = 1;
// 新寫法
class Foo {
static prop = 1;
}
上面代碼中,老寫法的靜態屬性定義在類的外部。整個類生成以後,再生成靜態屬性。這樣讓人很容易忽略這個靜態屬性,也不符合相關代碼應該放在一起的代碼組織原則。
new.target屬性
new是從構造函數生成實例的命令。ES6 為new命令引入了一個new.target屬性,該屬性一般用在在構造函數之中,返回new命令作用於的那個構造函數。如果構造函數不是通過new命令調用的,new.target會返回undefined,因此這個屬性可以用來確定構造函數是怎麽調用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error(‘必須使用new生成實例‘);
}
}
// 另一種寫法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error(‘必須使用 new 生成實例‘);
}
}
var person = new Person(‘張三‘); // 正確
var notAPerson = Person(‘張三‘); // 報錯
上面代碼確保構造函數只能通過new命令調用。
JS6-Class