類
類是用於建立物件的模板。JavaScript中生成物件例項的方法是通過建構函式,這跟主流面嚮物件語言(java,C#)寫法上差異較大,如下:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 1);
ES6 提供了更接近Java語言的寫法,引入了 Class(類)這個概念,作為物件的模板。通過class
關鍵字,可以定義類。
如下:constructor()
是構造方法,而this
代表例項物件:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
類的資料型別就是函式,它本身就是指向函式的建構函式:
// ES5 函式宣告
function Point() {
//...
}
// ES6 類宣告
class Point {
//....
constructor() {
}
}
typeof Point // "function"
Point === Point.prototype.constructor // true
在類裡面定義的方法是掛到Point.prototype
,所以類只是提供了語法糖,本質還是原型鏈呼叫。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
Point.prototype = {
//....
toString()
}
var p = new Point(1, 1);
p.toString() // (1,1)
類的另一種定義方式類表示式
// 未命名/匿名類
let Point = class {
constructor(x, y) {
this.x = x;
this.y = y;
}
};
Point.name // Point
函式宣告和類宣告有個重要區別,函式宣告會提升,類宣告不會提升。
let p = new Point(); // 被提升不會報錯 function Point() {} let p = new Point(); // 報錯,ReferenceError class Point {}
constructor()
constructor()
方法是類的預設方法,new
生成例項物件時會自動呼叫該方法。
一個類必須有constructor()
方法,如果沒有顯式定義,引擎會預設新增一個空的constructor()
。
constructor()
方法預設返回例項物件(即this
)。
class Point {
}
// 自動新增
class Point {
constructor() {}
}
getter和setter
與 ES5 一樣,在類的內部可以使用get
和set
關鍵字,對某個屬性設定存值函式和取值函式,攔截該屬性的存取行為。
class User {
constructor(name) {
this.name = name;
}
get name() {
return this.name;
}
set name(value) {
this.name = value;
}
}
this
類的方法內部的this
,它預設指向類的例項,在呼叫存在this
的方法時,需要使用 obj.method()
方式,否則會報錯。
class User {
constructor(name) {
this.name = name;
}
printName(){
console.log('Name is ' + this.name)
}
}
const user = new User('jack')
user.printName() // Name is jack
const { printName } = user;
printName() // 報錯 Cannot read properties of undefined (reading 'name')
如果要單獨呼叫又不報錯,一種方法可以在構造方法裡呼叫bind(this)
。
class User {
constructor(name) {
this.name = name;
this.printName = this.printName.bind(this);
}
printName(){
console.log('Name is ' + this.name)
}
}
const user = new User('jack')
const { printName } = user;
printName() // Name is jack
bind(this)
會建立一個新函式,並將傳入的this
作為該函式在呼叫時上下文指向。
另外可以使用箭頭函式,因為箭頭函式內部的this
總是指向定義時所在的物件。
class User {
constructor(name) {
this.name = name;
}
printName = () => {
console.log('Name is ' + this.name)
}
}
const user = new User('jack')
const { printName } = user;
printName() // Name is jack
靜態屬性
靜態屬性指的是類本身的屬性,而不是定義在例項物件this
上的屬性。
class User {
}
User.prop = 1;
User.prop // 1
靜態方法
可以在類裡面定義靜態方法,該方法不會被物件例項繼承,而是直接通過類來呼叫。
靜態方法裡使用this
是指向類。
class Utils {
static printInfo() {
this.info();
}
static info() {
console.log('hello');
}
}
Utils.printInfo() // hello
關於方法的呼叫範圍限制,比如:私有公有,ES6暫時沒有提供,一般是通過約定,比如:在方法前面加下劃線_print()
表示私有方法。
繼承
Java中通過extends
實現類的繼承。ES6中類也可以通過extends
實現繼承。
繼承時,子類必須在constructor
方法中呼叫super
方法,否則新建例項時會報錯。
class Point3D extends Point {
constructor(x, y, z) {
super(x, y); // 呼叫父類的constructor(x, y)
this.z = z;
}
toString() {
return super.toString() + ' ' + this.z ; // 呼叫父類的toString()
}
}
父類的靜態方法,也會被子類繼承。
class Parent {
static info() {
console.log('hello world');
}
}
class Child extends Parent {
}
Child.info() // hello world
super關鍵字
在子類的建構函式必須執行一次super
函式,它代表了父類的建構函式。
class Parent {}
class Child extends Parent {
constructor() {
super();
}
}
在子類普通方法中通過super
呼叫父類的方法時,方法內部的this
指向當前的子類例項。
class Parent {
constructor() {
this.x = 1;
this.y = 10
}
printParent() {
console.log(this.y);
}
print() {
console.log(this.x);
}
}
class Child extends Parent {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let c = new Child();
c.printParent() // 10
c.m() // 2
_proto_
和prototype
初學JavaScript時,_proto_
和prototype
很容易混淆。首先我們知道每個JS物件都會對應一個原型物件,並從原型物件繼承屬性和方法。
prototype
一些內建物件和函式的屬性,它是一個指標,指向一個物件,這個物件的用途就是包含所有例項共享的屬性和方法(我們把這個物件叫做原型物件)。_proto_
每個物件都有這個屬性,一般指向對應的建構函式的prototype
屬性。
下圖是一些擁有prototype
內建物件。
根據上面描述,看下面程式碼
var obj = {} // 等同於 var obj = new Object()
// obj.__proto__指向Object建構函式的prototype
obj.__proto__ === Object.prototype // true
// obj.toString 呼叫方法從Object.prototype繼承
obj.toString === obj.__proto__.toString // true
// 陣列
var arr = []
arr.__proto__ === Array.prototype // true
對於function物件,宣告的每個function同時擁有prototype
和__proto__
屬性,建立的物件屬性__proto__
指向函式prototype
,函式的__proto__
又指向內建函式物件(Function)的prototype
。
function Foo(){}
var f = new Foo();
f.__proto__ === Foo.prototype // true
Foo.__proto__ === Function.prototype // true
繼承中的__proto__
類作為建構函式的語法糖,也會同時有prototype
屬性和__proto__
屬性,因此同時存在兩條繼承鏈。
- 子類的
__proto__
屬性,表示建構函式的繼承,總是指向父類。 - 子類
prototype
屬性的__proto__
屬性,表示方法的繼承,總是指向父類的prototype
屬性。
class Parent {
}
class Child extends Parent {
}
Child.__proto__ === Parent // true
Child.prototype.__proto__ === Parent.prototype // true
繼承例項中的__proto__
子類例項的__proto__
屬性,指向子類構造方法的prototype
。
子類例項的__proto__
屬性的__proto__
屬性,指向父類例項的__proto__
屬性。也就是說,子類的原型的原型,是父類的原型。
class Parent {
}
class Child extends Parent {
}
var p = new Parent();
var c = new Child();
c.__proto__ === p.__proto__ // false
c.__proto__ === Child.prototype // true
c.__proto__.__proto__ === p.__proto__ // true
小結
JavaScript中的Class更多的還是語法糖,本質上繞不開原型鏈。歡迎大家留言交流。