一、JS 要變 Java 了!?
ES6 啟用了 class、constructor、static、extends、super 等關鍵字來支持類定義,感覺是馬上要變 Java 了,終於不用管 prototype 了嗎?
不是這樣的。啟用這一套關鍵字僅僅是為了減少「語法噪音」,減少定義類結構時的工作量,JS 基於原型的(prototype-based)對象系統無法輕易改成基於類的(class-based),這是 JS 語言設計者的選擇,不可能把現有的對象系統連根拔起,再把 Java 那一套塞進來縫合好。
P.S.「語法噪音」是從老趙那搬過來的詞,用 Python 和 Java 實現同樣的功能,diff 結果就是「語法噪音」,比如 JS 中的 function、prototype 等等輸入起來很難受的東西
而且,基於原型的對象系統相對靈活,比如 JS 可以在運行時動態修改類繼承樹,因此我們可以只預先定義好空的類繼承樹,需要時才去增添類成員,例如:
// 空的繼承樹
var SuperType = function() {};
var SubType = function() {};
SubType.prototype = new SuperType();
// 動態增強
void function() {
//...
SuperType.prototype.cl = console.log.bind(console);
new SubType().cl('invoked by subType'); // invoked by subType
}();
這種感覺很奇妙,Java 和 JS 一起畫畫,Java 用精細的刻刀在畫布上一絲不苟地完成了清明上河圖,裝裱好掛在牆上,四下驚艷。JS 拿起水筆畫了兩條彎彎扭扭的橫線,說「我畫完了,這就是清明上河圖」,觀眾感到被愚弄了,但也勉強把 JS 的作品裝裱好,掛在 Java 的大作旁邊。Java 束手而立靜待分曉,這時 JS 才開始忙活,拿起筆認真地在相框玻璃面上完成了一模一樣的作品,觀眾感到不可思議。突然人群裡傳出一個聲音,「左邊第三個房子應該是尖頂的,你們都畫錯了」,Java 臉上掛不住了,急匆匆地攤開另一張宣紙,想要重新畫一幅改掉那個礙眼的錯誤。JS 指著左上角說「是這裡嗎?我已經改好了」
JS 是命令式語言、動態語言和函數式語言的結合體,就整個體系而言,對象系統需要基於原型實現帶來的這種靈活性,不用羨慕 Java 精雕細刻的基於類的對象系統,完全不合身。ES 規範設計者不會也沒有理由去照搬 Java,至此,JavaScript 與 Java 仍然毫不相干,像 20 年前一樣
P.S. JS 和筆者都是 1995 年誕生的,至於 Netscape 與 JavaScript 和 Java Applet 的絲絲縷縷,請自行查找
二、class 帶來的新特性
ES6 之前的「class」可能是這樣的:
function Circle(radius) {
if (typeof radius !== 'number') {
throw new TypeError('radius: a number expected');
}
// 實例屬性
this.radius = radius;
// 靜態屬性
Circle.count++;
}
Circle.count = 0;
// 原型屬性
Circle.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
new Circle 得到的每個實例都具有兩個屬性,實例屬性 radius 和原型屬性 getArea
偶爾能看到用 ES5 特性實現的更嚴謹的「class」:
// 更嚴謹的,定義 getter/setter
function CircleEx(radius) {
this.radius = radius;
CircleEx.count++;
}
// 定義靜態屬性
Object.defineProperty(CircleEx, 'count', {
get: function() {
// this 指向 CircleEx
// console.log(this);
// CircleEx.count++ 先 get 返回 0
// 再 set 給 CircleEx 添上_count 屬性並賦值為 1
return !this._count ? 0 : this._count;
},
set: function(val) {
this._count = val;
}
});
CircleEx.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
// 定義原型屬性
// 實際上是為實例屬性定義 getter/setter
Object.defineProperty(CircleEx.prototype, 'radius', {
get: function() {
//!!! this 指向 CircleEx 實例
// 而不是其原型對象
// console.log(this);
// 構造函數中 this.radius = radius;
// 查找 radius 屬性,觸發 get,返回 this._radius
// 賦值為 radius
return this._radius;
},
set: function(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
});
new CircleEx 得到的每個實例都具有兩個屬性,實例屬性 _radius 和原型屬性 getArea,至於 radius,則是定義在原型對象上的訪問器(getter/setter),不可枚舉
之前說「偶爾」能看到這樣的「class」定義,就是因為太麻煩了,大家都懶得用。ES6 class 部分的目標就是改變現狀,提供更方便的類成員定義方式
簡化了對象的定義方式
可以直接在對象字面量中定義 getter/setter,簡化了函數類型屬性定義方式,包括一般函數、生成器和動態函數名,例如:
// getter/setter
var obj = {
// getter
//!!! getter 無參,無法傳參
get attr() {
console.log('getter');
return this._attr || 0;
},
// setter
//!!! setter 至少接受一個參數
set attr(val) {
console.log('setter');
this._attr = val;
},
// 預計算屬性(用 [] 語法添加的函數屬性,動態函數名)
[(function() {return 'print';})()](arg) {
console.log(arg);
},
// 一般方法
fn(arg) {
console.log(`arg = ${arg}`);
},
// 生成器
*gen(i) {
while(true) {
yield i++;
}
}
}
冗長的 function 關鍵字從類型定義中徹底消失了,甚至比 [箭頭函數](/articles/箭頭函數-es6 筆記 6/) 都簡潔。又感受到了初學 JS 時的欣喜——給變量添上一對圓括號就是函數調用,怎麼這麼簡單粗暴?
用 ES6 增強版對象字面量重寫之前的 CircleEx 類,將會是這樣:
// 重寫 CircleEx
CircleEx.prototype = {
getArea() {
return Math.PI * this.radius * this.radius;
},
get radius() {
return this._radius;
},
set radius(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
};
注意,與之前不同的是,此時訪問器 radius 是可枚舉的,並且作為一個原型屬性出現。但同樣的,訪問器自身不會被暴露出來,針對 radius 的所有操作都是對訪問器返回值的操作,而不是訪問器本身,因此:
typeof new CircleEx(1).radius === 'number' // true
啟用了 class 定義
P.S. 之所以說「啟用」而不是「引入」,是因為 ES6 class 相關的所有關鍵字本來就是保留字,只是現在被賦予了明確的含義
class 定義中,constructor 表示構造函數,static 關鍵字用來區分一般函數和特殊函數(含義同 Java、C++)
constructor 可選,默認提供空構造函數(constructor() {}),constructor必須是字面量形式,不能是動態函數名,否則將得到名為 constructor 的一般方法,而不是構造函數
用 class 語法重寫之前的 Circle 類,如下:
// 重寫 CircleEx
class MyCircle {
// 構造函數
constructor(radius) {
this.radius = radius;
MyCircle.count++;
}
// 實例屬性的 getter/setter
get radius() {
return this._radius;
}
set radius(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
// 原型屬性
getArea() {
return Math.PI * this.radius * this.radius;
}
// 靜態屬性
static get count() {
return this._count || 0;
}
static set count(val) {
this._count = val;
}
}
無論實際效果如何,但至少看起來清晰多了
類型保護
內置了類型保護,用 class 語法定義的類型,必須通過 new 操作符來調用,例如:
// 當作一般函數調用,會報錯
MyCircle();
// Uncaught TypeError: Class constructors cannot be invoked without 'new'
這種內置保護能夠避免一些問題,但確實也限制了靈活性,比如某些類庫提供的 API 中含有既能作為一般函數調用,也能作為構造函數使用的函數,用 class 語法就無法實現
此外,類定義中引用的類名不會被外部力量改變,例如:
class T {
static get val() {
return 'val';
}
get() {
return T.val;
}
}
// test
var t = new T();
console.log(t.get()); // val
// 外力破壞
T = null;
console.log(t.get()); // val
//! 報錯 Uncaught TypeError: T is not a constructor
// new T();
在 ES6 之前是沒有這種保護的,用 function 重寫,遭到外力破壞後必定出錯
支持類表達式(匿名類)
// 匿名類
var circle = new class {
constructor(radius) {
this.radius = radius;
}
}(3);
console.log(circle.radius); // 3
匿名類好像沒什麼用,因為一般來說,類是對象模板,通過類能實現對象的量產,Java 用匿名類來創建不需要量產的臨時對象,而 JS 有 N 種方法可以創建對象,用匿名類可能是最傻的方式
類中定義的方法可配置不可枚舉
比如 MyClass.prototype(通過 class 語法定義的)所有屬性都不可枚舉,類中定義的方法也不可枚舉
這樣做似乎是在刻意掩蓋對象系統基於原型的事實,比如 for...in 無法枚舉之前的 MyCircle.prototype,但可以通過 Object.getOwnPropertyNames() 發現一些痕跡,例如:
console.log(Object.getOwnPropertyNames(MyCircle.prototype));
// log print:
// ["constructor", "radius", "getArea"]
這些屬性都真實存在於原型對象上,但默認不可枚舉掩蓋了這個事實,可能是不希望我們在使用 class 語法的同時,手動操作 prototype 對象,破壞既有規則
可能總覺得這種掩蓋 prototype 的做法欠妥,但又說不上哪裡不對,好,考慮下原型繼承的經典問題:原型引用屬性會在實例間共享,例如:
function Type() {}
Type.prototype.issue = [1, 2];
// test
var t1 = new Type();
var t2 = new Type();
t1.issue.push(3);
console.log(t2.issue); // [1, 2, 3]
class 語法存在這個問題嗎?首先我們得用 class 語法在 Type 的原型上定義一個數組類型屬性,沮喪地發現根本做不到,除非直接訪問 prototype,所以不用擔心這種「掩蓋」會引發隱秘的問題,ES6 設計者比我們考慮得多得多
三、總結
清爽的類型定義語法,keep it simple
ES 正在逐漸剔除語法噪音:
箭頭函數 + `class` 剔掉了 `function`
默認參數 + 不定參數剔掉了 `arguments`
`class` + `extends` + `super` 剔掉了 `prototype`
從函數式語言的角度來看,這些變化是極好的,數學家追求的正是極致簡潔的美
參考資料
-
《ES6 in Depth》:InfoQ 中文站提供的免費電子書
-
《JavaScript 語言精髓與編程實踐》
暫無評論,快來發表你的看法吧