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