Class:向傳統類模式轉變的構造函數

来源:https://www.cnblogs.com/lalalagq/archive/2018/10/06/9748257.html
-Advertisement-
Play Games

前言 JS基於原型的‘類’,一直被轉行前端的碼僚們大呼驚奇,但接近傳統模式使用class關鍵字定義的出現,卻使得一些前端同行深感遺憾而紛紛留言:“還我獨特的JS”、“凈搞些沒實質的東西”、“自己沒有類還非要往別家的類上靠”,甚至是“已轉行”等等。有情緒很正常,畢竟新知識意味著更多時間與精力的開銷,又 ...


前言

JS基於原型的‘類’,一直被轉行前端的碼僚們大呼驚奇,但接近傳統模式使用class關鍵字定義的出現,卻使得一些前端同行深感遺憾而紛紛留言:“還我獨特的JS”、“凈搞些沒實質的東西”、“自己沒有類還非要往別家的類上靠”,甚至是“已轉行”等等。有情緒很正常,畢竟新知識意味著更多時間與精力的開銷,又不是簡單的閉眼享受。

然而歷史的軸印前行依舊,對於class可以肯定的一點是你不能對面試官說:“拜托,不是小弟不懂,僅僅是不願意瞭解,您換個問題唄!”一方面雖然class只是個語法糖,但extends對繼承的改進還是不錯的。另一方面今後可能在‘類’上出現的新特性應該是由class而不是構造函數承載,誰也不確定它將來會出落得怎樣標緻。因此,來來來,慢慢的喝下這碗熱氣騰騰的紅糖薑湯。

1 class

ECMAScript中沒有類的概念,我們的實例是基於原型由構造函數生成具有動態屬性和方法的對象。不過為了與國際接軌,描述的更為簡便和高大上,依然會使用‘類’這一詞。所以JS的類等同於構造函數。ES6的class只是個語法糖,其定義生成的對象依然構造函數。不過為了與構造函數模式區分開,我們稱其為類模式。學習class需要有構造函數和原型對象的知識,具體可以自行百度。


// ---使用構造函數
function C () {
  console.log('New someone.');
}

C.a = function () { return 'a'; }; // 靜態方法

C.prototype.b = function () { return 'b'; }; // 原型方法


// ---使用class
class C {
  static a() { return 'a'; } // 靜態方法
  
  constructor() { console.log('New someone.'); } // 構造方法
  
  b() { return 'b'; } // 原型方法
};

1.1 與變數對比

關鍵字class類似定義函數的關鍵字function,其定義的方式有聲明式和表達式(匿名式和命名式)兩種。通過聲明式定義的變數的性質與function不同,更為類似letconst,不會提前解析,不存在變數提升,不與全局作用域掛鉤和擁有暫時性死區等。class定義生成的變數就是一個構造函數,也因此,類可以寫成立即執行的模式。



// ---聲明式
class C {}
function F() {}

// ---匿名錶達式
let C = class {};
let F = function () {};

// ---命名錶達式
let C = class CC {};
let F = function FF() {};

// ---本質是個函數
class C {}
console.log(typeof C); // function
console.log(Object.prototype.toString.call(C)); // [object Function]
console.log(C.hasOwnProperty('prototype')); // true

// ---不存在變數提升
C; // 報錯,不存在C。
class C {}
// 存在提前解析和變數提升
F; // 不報錯,F已被聲明和賦值。
function F() {}

// ---自執行模式
let c = new (class {
})();
let f = new (function () {
})();

1.2 與對象對比

類內容({}裡面)的形式與對象字面量相似。不過類內容裡面只能定義方法不能定義屬性,方法的形式只能是函數簡寫式,方法間不用也不能用逗號分隔。方法名可以是帶括弧的表達式,也可以為Symbol值。方法分為三類,構造方法(constructor方法)、原型方法(存在於構造函數的prototype屬性上)和靜態方法(存在於構造函數本身上)




class C {
  // 原型方法a
  a() { console.log('a'); }
  // 構造方法,每次生成實例時都會被調用並返回新實例。
  constructor() {}
  // 靜態方法b,帶static關鍵字。
  static b() { console.log('b'); }
  // 原型方法,帶括弧的表達式
  ['a' + 'b']() { console.log('ab'); }
  // 原型方法,使用Symbol值
  [Symbol.for('s')]() { console.log('symbol s'); }
}

C.b(); // b

let c = new C();
c.a(); // a
c.ab(); // ab
c[Symbol.for('s')](); // symbol s

不能直接定義屬性,並不表示類不能有原型或靜態屬性。解析class會形成一個構造函數,因此只需像為構造函數添加屬性一樣為類添加即可。更為直接也是推薦的是只使用getter函數定義只讀屬性。為什麼不能直接設置屬性?是技術不成熟?是官方希望傳遞某種思想?抑或僅僅是筆者隨意拋出的一個問題?


// ---直接在C類(構造函數)上修改
class C {}
C.a = 'a';
C.b = function () { return 'b'; };
C.prototype.c = 'c';
C.prototype.d = function () { return 'd'; };

let c = new C();
c.c; // c
c.d(); // d

// ---使用setter和getter
// 定義只能獲取不能修改的原型或靜態屬性
class C {
  get a() { return 'a'; }
  static get b() { return 'b'; }
}

let c = new C();
c.a; // a
c.a = '1'; // 賦值沒用,只有get沒有set無法修改。

1.3 與構造函數對比

下麵是使用構造函數和類實現相同功能的代碼。直觀上,class簡化了代碼,使得內容更為聚合。constructor方法體等同構造函數的函數體,如果沒有顯式定義此方法,一個空的constructor方法會被預設添加用於返回新的實例。與ES5一樣,也可以自定義返回另一個對象而不是新實例。


// ---構造函數
function C(a) {
  this.a = a;
}

// 靜態屬性和方法
C.b = 'b';
C.c = function () { return 'c'; };

// 原型屬性和方法
C.prototype.d = 'd';
C.prototype.e = function () { return 'e'; };
Object.defineProperty(C.prototype, 'f', { // 只讀屬性
  get() {
    return 'f';
  }
});

// ---類
class C {
  static c() { return 'c'; }
  
  constructor(a) {
    this.a = a;
  }
  
  e() { return 'e'; }
  get f() { return 'f'; }
}

C.b = 'b';
C.prototype.d = 'd';

類雖然是個函數,但只能通過new生成實例而不能直接調用。類內部所定義的全部方法是不可枚舉的,在構造函數本身和prototype上添加的屬性和方法是可枚舉的。類內部定義的方法預設是嚴格模式,無需顯式聲明。以上三點增加了類的嚴謹性,比較遺憾的是,依然還沒有直接定義私有屬性和方法的方式。


// ---能否直接調用
class C {}
C(); // 報錯

function C() {}
C(); // 可以


// ---是否可枚舉
class C {
  static a() {} // 不可枚舉
  b() {} // 不可枚舉
}

C.c = function () {}; // 可枚舉
C.prototype.d = function () {}; // 可枚舉

isEnumerable(C, ['a', 'c']); // a false, c true
isEnumerable(C.prototype, ['b', 'd']); // b false, d true

function isEnumerable(target, keys) {
  let obj = Object.getOwnPropertyDescriptors(target);
  keys.forEach(k => {
    console.log(k, obj[k].enumerable);
  });
}


// ---是否為嚴格模式
class C {
  a() {
    let is = false;
    try {
      n = 1;
    } catch (e) {
      is = true;
    }
    console.log(is ? 'true' : 'false');
  }
}

C.prototype.b = function () {
  let is = false;
  try {
    n = 1;
  } catch (e) {
    is = true;
  }
  console.log(is ? 'true' : 'false');
};

let c = new C();
c.a(); // true,是嚴格模式。
c.b(); // false,不是嚴格模式。

在方法前加上static關鍵字表示此方法為靜態方法,它存在於類本身,不能被實例直接訪問。靜態方法中的this指向類本身。因為處於不同對象上,靜態方法和原型方法可以重名。ES6新增了一個命令new.target,指代new後面的構造函數或class,該命令的使用有某些限制,具體請看下麵示例。


// ---static
class C {
  static a() { console.log(this === C); }
  a() { console.log(this instanceof C); }
}

let c = new C();
C.a(); // true
c.a(); // true


// ---new.target
// 構造函數
function C() {
  console.log(new.target);
}

C.prototype.a = function () { console.log(new.target); };

let c = new C(); // 列印出C
c.a(); // 在普通方法中為undefined。

// ---類
class C {
  constructor() { console.log(new.target); }
  a() { console.log(new.target); }
}

let c = new C(); // 列印出C
c.a(); // 在普通方法中為undefined。

// ---在函數外部使用會報錯
new.target; // 報錯

2 extends

ES5中的經典繼承方法是寄生組合式繼承,子類會分別繼承父類實例和原型上的屬性和方法。ES6中的繼承本質也是如此,不過實現方式有所改變,具體如下麵的代碼。可以看到,原型上的繼承是使用extends關鍵字這一更接近傳統語言的形式,實例上的繼承是通過調用super完成子類this塑造。錶面上看,方式更為的統一和簡潔。


class C1 {
  constructor(a) { this.a = a; }
  b() { console.log('b'); }
}

class C extends C1 { // 繼承原型數據
  constructor() {
    super('a'); // 繼承實例數據
  }
}

2.1 與構造函數對比

使用extends繼承,不僅僅會將子類的prototype屬性的原型對象(__proto__)設置為父類的prototype,還會將子類本身的原型對象(__proto__)設置為父類本身。這意味著子類不單單會繼承父類的原型數據,也會繼承父類本身擁有的靜態屬性和方法。而ES5的經典繼承只會繼承父類的原型數據。不單單是財富,連老爸的名氣也要獲得,不錯不錯。


class C1 {
  static get a() { console.log('a'); }
  static b() { console.log('b'); }
}

class C extends C1 {
}
// 等價,沒有構造方法會預設添加。
class C extends C1 {
  constructor(...args) {
    super(...args);
  }
}

let c = new C();
C.a; // a,繼承了父類的靜態屬性。
C.b(); // b,繼承了父類的靜態方法。
console.log(Object.getPrototypeOf(C) === C1); // true,C的原型對象為C1
console.log(Object.getPrototypeOf(C.prototype) === C1.prototype); // true,C的prototype屬性的原型對象為C1的prototype

ES5中的實例繼承,是先創造子類的實例對象this,再通過callapply方法,在this上添加父類的實例屬性和方法。當然也可以選擇不繼承父類的實例數據。而ES6不同,它的設計使得實例繼承更為優秀和嚴謹。

在ES6的實例繼承中,是先調用super方法創建父類的this(依舊指向子類)和添加父類的實例數據,再通過子類的構造函數修飾this,與ES5正好相反。ES6規定在子類的constructor方法里,在使用到this之前,必須先調用super方法得到子類的this。不調用super方法,意味著子類得不到this對象。


class C1 {
  constructor() {
    console.log('C1', this instanceof C);
  }
}

class C extends C1 {
  constructor() {
    super(); // 在super()之前不能使用this,否則報錯。
    console.log('C');
  }
}

new C(); // 先列印出C1 true,再列印C。

2.2 super

關鍵字super比較奇葩,在不同的環境和使用方式下,它會指代不同的東西(總的說可以指代對象或方法兩種)。而且在不顯式的指明是作為對象或方法使用時,比如console.log(super),會直接報錯。

作為函數時。super只能存在於子類的構造方法中,這時它指代父類構造函數。

作為對象時。super在靜態方法中指代父類本身,在構造方法和原型方法中指代父類的prototype屬性。不過通過super調用父類方法時,方法的this依舊指向子類。即是說,通過super調用父類的靜態方法時,該方法的this指向子類本身;調用父類的原型方法時,該方法的this指向該(子類的)實例。而且通過super對某屬性賦值時,在子類的原型方法里指代該實例,在子類的靜態方法里指代子類本身,畢竟直接在子類中通過super修改父類是很危險的。

很迷糊對吧,瘋瘋癲癲的,還是結合著代碼看吧!


class C1 {
  static a() {
    console.log(this === C);
  }
  b() {
    console.log(this instanceof C);
  }
}

class C extends C1 {
  static c() {
    console.log(super.a); // 此時super指向C1,列印出function a。
    
    this.x = 2; // this等於C。
    super.x = 3; // 此時super等於this,即C。
    console.log(super.x); // 此時super指向C1,列印出undefined。
    console.log(this.x); // 值已改為3。

    super.a(); // 列印出true,a方法的this指向C。
  }

  constructor() {
    super(); // 指代父類的構造函數
    
    console.log(super.c); // 此時super指向C1.prototype,列印出function c。

    this.x = 2; // this等於新實例。
    super.x = 3; // 此時super等於this,即實例本身。
    console.log(super.x); // 此時super指向C1.prototype,列印出undefined。
    console.log(this.x); // 值已改為3。

    super.b(); // 列印出true,b方法的this指向實例本身。
  }
}

2.3 繼承原生構造函數

使用構造函數模式,構建繼承了原生數據結構(比如Array)的子類,有許多缺陷的。一方面由上文可知,原始繼承是先創建子類this,再通過父類構造函數進行修飾,因此無法獲取到父類的內部屬性(隱藏屬性)。另一方面,原生構造函數會直接忽略callapply方法傳入的this,導致子類根本無法獲取到父類的實例屬性和方法。


function MyArray(...args) {
  Array.apply(this, args);
}

MyArray.prototype = Array.prototype;
// MyArray.prototype.constructor = MyArray;

let arr = new MyArray(1, 2, 3); // arr為對象,沒有儲存值。
arr.push(4, 5); // 在arr上新增了0,1和length屬性。
arr.map(d => d); // 返回數組[4, 5]
arr.length = 1; // arr並沒有更新,依舊有0,1屬性,且arr[1]為5。

創建類的過程,是先構造一個屬於父類卻指向子類的this(繞口),再通過父類和子類的構造函數進行修飾。因此可以規避構造函數的問題,獲取到父類的實例屬性和方法,包括內部屬性。進而真正的創建原生數據結構的子類,從而簡單的擴展原生數據類型。另外還可以通過設置Symbol.species屬性,使得衍生對象為原生類而不是自定義子類的實例。


class MyArray extends Array { // 實現是如此的簡單
  static get [Symbol.species]() { return Array; }
}

let arr = new MyArray(1, 2, 3); // arr為數組,儲存有1,2,3。
arr.map(d => d); // 返回數組[1, 2, 3]
arr.length = 1; // arr正常更新,已包含必要的內部屬性。

需要註意的是繼承Object的子類。ES6改變了Object構造函數的行為,一旦發現其不是通過new Object()這種形式調用的,構造函數會忽略傳入的參數。由此導致Object子類無法正常初始化,但這不是個大問題。


class MyObject extends Object {
  static get [Symbol.species]() { return Object; }
}

let o = new MyObject({ id: 1 });
console.log(o.hasOwnPropoty('id')); // false,沒有被正確初始化

推薦

ES6精華:Symbol
ES6精華:Promise
Async:簡潔優雅的非同步之道
Generator:JS執行權的真實操作者


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 點進來的同學,大部分是為了學編程而來的,這裡面有一部分學編程是出於興趣愛好,但大部分都是為了找工作或跳槽吧!其中有些人也許是覺得難,也許是遇到瓶頸,也許是因為惰性,總之半途而廢了。在這新一年的開始,我想對你說一句:不要輕易放棄,如果你覺得艱難,說明你正在走上坡路!在為你講為什麼要學習大數據前給分享一 ...
  • 據外媒phonearena報道,估計有3200萬台Android設備很快就無法使用谷歌Chrome移動瀏覽器。根據XDA最近提交的一份文件顯示,Chrome移動瀏覽器應用程式的最低API級別將從4.1提高到4.4。 這意味著仍然運行由Jelly Bean(Jelly Bean是Android 4.1 ...
  • 一、概述 在 RxJava 中,一個實現了 介面的對象可以訂閱一個 類的實例。訂閱者對 發射的任何數據或數據序列作出響應。這種模式簡化了併發操作,因為它不需要阻塞等待 發射數據,而是創建了一個處於待命狀態的觀察者哨兵,哨兵在未來某個時刻響應 的通知。RxJava 提供了一套非同步編程的 API,並且支 ...
  • 列表視圖 為實現各種排列組合類的視圖(包括但不限於Spinner、ListView、GridView等等),Android提供了五花八門的適配器用於組裝某個規格的數據,常見的適配器有:數組適配器ArrayAdapter、簡單適配器SimpleAdapter、基本適配器BaseAdapter、翻頁適配 ...
  • 本文主要介紹Flutter佈局中的Padding、Align以及Center控制項,詳細介紹了其佈局行為以及使用場景,並對源碼進行了分析。 ...
  • 在函數內部,有兩個特殊的對象:arguments和this。 1、arguments arguments是一個類數組對象。包含著傳入函數中的所有參數。但這個對象還有一個名叫callee的屬性,該屬性是一個指針,指向擁有這個arguments對象的函數。 經典案例:階乘函數 定義階乘函數一般都要用到遞 ...
  • 6-2 css樣式的優點 為什麼使用css樣式來設置網頁的外觀樣式呢?右邊編輯器是一段文字,我們想把“超酷的互聯網”、“服務及時貼心”、“有趣易學”這三個短語的文本顏色設置為紅色,這時就 可以通過設置樣式來設置,而且只需要編寫一條css樣式語句。 第一步:把這三個短語用<span></span>括起 ...
  • 最近阿裡正式開源的BizCharts圖表庫基於React技術棧,各個圖表項皆採用了組件的形式,貼近React的使用特點。同時BizCharts基於G2進行封裝,Bizcharts也繼承了G2相關特性。公司目前統一使用的是ECharts圖表庫,下文將對3種圖表庫進行分析比對。 BizCharts 文檔 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...