繼承6種套餐
參照紅皮書,JS繼承一共6種
1.原型鏈繼承
核心思想:子類的原型指向父類的一個實例
Son.prototype=new Father();
2.構造函數繼承
核心思想:借用apply和call方法在子對象中調用父對象
function Son(){Father.call(this);}
3.組合繼承(1+2)(常用)
核心思想:1+2,但記得修正constructor
function Son(){Father.call(this);}
Son.prototype=new Father();
Son.prototype.constructor = Son;
4.原型式繼承
核心思想:返回一個臨時類型的一個新實例,現提出了規范的原型式繼承,使用Object.create()方法。
var person={name:"xiaoming",age:16}
var anotherperson=Object.create(person,{name:"xiaowang"})
5.寄生式繼承
核心思想:創建一個僅用于封裝繼承過程的函數,該函數在內部使用某種方式增強對象
function createAnother(original){
var clone=object(original);
clone.name="ahaha";
return clone;
}
6.寄生組合繼承
核心思想:3+5
function inheritPropertype(son,father){
var prototype=object(father.prototype);//創建
prototype.constructor=son;//增強
son.prototype=prototype;//指定
}
在阮一峰老師的解說下,他將繼承分成了兩種,構造函數的繼承和非構造函數的繼承
構造函數的繼承:
1.apply或call
2.prototype,即子類原型屬性指向父類實例
3.直接的prototype,子類原型=父類原型
4.利用空對象作為中介,這種方法類似寄生繼承,但是會變成子類->中介->父類這樣的繼承關系。好處是當子類對原型進行變動時,對父類沒有影響。
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;//繼承方法2
Child.prototype = new F();//繼承方法1
Child.prototype.constructor = Child;//修正
Child.uber = Parent.prototype;//為子對象設一個uber屬性,這個屬性直接指向父對象的prototype屬性。只是為
??????????????????????????????????????????????????????? //了實現繼承的完備性,純屬備用性質。
}
5.拷貝繼承,將父對象的prototype對象中的屬性,一一拷貝給Child對象的prototype對象。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
非構造函數的繼承:
1.原型式繼承。
2.淺拷貝
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
子對象獲得的只是一個內存地址,而不是真正拷貝,因此存在父對象被篡改的可能。
3.深拷貝
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
ES6的class語法糖
不知道為什么標題都是跟吃的有關
可能是因為到了半夜吧(虛
在學ES6之前,我們苦苦背下JS繼承的典型方法
學習ES6后,發現官方雞賊地給我們一個語法糖——class。它可以看作是構造函數穿上了統一的制服,所以class的本質依然是函數,一個構造函數。
class是es6新定義的變量聲明方法(復習:es5的變量聲明有var function和隱式聲明 es6則新增let const class import),它的內部是嚴格模式。class不存在變量提升。
例:
//定義類
classPoint{
??? constructor(x,y){
??? ??? this.x=x;
???? ?? this.y=y;
??? }
??? toString(){
??? ??? return'('+this.x+', '+this.y+')';
??? }
}
constructor就是構造函數,不多說,跟c++學的時候差不多吧,this對象指向實例。
類的所有方法都定義在類的prototype屬性上面,在類的內部定義方法不用加function關鍵字。在類的外部添加方法,請指向原型,即實例的__proto__或者類的prototype。
Object.assign方法可以很方便地一次向類添加多個方法。
Object.assign(Point.prototype,{toString(){},toValue(){}});
私有的,靜態的,實例的
私有方法,私有屬性
類的特性是封裝,在其他語言的世界里,有private、public和protected來區分,而js就沒有
js在es5的時代,嘗試了一些委婉的方法,比如對象屬性的典型的set和get方法,在我之前說的JS的數據屬性和訪問器屬性
現在es6規定,可以在class里面也使用setter和getter:
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'
那么在這次es6的class里面,如何正式地去表示私有呢?
方法有叁:
1,老辦法,假裝私有。私有的東西,命名前加個下劃線,當然了這只是前端程序員的自我暗示,實際上在外部應該還是可以訪問得到私有方法。
2,乾坤大挪移。把目標私有方法挪出class外,class的一個公有方法內部調用這個外部的“私有”方法。
class Widget {
foo (baz) { bar.call(this, baz); } // ...
}
function bar(baz) { return this.snaf = baz; }
3,ES6順風車,SYMBOL。利用Symbol值的唯一性,將私有方法的名字命名為一個Symbol值。Symbol是第三方無法獲取的,所以外部也就無法偷看私有方法啦。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) { this[bar](baz); }
// 私有方法
[bar](baz) { return this[snaf] = baz; }
// ... };
那屬性怎么私有化呢?現在還不支持,但ES6有一個提案,私有屬性應在命名前加#號。
靜態方法,靜態屬性
類相當于實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接通過類來調用,這就稱為“靜態方法”。如果靜態方法包含this關鍵字,這個this指的是類,而不是實例。父類的靜態方法,可以被子類繼承。
?ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。
聲明一個靜態屬性,目前只支持以下寫法,定義在外部:
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
ES6當然也有提案,靜態屬性的聲明采用static關鍵字,不過也是只提案。
實例屬性
直接寫。
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
我有特殊的繼承技巧
既然已經把class明擺出來,當然就可以擺脫“私生子”的身份,光明正大繼承了。
Class 可以通過extends關鍵字實現繼承:
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 調用父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 調用父類的toString()
}
}
在這里Point是父類,ColorPoint是子類,在子類中,super關鍵字代表父類,而在子類的構造函數中必須調用super方法,通過super方法新建一個父類的this對象(子類自身沒有this對象),子類是依賴于父類的?;谶@個設計思想,我們在子類中需要注意:子類實例實際上依賴于父類的實例,是先有爹后有子,所以構造函數先super后用this;父類的靜態方法是會被子類所繼承的。
Class繼承的原理:
class A { }
class B { }
// B 的實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype);//B.prototype.__proto__=A.prototype
// B 的實例繼承 A 的靜態屬性
Object.setPrototypeOf(B, A);//B.__proto__=A
const b = new B();
在這里我們重新擦亮雙眼,大喊三遍:class的本質是構造函數class的本質是構造函數class的本質是構造函數
在之前的原型學習筆記里面,我學習到了prototype是函數才有的屬性,而__proto__是每個對象都有的屬性。
在上述的class實質繼承操作中,利用了Object.setPrototypeOf(),這個方法把參數1的原型設為參數2。
所以實際上我們是令B.prototype.__proto__=A.prototype,轉化為圖像就是上圖所示,Father.prototype(更正圖上的Father)截胡,變為了Son.prototype走向Object.prototype的中間站。
那為什么還有第二步B.__proto__=A呢?在class出來以前,我們的繼承操作僅到上一步為止。
但是既然希望使用class來取代野路子繼承,必須考慮到方法面面,譬如父類靜態屬性的繼承。
在沒有這一步之前,我們看看原本原型鏈的意義:Son.__proto__==Function.prototype,意味著Son是Function 的一個實例。因為我們可以通過類比,一個類的實例的__proto__的確指向了類的原型對象(prototype)。
所以B.__proto__=A意味著B是A的一個實例嗎?可以說有這樣的意味在里面,所以假使將B看作是A的一個實例,A是一個類似于原型對象的存在,而A的靜態屬性在這里失去了相對性,可看作是一個實例屬性,同時B還是A的子類,那么A的靜態屬性就是可繼承給B的,并且繼承后,B對繼承來的靜態對象如何操作都影響不到A,AB的靜態對象是互相獨立的。
當然,上述只是我一個弱雞的理解,讓我們看看在阮一峰大神的教程里是怎么解讀的:
大多數瀏覽器的 ES5 實現之中,每一個對象都有__proto__屬性,指向對應的構造函數的prototype屬性。Class 作為構造函數的語法糖,同時有prototype屬性和__proto__屬性,因此同時存在兩條繼承鏈。
(1)子類的__proto__屬性,表示構造函數的繼承,總是指向父類。
(2)子類prototype屬性的__proto__屬性,表示方法的繼承,總是指向父類的prototype屬性。
經過上述的我個人推測和大神的準確解說,解除了我心中一個顧慮:一個類的原型畢竟指向函數的原型對象,如果我們把子類的原型指向父類,是否會對它函數的本質有一定的影響?
事實上我們可以把這個操作視為“子類降級”,子類不再直接地指向函數原型對象,它所具備的函數的一些方法特性等,會順著原型鏈指向函數原型對象,當我們希望對某個子類實行一些函數特有的操作等,編譯器自然會通過原型鏈尋求目標。這就是原型鏈的精妙之處。
在阮一峰老師的ES6教程的“extends的繼承目標”一節中,講解了三種特殊的繼承,Object,不繼承,null。從這里也可以看見Function.prototype和子類的原型指向在原型鏈的角色。
class A{
constructor(){}
}
console.log(A.prototype,A.__proto__,A.prototype.__proto__)
//A.prototype==A {}//A.__proto__==[Function]
//A.prototype.__proto__=={}
super
剛才有說到構造函數里面有super(x,y),方法里面有super.toString(),也就是說super有兩種意義
1,父類的構造函數
然而這個super方法是在子類構造函數里面使用的,所以它應當返回一個子類的實例,所以super里面的this應該指向子類。super()在這里相當于A.prototype.constructor.call(this)。
super()只能用在子類的構造函數之中,用在其他地方會報錯。
2,與父類相關的對象
super作為對象時,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類。
Decorator-修飾器
修飾器是一個對類進行處理的函數。修飾器函數的第一個參數,就是所要修飾的目標類。
例:
@testable class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
另外修飾器也可以修飾方法
class Math {
@log
add(a, b) { return a + b; }
}
function log(target, name, descriptor) {
var oldValue = descriptor.value; descriptor.value = function() {
console.log(`Calling ${name} with`, arguments);
return oldValue.apply(null, arguments);
};
return descriptor;
}
const math = new Math(); // passed parameters should get logged now
math.add(2, 4);
修飾器函數一共可以接受三個參數。第一個是類的原型對象,第二個是要修飾的參數,第三個是修飾參數的數據屬性對象
太累了,不想細說了,先寫到這