這篇文章帶給大家的內容是關於js中的類別向傳統類別模式轉變的建構子詳解(附範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
JS基於原型的'類',一直被轉行前端的碼僚們大呼驚奇,但接近傳統模式使用class關鍵字定義的出現,卻使得一些前端同行深感遺憾而紛紛留言:“還我獨特的JS”、“淨搞些沒實質的東西”、“自己沒有類還非要往別家的類上靠”,甚至是“已轉行”等等。有情緒很正常,畢竟新知識意味著更多時間與精力的開銷,又不是簡單的閉眼享受。
然而歷史的軸印前行依舊,對於class可以肯定的一點是你不能對面試官說:「拜託,不是小弟不懂,僅僅是不願意了解,您換個問題唄!」一雖然方面class只是語法糖,但extends對繼承的改進還是不錯的。另一方面今後可能在‘類’上出現的新特性應該是由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'; } // 原型方法 };
關鍵字class類似定義函數的關鍵字function,其定義的方式有宣告式和表達式(匿名式和命名式)兩種。透過宣告式定義的變數的性質與function不同,更為類似let和const,不會事先解析,不存在變數提升,不與全域作用域掛鉤和擁有暫時性死區等。 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 () { })();
類別內容({}裡面)的形式與物件字面量相似。不過類別內容裡面只能定義方法不能定義屬性,方法的形式只能是函數簡寫式,方法間不用也不能用逗號分隔。方法名可以是括號的表達式,也可以是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无法修改。
以下是使用建構子和類別實作相同功能的程式碼。直覺上,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; // 报错
ES5中的經典繼承方法是寄生組合式繼承,子類別會分別繼承父類別實例和原型上的屬性和方法。 ES6中的繼承本質也是如此,不過實作方式有所改變,具體如下面的程式碼。可以看到,原型上的繼承是使用extends關鍵字這個更接近傳統語言的形式,實例上的繼承是透過呼叫super完成子類別this塑造。表面上看,方式更為的統一簡潔。
class C1 { constructor(a) { this.a = a; } b() { console.log('b'); } } class C extends C1 { // 继承原型数据 constructor() { super('a'); // 继承实例数据 } }
使用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,再通过call或apply方法,在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。
关键字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指向实例本身。 } }
使用构造函数模式,构建继承了原生数据结构(比如Array)的子类,有许多缺陷的。一方面由上文可知,原始继承是先创建子类this,再通过父类构造函数进行修饰,因此无法获取到父类的内部属性(隐藏属性)。另一方面,原生构造函数会直接忽略call或apply方法传入的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,没有被正确初始化
以上是js中的類別向傳統類別模式轉變的建構子詳解(附範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!