“睡服”面试官系列第二十一篇之class基本语法(建议收藏学习)
目錄
?
1. 簡介
2. 嚴格模式
3. constructor 方法
4. 類的實例對象
5. Class 表達式
6. 不存在變量提升
7. 私有方法
8. 私有屬性
9. this 的指向
10. name 屬性
11. Class 的取值函數(getter)和存值函數(setter)
12. Class 的 Generator 方法
13. Class 的靜態方法
14. Class 的靜態屬性和實例屬性
14.1類的實例屬性
14.2類的靜態屬性
15. new.target 屬性
1. 簡介
JavaScript 語言中,生成實例對象的傳統方法是通過構造函數。下面是一個例子。
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; var p = new Point(1, 2);上面這種寫法跟傳統的面向對象語言(比如 C++ 和 Java)差異很大,很容易讓新學習這門語言的程序員感到困惑。
ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為對象的模板。通過 class 關鍵字,可以定義類。
基本上,ES6 的 class 可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的 class 寫法只是讓對象原型的寫法更加清晰、更像面向對象
編程的語法而已。上面的代碼用 ES6 的 class 改寫,就是下面這樣
上面代碼定義了一個“類”,可以看到里面有一個 constructor 方法,這就是構造方法,而 this 關鍵字則代表實例對象。也就是說,ES5 的構造函數
Point ,對應 ES6 的 Point 類的構造方法。
Point 類除了構造方法,還定義了一個 toString 方法。注意,定義“類”的方法的時候,前面不需要加上 function 這個關鍵字,直接把函數定義放進去了
就可以了。另外,方法之間不需要逗號分隔,加了會報錯。
ES6 的類,完全可以看作構造函數的另一種寫法。
上面代碼表明,類的數據類型就是函數,類本身就指向構造函數。
使用的時候,也是直接對類使用 new 命令,跟構造函數的用法完全一致
構造函數的 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 方法可以很方便地一次向類添加多個方
法。
prototype 對象的 constructor 屬性,直接指向“類”的本身,這與 ES5 的行為是一致的。
Point.prototype.constructor === Point // true另外,類的內部所有定義的方法,都是不可枚舉的(non-enumerable)。
class Point { constructor(x, y) { // ... } toString() { // ... } } Object.keys(Point.prototype) // [] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]上面代碼中, toString 方法是 Point 類內部定義的方法,它是不可枚舉的。這一點與 ES5 的行為不一致。
var Point = function (x, y) { // ... }; Point.prototype.toString = function() { // ... }; Object.keys(Point.prototype) // ["toString"] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]上面代碼采用 ES5 的寫法, toString 方法就是可枚舉的。
類的屬性名,可以采用表達式。
上面代碼中, Square 類的方法名 getArea ,是從表達式得到的。
2. 嚴格模式
類和模塊的內部,默認就是嚴格模式,所以不需要使用 use strict 指定運行模式。只要你的代碼寫在類或模塊之中,就只有嚴格模式可用。
考慮到未來所有的代碼,其實都是運行在模塊之中,所以 ES6 實際上把整個語言升級到了嚴格模式。
3. constructor 方法
constructor 方法是類的默認方法,通過 new 命令生成對象實例時,自動調用該方法。一個類必須有 constructor 方法,如果沒有顯式定義,一個空的
constructor 方法會被默認添加。
上面代碼中,定義了一個空的類 Point ,JavaScript 引擎會自動為它添加一個空的 constructor 方法。
constructor 方法默認返回實例對象(即 this ),完全可以指定返回另外一個對象
上面代碼中, constructor 函數返回一個全新的對象,結果導致實例對象不是 Foo 類的實例。
類必須使用 new 調用,否則會報錯。這是它跟普通構造函數的一個主要區別,后者不用 new 也可以執行。
4. 類的實例對象
生成類的實例對象的寫法,與 ES5 完全一樣,也是使用 new 命令。前面說過,如果忘記加上 new ,像函數那樣調用 Class ,將會報錯。
class Point { // ... } // 報錯 var point = Point(2, 3); // 正確 var point = new Point(2, 3);與 ES5 一樣,實例的屬性除非顯式定義在其本身(即定義在 this 對象上),否則都是定義在原型上(即定義在 class 上)
//定義類 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } } var point = new Point(2, 3); point.toString() // (2, 3) point.hasOwnProperty('x') // true point.hasOwnProperty('y') // true point.hasOwnProperty('toString') // false point.__proto__.hasOwnProperty('toString') // true上面代碼中, x 和 y 都是實例對象 point 自身的屬性(因為定義在 this 變量上),所以 hasOwnProperty 方法返回 true ,而 toString 是原型對象的屬性
(因為定義在 Point 類上),所以 hasOwnProperty 方法返回 false 。這些都與 ES5 的行為保持一致。
與 ES5 一樣,類的所有實例共享一個原型對象。
上面代碼中, p1 和 p2 都是 Point 的實例,它們的原型都是 Point.prototype ,所以 __proto__ 屬性是相等的。
這也意味著,可以通過實例的 __proto__ 屬性為“類”添加方法。
__proto__ 并不是語言本身的特性,這是各大廠商具體實現時添加的私有屬性,雖然目前很多現代瀏覽器的 JS 引擎中都提供了這個私有屬
性,但依舊不建議在生產中使用該屬性,避免對環境產生依賴。生產環境中,我們可以使用 Object.getPrototypeOf 方法來獲取實例對象
的原型,然后再來為原型添加方法/屬性。
上面代碼在 p1 的原型上添加了一個 printName 方法,由于 p1 的原型就是 p2 的原型,因此 p2 也可以調用這個方法。而且,此后新建的實例 p3 也可以調用
這個方法。這意味著,使用實例的 __proto__ 屬性改寫原型,必須相當謹慎,不推薦使用,因為這會改變“類”的原始定義,影響到所有實例。
5. Class 表達式
與函數一樣,類也可以使用表達式的形式定義。
const MyClass = class Me { getClassName() { return Me.name; } };上面代碼使用表達式定義了一個類。需要注意的是,這個類的名字是 MyClass 而不是 Me , Me 只在 Class 的內部代碼可用,指代當前類。
let inst = new MyClass(); inst.getClassName() // Me Me.name // ReferenceError: Me is not defined上面代碼表示, Me 只在 Class 內部有定義。
如果類的內部沒用到的話,可以省略 Me ,也就是可以寫成下面的形式。
采用 Class 表達式,可以寫出立即執行的 Class。
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('張三'); person.sayName(); // "張三"上面代碼中, person 是一個立即執行的類的實例。
6. 不存在變量提升
類不存在變量提升(hoist),這一點與 ES5 完全不同。
new Foo(); // ReferenceError class Foo {}上面代碼中, Foo 類使用在前,定義在后,這樣會報錯,因為 ES6 不會把類的聲明提升到代碼頭部。這種規定的原因與下文要提到的繼承有關,必須保證
子類在父類之后定義
上面的代碼不會報錯,因為 Bar 繼承 Foo 的時候, Foo 已經有定義了。但是,如果存在 class 的提升,上面代碼就會報錯,因為 class 會被提升到代碼頭
部,而 let 命令是不提升的,所以導致 Bar 繼承 Foo 的時候, Foo 還沒有定義。
7. 私有方法
私有方法是常見需求,但 ES6 不提供,只能通過變通方法模擬實現。
一種做法是在命名上加以區別。
上面代碼中, _bar 方法前面的下劃線,表示這是一個只限于內部使用的私有方法。但是,這種命名是不保險的,在類的外部,還是可以調用到這個方法。
另一種方法就是索性將私有方法移出模塊,因為模塊內部的所有方法都是對外可見的
上面代碼中, foo 是公有方法,內部調用了 bar.call(this, baz) 。這使得 bar 實際上成為了當前模塊的私有方法。
還有一種方法是利用 Symbol 值的唯一性,將私有方法的名字命名為一個 Symbol 值。
上面代碼中, bar 和 snaf 都是 Symbol 值,導致第三方無法獲取到它們,因此達到了私有方法和私有屬性的效果
8. 私有屬性
與私有方法一樣,ES6 不支持私有屬性。目前,有一個提案,為 class 加了私有屬性。方法是在屬性名之前,使用 # 表示。
class Point { #x; constructor(x = 0) { #x = +x; // 寫成 this.#x 亦可 } get x() { return #x } set x(value) { #x = +value } }上面代碼中, #x 就表示私有屬性 x ,在 Point 類之外是讀取不到這個屬性的。還可以看到,私有屬性與實例的屬性是可以同名的(比如, #x 與 get
x() )。
私有屬性可以指定初始值,在構造函數執行時進行初始化
之所以要引入一個新的前綴 # 表示私有屬性,而沒有采用 private 關鍵字,是因為 JavaScript 是一門動態語言,使用獨立的符號似乎是唯一的可靠方法,
能夠準確地區分一種屬性是否為私有屬性。另外,Ruby 語言使用 @ 表示私有屬性,ES6 沒有用這個符號而使用 # ,是因為 @ 已經被留給了 Decorator。
該提案只規定了私有屬性的寫法。但是,很自然地,它也可以用來寫私有方法
9. 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 方法而導致報錯。
一個比較簡單的解決方法是,在構造方法中綁定 this ,這樣就不會找不到 print 方法了。
另一種解決方法是使用箭頭函數
class Logger { constructor() { this.printName = (name = 'there') => { this.print(`Hello ${name}`); }; } // ... }還有一種解決方法是使用 Proxy ,獲取方法的時候,自動綁定 this 。
function selfish (target) { const cache = new WeakMap(); const handler = { get (target, key) { const value = Reflect.get(target, key); if (typeof value !== 'function') { return value; } if (!cache.has(value)) { cache.set(value, value.bind(target)); } return cache.get(value); } }; const proxy = new Proxy(target, handler); return proxy; } const logger = selfish(new Logger());10. name 屬性
由于本質上,ES6 的類只是 ES5 的構造函數的一層包裝,所以函數的許多特性都被 Class 繼承,包括 name 屬性。
class Point {} Point.name // "Point"name 屬性總是返回緊跟在 class 關鍵字后面的類名。
11. Class 的取值函數(getter)和存值函數(setter)
與 ES5 一樣,在“類”的內部可以使用 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 屬性有對應的存值函數和取值函數,因此賦值和讀取行為都被自定義了。
存值函數和取值函數是設置在屬性的 Descriptor 對象上的。
上面代碼中,存值函數和取值函數是定義在 html 屬性的描述對象上面,這與 ES5 完全一致。
12. Class 的 Generator 方法
如果某個方法之前加上星號( * ),就表示該方法是一個 Generator 函數。
class Foo { constructor(...args) { this.args = args; } * [Symbol.iterator]() { for (let arg of this.args) { yield arg; } } } for (let x of new Foo('hello', 'world')) { console.log(x); } // hello // world上面代碼中, Foo 類的 Symbol.iterator 方法前有一個星號,表示該方法是一個 Generator 函數。 Symbol.iterator 方法返回一個 Foo 類的默認遍歷
器, for...of 循環會自動調用這個遍歷器。
13. Class 的靜態方法
類相當于實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上 static 關鍵字,就表示該方法不會被實例繼承,而是直接通過
類來調用,這就稱為“靜態方法”。
上面代碼中, Foo 類的 classMethod 方法前有 static 關鍵字,表明該方法是一個靜態方法,可以直接在 Foo 類上調用( Foo.classMethod() ),而不是
在 Foo 類的實例上調用。如果在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。
注意,如果靜態方法包含 this 關鍵字,這個 this 指的是類,而不是實例。
上面代碼中,靜態方法 bar 調用了 this.baz ,這里的 this 指的是 Foo 類,而不是 Foo 的實例,等同于調用 Foo.baz 。另外,從這個例子還可以看出,靜
態方法可以與非靜態方法重名。
父類的靜態方法,可以被子類繼承。
上面代碼中,父類 Foo 有一個靜態方法,子類 Bar 可以調用這個方法。
靜態方法也是可以從 super 對象上調用的
14. Class 的靜態屬性和實例屬性
靜態屬性指的是 Class 本身的屬性,即 Class.propName ,而不是定義在實例對象( this )上的屬性。
class Foo { } Foo.prop = 1; Foo.prop // 1上面的寫法為 Foo 類定義了一個靜態屬性 prop 。
目前,只有這種寫法可行,因為 ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性
目前有一個靜態屬性的提案,對實例屬性和靜態屬性都規定了新的寫法。
14.1類的實例屬性
類的實例屬性可以用等式,寫入類的定義之中。
上面代碼中, myProp 就是 MyClass 的實例屬性。在 MyClass 的實例上,可以讀取這個屬性。
以前,我們定義實例屬性,只能寫在類的 constructor 方法里面。
上面代碼中,構造方法 constructor 里面,定義了 this.state 屬性。
有了新的寫法以后,可以不在 constructor 方法里面定義。
這種寫法比以前更清晰。
為了可讀性的目的,對于那些在 constructor 里面已經定義的實例屬性,新寫法允許直接列出
14.2類的靜態屬性
類的靜態屬性只要在上面的實例屬性寫法前面,加上 static 關鍵字就可以了
class MyClass { static myStaticProp = 42; constructor() { console.log(MyClass.myStaticProp); // 42 } }同樣的,這個新寫法大大方便了靜態屬性的表達。
// 老寫法 class Foo { // ... } Foo.prop = 1; // 新寫法 class Foo { static prop = 1; }上面代碼中,老寫法的靜態屬性定義在類的外部。整個類生成以后,再生成靜態屬性。這樣讓人很容易忽略這個靜態屬性,也不符合相關代碼應該放在一
起的代碼組織原則。另外,新寫法是顯式聲明(declarative),而不是賦值處理,語義更好。
15. new.target 屬性
new 是從構造函數生成實例對象的命令。ES6 為 new 命令引入了一個 new.target 屬性,該屬性一般用在構造函數之中,返回 new 命令作用于的那個構造
函數。如果構造函數不是通過 new 命令調用的, new.target 會返回 undefined ,因此這個屬性可以用來確定構造函數是怎么調用的。
上面代碼確保構造函數只能通過 new 命令調用。
Class 內部調用 new.target ,返回當前 Class。
需要注意的是,子類繼承父類時, new.target 會返回子類。
class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); // ... } } class Square extends Rectangle { constructor(length) { super(length, length); } } var obj = new Square(3); // 輸出 false上面代碼中, new.target 會返回子類
利用這個特點,可以寫出不能獨立使用、必須繼承后才能使用的類。
class Shape { constructor() { if (new.target === Shape) { throw new Error('本類不能實例化'); } } } class Rectangle extends Shape { constructor(length, width) { super(); // ... } } var x = new Shape(); // 報錯 var y = new Rectangle(3, 4); // 正確上面代碼中, Shape 類不能被實例化,只能用于繼承。
注意,在函數外部,使用 new.target 會報錯。
總結
本博客源于本人閱讀相關書籍和視頻總結,創作不易,謝謝點贊支持。學到就是賺到。我是歌謠,勵志成為一名優秀的技術革新人員。
歡迎私信交流,一起學習,一起成長。
推薦鏈接 其他文件目錄參照
“睡服“面試官系列之各系列目錄匯總(建議學習收藏)
總結
以上是生活随笔為你收集整理的“睡服”面试官系列第二十一篇之class基本语法(建议收藏学习)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CSS半透明背景
- 下一篇: 武大计算机考研 932教材,2018武汉