日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

本文深入淺出地討論了 JAVAScript 構造函數、原型、類、繼承的特性和用法,以及它們之間的關系。希望看完本文,能幫助大家對它們有更加清晰通透的認識和掌握!

01、前言

眾所周知,JavaScript 是一門面向對象的語言,而構造函數、原型、類、繼承都是與對象密不可分的概念。在我們日常前端業務開發中,系統和第三方庫已經為我們提供了大部分需要的類,我們的關注點更多是在對象的使用和數據處理上,而比較少需要去自定義構造函數和類,對原型的直接接觸就更少了。

 

然而,能深度理解并掌握好構造函數、原型、類與繼承,對我們的代碼設計大有裨益,也是作為一名高級前端工程師必不可少的基本功。

 

本文旨在用最通俗易懂的解釋和簡單生動的代碼示例,來徹底捋清對象、構造函數、原型、類與繼承。我們會以問答對話的形式,層層遞進,從構造函數談起,再引出原型與原型鏈,分析類為什么是語法糖,最后再推理出 JS 的幾種繼承方式。

 

在進入正式篇章之前,我們可以先嘗試思考以下幾個問題:

 

1.new Date().__proto__ == Date.prototype?

2.new Date().constructor == Date?

3.Date.__proto__ == Function.prototype?

4.Function.__proto__ == Function.prototype?

5.Function.prototype.__proto__== Object.prototype?

6.Object.prototype.__proto__ == null?

 

—— 思考分割線 ——

 

沒錯,它們都是 true !為啥?聽我娓娓道來~

02、構造函數

某IT公司前端研發部,新人小Q和職場混跡多年的老白聊起著構造函數、原型與類的話題。

小Q:構造函數我知道呀,平時 new Date(),new Promise() 經常用, Date,Promise 不就是構造函數,我們通過 new 一個構造函數去創建并返回一個新對象。

老白:沒錯,這些是系統自帶的一些構造函數,那你可以自己寫個構造函數嗎?

小Q:雖然平時用的不多,但也難不倒我~

// 定義個構造函數
function Person(name) {
    this.name = name;
}
// new構造函數,創建對象
let person = new Person("張三");

小Q:看吧 person 就是對象,Person 就是構造函數,清晰明了!

老白:那我要是單純寫這個方法算不算構造函數?

function add(a, b) {
    return a + b;
}

小Q:這不是吧,這明顯就是個普通函數啊?

老白:可是它也可以 new 對象哦!

function add(a, b) {
    return a + b;
}
let a = new add(1, 2);
// add {}
console.log(a);
// true
console.log(a instanceof add);
// object
console.log(typeof a);

小Q:誒?

老白:其實所謂構造函數,就是普通函數,關鍵看你要不要 new 它,但是 new 是在使用的時候,在定義的時候咋知道它后面會不會被 new 呢,所以構造函數只不過是當被用來new 時的稱呼。就像你上面的 Person 函數,不要 new 直接運行也是可以的嘛。

function Person(name) {
    this.name = name;
}
Person("張三");

小Q:哦,我懂了,所有函數都可以被 new,都可以作為構造函數咯,所謂構造函數只是一種使用場景。

老白:嗯嗯,總結得很好,但也不全對,比如箭頭函數就不能被 new,因為它沒有自己的 this 指向,所以不能作為構造函數。比如下面這樣就會報錯。

let Person = (name) => {
    this.name = name;
};
// Uncaught TypeError: Person is not a constuctor
let person = new Person("張三");

小Q:原來如此,那你剛剛 Person("張三"); ,既然沒有創建新對象,那里面的 this 又指向誰了?

老白:這就涉及到函數內 this 指向問題了,可以簡單總結以下 5 種場景。

1. 通過 new 調用,this 指向創建的新對象;

2. 直接當做函數調用,this 指向 window(嚴格模式下為 undefined);

function Person(name) {
    this.name = name;
}
// this 指向 window
Person("張三");
// 張三
console.log(window.name);

(看吧,不注意的話,不小心把 window 對象改了都不知道)

3.作為對象的方法調用,this 指向該對象;

function Person(name) {
    this.name = name;
}
let obj = {
    Person,
};
// this 指向 obj
obj.Person("張三");
// { "name": "張三", Person: f }
console.log(obj);

4.通過 Apply,call,bind 方法,顯式指定 this;

function Person(name) {
    this.name = name;
}
// this 指向 call 的第一個參數
Person.call(Math, "張三");
// 張三
console.log(Math.name);

5.箭頭函數中沒有自己的 this 指向,取決于上下文:

function Person(name) {
    this.name = name;
    
    // 普通函數,this 取決于調用者,即上述的 4 種情況
    setTimeout(function() {
        console.log(this);
    }, 0)
    
    // 箭頭函數,this 取決于上下文,我們可以忽略箭頭函數的存在
    // 即同上面 this.name = name 中的 this 指向一樣
    setTimeout(() => {
        console.log(this);
    }, 0)
}

小Q:原來 this 指向都有這么多種情況,好的,小本本記下了,等下就去試驗下。

小Q:等下,我重新看了你的 new add(1, 2),那 a + b = 3 還被 return 了呢,這 3 return 到哪去了?

function add(a, b) {
    return a + b;
}
let a = new add(1, 2);

老白:沒錯,你注意到了,構造函數是不需要 return 的,函數中的 this 就是創建并返回的新對象了。

但當 new 一個有 return 的構造函數時,如果 return 的是基本類型,則 return 的數據直接被拋棄。

如果 return 一個對象,則最終返回的新對象就是 return 的這個對象,這時原本 this 指向的對象就會被拋棄。

function Person(name) {
    this.name = name;
    // 返回的是對象類型
    return new Date();
}
let person = new Person("張三");
// 返回的是 Date 對象
// Sat Jul 29 2023 16:13:01 GMT+0800 (中國標準時間)
console.log(person);

老白:當然如果要把一個函數的使用用途作為構造函數的話,像我剛剛起名 add() 肯定是不規范的, 一般首字母要大寫,并且最好用名詞,像你起的 Person 就不錯。

小Q:新知識get√

要點歸納

1. 除箭頭函數外的所有函數都可以作為構造函數被new

2. 函數內this指向問題

3. 構造函數return問題

4. 構造函數命名規范

03、原型

小Q:都說原型原型,可看了這么久,這代碼里也沒出現原型呀?

老白:沒錯,原型是個隱藏的家伙,我們可以通過對象或者構造函數去拿到它。

// 構造函數
function Person(name) {
    this.name = name;
}
// 對象
let person = new Person("張三");

// 通過對象拿到原型(2種方法)
let proto1 = Object.getPrototypeOf(person);
let proto2 = person.__proto__;

// 通過構造函數拿到原型
let proto3 = Person.prototype;

// 驗證一下
// true
console.log(proto1 == proto2);
// true
console.log(proto1 == proto3);

小Q:可是這個原型是哪來的呀,我代碼里也沒創建它呀?

老白:當你聲明一個函數時,系統就自動幫你生成了一個關聯的原型啦,當然它也是一個普通對象,包含 constructor 字段指向構造函數,并且構造函數的 prototype 屬性也會指向這個原型。

當你用構造函數創建對象時,系統又幫你把對象的 __proto__ 屬性指向原型。

// 構造函數
function Person(name) {
    this.name = name;
}
// 可以理解為:聲明函數時,系統自動執行了下面代碼
Person.prototype = {
    // 指向構造函數
    constructor: Person 
}

// 對象
let person = new Person("張三");
// 可以理解為:創建對象時,系統自動執行了下面代碼
person.__proto__ == Person.prototype;

小Q:它們的引用關系,稍微有點繞啊~

老白:沒事,我畫兩個圖來表示,更加清晰點。

(備注:proto 只是單純用來表示原型的一個代名而已,代碼中并不存在)

深度解讀 JS 構造函數、原型、類與繼承圖片

深度解讀 JS 構造函數、原型、類與繼承圖片

小Q:懂了!

老白:那你說說 {}.__proto__ 和 {}.consrtuctor 分別是什么?

小Q:讓我分析下,{} 其實就是 new Object() 的一種字面量寫法,本質上就是 Object 對象,那 {}.__proto__ 就是原型 Object.prototype,{}.constructor 就是構造函數 Object,對吧?

老白:沒錯,只要能熟練掌握上面這個圖,構造函數,原型和對象這三者的引用關系基本很清晰了。一開始提的1、2 題基本也迎刃而解了!

  1. new Date().__proto__ == Date.prototype ?
  2. new Date().constructor == Date ?

小Q:那這個原型有什么用呢?

老白:一句話總結:當訪問對象的屬性不存在時,就會去訪問原型的屬性。

深度解讀 JS 構造函數、原型、類與繼承圖片

圖3

老白:我們可以通過代碼驗證下,person 對象是沒有 age 屬性的,所以 person.age 返回的其實是原型的 age 屬性值,當原型的 age 屬性改變時,person.age 也會跟著改變。

function Person(name) {
    this.name = name;
}
// 給原型增加age屬性
Person.prototype.age = 18;

// 對象
let person = new Person("張三");
// 18
console.log(person.age);
// 修改原型的age屬性
Person.prototype.age++;
// 19
console.log(person.age);

小Q:那如果我直接 person.age++ 呢,改的是 person 還是原型?

老白:這樣的話就相當于 person.age = person.age + 1 啦,等號右邊的 person.age 因為  對象目前還沒 age 屬性,所以拿到的是原型的 age 屬性,即18,然后 18 + 1 = 19 將賦值給 person 對象。

后續當你再訪問 person.age 時,因為 person 對象已經存在 age 屬性了,就不會再檢索到原型上了。

這種行為我們一般稱為重寫,在這個例子里也描述為:person 對象重寫了原型上的 age 屬性。

深度解讀 JS 構造函數、原型、類與繼承圖片

圖4

小Q:那這樣的話使用起來豈不是很亂,我還得很小心的分析 person.age 到底是 person 對象的還是原型的?

老白:沒錯,如果你不想出現這種無意識的重寫,將原型上的屬性設為對象類型不失為一種辦法。

function Person(name) {
    this.name = name;
}
// 原型的info屬性是對象
Person.prototype.info = {
    age: 18,
};
let person = new Person("張三");

person.info.age++;

小Q:我懂了,改變的是 info 對象的 age 屬性, person 并沒有重寫 info 屬 性,所以 person 對象本身依然沒有 info 屬性,person.info 依然指向原型。

老白:沒錯!不過這樣也有個壞處,每一個 Person 對象都可以共享原型的 info ,當 info 中的屬性被某個對象改變了,也會對其他對象造成影響。

function Person(name) {
    this.name = name;
}
Person.prototype.info = {
    age: 18,
};
let person1 = new Person("張三");
let person2 = new Person("李四");

// person1修改info
person1.info.age = 19;
// person2也會被影響,打印:19
console.log(person2.info.age);

老白:這對我們代碼的設計并不好,所以我們一般不在原型上定義數據,而是定義函數,這樣對象就可以直接使用掛載在原型上的這些函數了。

function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log("hello");
}
let person = new Person("張三");
// hello
person.sayHello();

小Q:我理解了,數據確實不應該被共享,每個對象都應該有自己的數據好點,但是函數無所謂,多個對象可以共享同一個原型函數。

老白:所以你知道為啥 {} 這個對象本身沒有任何屬性,卻可以執行 toString() 方法嗎?

小Q:【恍然大悟】來自它的原型 Object.prototype !

老白:不僅如此,很多系統自帶的構造函數產生的對象,其方法都是掛載在原型上的。比如我們經常用的數組方法,你以為是數組對象自己的方法嗎?不,是數組原型 Array.prototype 的方法,我們可以驗證下。

let array = [];
// array對象的push和原型上的push是同一個
// 打印:true
console.log(array.push == Array.prototype.push);
// array對象本身沒有自己的push屬性
// 打印:false
console.log(array.hasOwnProperty("push"));

深度解讀 JS 構造函數、原型、類與繼承圖片

小Q:【若有所思】

老白:再比如,你隨便定義一個函數 function fn() {},為啥它就能 fn.call() 這樣執行呢,它的 call 屬性是哪來的?

小Q:來自它的原型?函數其實是 Function 的對象,那它的原型就是 Function.prototype,試驗一下。

function fn() {}
// true
console.log(fn.constructor == Function);
// true
console.log(fn.call == Function.prototype.call);

老白:回答正確。在實際開發中,我們也可以通過修改原型上的函數,來改變對象的函數執行。比如說我們修改數組原型的 push 方法,加個監聽,這樣所有數組對象執行 push 方法時就能被監聽到了。

Array.prototype.push = (function (push) {
    // 閉包,push是原始的那個push方法
    return function (...items) {
        
        // 執行push要指定this
        push.call(this, ...items);
        
        console.log("監聽push完成,執行一些操作");
    };
})(Array.prototype.push);

let array = [];
// 打印:監聽push完成,執行一些操作
array.push(1, 2);
// 打印:[1, 2]
console.log(array);

老白:不只修改,也可以新增,比如說某些舊版瀏覽器數組不支持 includes 方法,那我們就可以在原型上新增一個 includes 屬性,保證代碼中數組對象使用 includes() 不會報錯(這也是 Polyfill.js 的目的)。

// 沒有includes
if(!Array.prototype.includes) {
    Array.prototype.includes = function() {
        // 自己實現includes
    }
}

小Q:又又漲知識了~

老白:原型相關的也說的差不多了,結合剛剛討論的構造函數,考你一個:手寫一個 new 函數。

小Q:啊啊,提示一下?

老白:好,我們簡單分析一下 new 都做了什么

  1. 創建一個對象,綁定原型;
  2. 以這個對象為 this 指向執行構造函數。

小Q:我試試~

function myNew(Fn, ...args) {
    var obj = {
        __proto__: Fn.prototype,
    };
    Fn.apply(obj, args);
    return obj;
}

小Q:試驗通過!

// 構造函數
function Person(name) {
    this.name = name;
}
// 原型
Person.prototype.age = 18;
// 創建對象
let person = myNew(Person, "張三");

// Person {name: "張三"}
console.log(person);
// 18
console.log(person.age);

老白:不錯不錯,讓我幫你再稍微完善一下嘿嘿~

function myNew(Fn, ...args) {
    // 通過Object.create指定原型,更加符合規范
    var obj = Object.create(Fn.prototype);
    
    // 指定this為obj對象,執行構造函數
    let result = Fn.apply(obj, args);
    
    // 判斷構造函數的返回值是否是對象
    return result instanceof Object ? result : obj;
}

 

要點歸納

1. 對象,構造函數,原型三者的引用關系

2. 原型的定義,特性及用法

3. 手寫new函數

 

04、原型鏈

老白:剛剛我們說當訪問對象的屬性不存在時,就會去訪問原型的屬性,那假如原型上的屬性也不存在呢?

小Q:返回 undefined?

老白:不對哦,原型本身也是一個對象,它也有它自己的原型。所以當訪問一個對象的屬性不存在時,就會檢索它的原型,檢索不到就繼續往上檢索原型的原型,一直檢索到根原型 Object.prototype,如果還沒有,才會返回 undefined,這也稱為原型鏈。

深度解讀 JS 構造函數、原型、類與繼承圖片

小Q:原來如此,所以說所有的對象都可以使用根原型 Object.prototype 上定義的方法咯。

老白:沒錯,不過有一些原型會重寫根原型上的方法,就比如 toString(),在 Date.prototype,Array.prototype 中都會有它們自己的定義。

// [object Object]
console.log({}.toString())

// 1,2,3
console.log([1,2,3].toString())

// Tue Aug 01 2023 17:58:05 GMT+0800 (中國標準時間)
console.log(new Date().toString())

小Q:理解了原型鏈,看回開始的3~6題,好像也不難了。

Date、Function 的原型是 Function.prototype,第 3、4 題就解了。

Function.prototype 的原型是 Object.prototype,第 5 題也解了。

Object.prototype 是根原型,所以它的 __proto__ 屬性就為 null,第 6 題也解了。

  1. Date.__proto__ == Function.prototype ?
  2. Function.__proto__ == Function.prototype ?
  3. Function.prototype.__proto__== Object.prototype ?
  4. Object.prototype.__proto__ == null ?

老白:完全正確。最后再考你一道和原型鏈相關的題,手寫 instanceOf 函數。提示一下,instanceOf 的原理是判斷構造函數的 prototype 屬性是否在對象的原型鏈上。

// array的原型鏈:Array.prototype → Object.prototype
let array = [];
// true
console.log(array instanceof Array);
// true
console.log(array instanceof Object);
// false
console.log(array instanceof Function);

小Q:好了嘞~

function myInstanceof(obj, Fn) {
    while (true) {
        obj = obj.__proto__; 
        // 匹配上了
        if (obj == Fn.prototype) {
            return true;
        }
        // 到達原型鏈的盡頭了
        if (obj == null) {
            return false;
        }
    }
}

檢測一下:

let array = [];
// true
console.log(myInstanceof(array, Array));
// true
console.log(myInstanceof(array, Object));
// false
console.log(myInstanceof(array, Function));

老白:Good!

要點歸納

1. 原型鏈

2. 手寫 instanceOf函數

05、類

小Q:好不容易把構造函數和原型都弄懂,怎么 ES6 又推出類呀,學不動了 T_T。

老白:不慌,類其實只是種語法糖,本質上還是”構造函數+原型“。

我們先看一下類的語法,類中可以包含有以下4種寫法不同的元素。

  • 對象屬性:key = xx
  • 原型屬性:key() {}
  • 靜態屬性:static key = x 或 static key() {}
  • 構造器:constructor() {}
class Person {
    // 對象屬性
    a = "a";
    b = function () {
        console.log("b");
    };
    // 原型屬性
    c() {
        console.log("c");
    }
    // 構造器
    constructor() {
        // 修改對象屬性
        this.a = "A";
        // 新增對象屬性
        this.d = "d";
    }
    // 靜態屬性
    static e = "e";
    static f() {
        console.log("f");
    }
}

我們再將這種 class 語法糖寫法還原成構造函數寫法。

function Person() {
    // 對象屬性
    this.a = "a";
    this.b = function () {
        console.log("b");
    };
    // 構造器
    this.a = "A";
    this.d = "d";
}

// 原型屬性
Person.prototype.c = function () {
    console.log("c");
};

// 靜態屬性
Person.e = "e";
Person.f = function () {
    console.log("f");
};

通過下面一些方法檢測,上面的2種寫法會得到同樣的結果。

// Person類本質是個構造函數,打印:function
console.log(typeof Person);

// Person的靜態屬性,打印:e
console.log(Person.e);

// 可以看到原型屬性c,打印:{constructor: ƒ, c: ƒ}
console.log(Person.prototype);

let person = new Person();

// 可以看到對象屬性a b d,打印:Person {a: 'A', d: 'd', b: ƒ}
console.log(person);
// 對象的構造函數就是Person,打印:true
console.log(person.constructor == Person);

小Q:所以類只不過是將本來比較繁瑣的構造函數的寫法給簡化了而已,這語法糖果然甜~

小Q:不過我發現一個問題,在 class 寫法中的原型屬性只能是函數,不能是數據?

老白:沒錯,這也呼應了前面說的,原型上只推薦定義函數,不推薦定義數據,避免不同對象共享同一個數據。

要點歸納

1. 類的語法

2. 類還原成構造函數寫法

06、繼承

小Q:我又又發現了一個問題,ES6 的 class 還可以 extends 另一個類呢,這也是語法糖?

老白:沒錯,這就是繼承,但是要弄懂 ES6 的這套繼承是怎么來的,還得從最開始的繼承方式說起。所謂繼承,就是我們是希望子類可以擁有父類的屬性方法,這和上面談到的原型的特性有點不謀而合。

我們用一個例子來思考思考,有這么 2 個類,如何讓 Cat 繼承 Animal,使得 Cat 的對象也有 type 屬性呢?

// 父類
function Animal() {
    this.type = "動物";
}
// 子類
function Cat() {
    this.name = "貓";
}

小Q:讓 Animal 對象充當 Cat 的原型!

function Animal() {
    this.type = "動物";
}
function Cat() {
    this.name = "貓";
}
// 指定Cat的原型
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

let cat = new Cat();
// Cat對象擁有了Animal的屬性
console.log(cat.type);

老白:沒錯,這是我們學完原型之后,最直觀的一種繼承實現方式,這種繼承又叫原型鏈式繼承。但是這種繼承方式存在 2 個缺點:

  1. 父類對象作為原型,其屬性會被所有子類對象共享;
  2. 創建子類對象時無法向父類構造函數傳參。
function Animal(type) {
    this.type = type;
}
function Cat(type) {
    this.name = "貓";
}
// 在這里就已經創建了Animal對象
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 創建子類對象時無法向父類構造函數傳參
let cat = new Cat("哺乳動物");

// type屬性來自原型,被所有Cat對象共享,打印:undefined
console.log(cat.type);

小Q:我想到個辦法,可以一舉解決上面2個缺點。

在子類構造函數中執行父類構造函數,并且指定執行父類構造函數中的 this 是子類對象,這樣屬性就都是屬于子類對象本身了,不存在共享。同時在創建子類對象時,也可以給父類構造函數傳參了,一舉兩得。

function Animal(type) {
    this.type = type;
}
function Cat(type) {
    // 執行父類,顯式指定this就是子類的對象
    Animal.call(this, type);
    this.name = "貓";
}
let cat = new Cat("哺乳動物");

// Cat {type: '哺乳動物', name: '貓'}
console.log(cat);

老白:這種繼承方式叫 構造函數式繼承,確實解決了 原型鏈式繼承 帶來的問題,不過這種繼承方式因為沒有用到原型,又有產生了2個新的問題:

  1. 沒有繼承父類原型的屬性方法;
  2. 子類對象不是父類的實例。
function Animal(type) {
    this.type = type;
}
// 父類的原型方法
Animal.prototype.eat = function () {
    console.log("吃");
};
function Cat(type) {
    Animal.call(this, type);
    this.name = "貓";
}
let cat = new Cat("哺乳動物");

// 沒有繼承父類原型的屬性方法,打印:undefined
console.log(cat.eat);
// 子類對象不是父類的實例,打印:false
console.log(cat instanceof Animal);

小Q:看來還要再改進,不如我把 原型鏈式 和 構造函數式 這 2 種繼承方式都用上,讓它們互補。

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function () {
    console.log("吃");
};
// 子類構造函數
function Cat(type) {
    Animal.call(this, type);
    this.name = "貓";
}
// 父類對象充當子類原型
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

試驗一下,果然所有問題都解決了。

// 可以給父類構造函數傳參
let cat = new Cat("哺乳動物");

// 子類對象擁用自己屬性,而非來自原型,避免數據共享
// 打印:Cat {type: '哺乳動物', name: '貓'}
console.log(cat);

// 子類對象可以繼承到父類原型的方法,打印:吃
cat.eat();

// 子類對象屬于父類的實例,打印:true
console.log(cat instanceof Animal);

老白:非常聰明,你又道出了第三種繼承方式,組合式繼承。即 原型鏈式 + 構造函數式 = 組合式。問題確實都解決了,但是有沒有發現,這種方式執行了 2 遍父類構造函數。

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function () {
    console.log("吃");
};
function Cat(type) {
    // 第二次執行父類構造函數
    Animal.call(this, type);
    this.name = "貓";
}
// 第一次執行父類構造函數
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

小Q:多執行了一遍,確實不夠完美,這怎么搞?

老白:其實關鍵在 Cat.prototype = new Animal(),你只不過想讓子類對象也能繼承到父類的原型,而這里創建了一個父類對象,為啥?說到底還是利用原型鏈: 子類對象 → 父類對象 → 父類原型。

如果我們不要中間那個"父類對象",而是用一個“空對象x”替換,讓原型鏈變成:子類對象 → 空對象x → 父類原型,這樣也能達到目的,就不用執行那遍沒必要的父類構造函數了。

// 組合式繼承:創建父類對象做子類原型
let animal = new Animal();
Cat.prototype = animal;

// 改進:創建一個空對象做子類原型,并且這個空對象的原型是父類原型
let x = Object.create(Animal.prototype);
Cat.prototype = x;

小Q:妙啊,這回完美了。

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function () {
    console.log("吃");
};
function Cat(type) {
    Animal.call(this, type);
    this.name = "貓";
}
// 寄生組合式,改進了組合式,少執行了一遍沒必要的父類構造函數
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

老白:這種繼承方式又叫 寄生組合式繼承,相當于在組合式繼承的基礎上進一步優化。回顧上面的幾種繼承方式的演變過程,原型鏈式 → 構造函數式 → 組合式 → 寄生組合式, 其實就是不斷優化的過程,最終我們才推理出比較完美的繼承方式。

小Q:那 ES6 class 的 extends 繼承 又是怎樣呢?

老白:說到底就是 寄生組合式繼承 的語法糖。我們先看看它的語法。

class Animal {
    eat() {
        console.log("吃");
    }
    constructor(type) {
        this.type = type;
    }
}
// Cat繼承Animal
class Cat extends Animal {
    constructor(type) {
        // 執行父類構造函數,相當于 Animal.call(this, type);
        super(type);
        
        // 執行完super(),子類對象就有父類屬性了,打印:哺乳動物
        console.log(this.type);
        
        this.name = "貓";
    }
}

創建對象試驗一下:

let cat = new Cat("哺乳動物");

// 子類原型的原型就是父類原型,打印:true
console.log(Cat.prototype.__proto__ == Animal.prototype);

// 子類本身擁有父類的屬性,打印:Cat {type: '哺乳動物', name: '貓'}
console.log(cat);

打印的結果展示的特性和 寄生組合式 是一樣的:

  1. 子類原型的原型就是父類原型;
  2. 子類本身擁有父類的屬性。

特性1 可以理解為 extends 背地里執行了:

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

特性2 在于 super(),它相當于 Animal.call(this),執行 super() 就是執行父類構造函數,將原本父類中的屬性都賦值給子類對象。

在 ES6 的語法中還要求 super() 必須在 this 的使用前調用,也是為了保證父類構造函數先執行,避免在子類構造器中設置的 this 屬性被父類構造函數覆蓋。

class Animal {
    constructor() {
        // 假如不報錯,this.name = "貓" 就被 this.name= "狗" 覆蓋了
        this.name = "狗";
    }
}
class Cat extends Animal {
    constructor(type) {
        this.name = "貓";
        // 沒有在this使用前調用,報錯
        super();
    }
}

小Q:看懂 寄生組合式繼承, extends 繼承 就是小菜一碟呀~

老白:最后再補充一下 super 的語法,可以子類的靜態屬性方法中通過 super.xx 訪問父類靜態屬性方法。

class Animal {
    constructor() {}
    static num = 1;
    static say() {
        console.log("hello");
    }
}
class Cat extends Animal {
    constructor() {
        super();
    }
    // super.num 相當于 Animal.num
    static count = super.num + 1;
    
    static talk() {
        // super.say() 相當于 Animal.say()
        super.say();
    }
}
// 2
console.log(Cat.count);
// hello
Cat.talk();

 super 是一個語法糖的特殊關鍵詞,特殊用法,并不指向某個對象,不能單獨使用,以下情況都是不允許的。

class Animal {}
class Cat extends Animal {
    constructor() {
        // 報錯
        let _super = super;
        // 報錯
        console.log(super);
    }
    static talk() {
        // 報錯
        console.log(super);
    }
}

要點歸納

  1. 原型鏈式繼承
  2. 構造函數式繼承
  3. 組合式繼承
  4. 寄生組合式繼承
  5. extends 繼承

07、總結

本文深入淺出地討論了 JavaScript 構造函數、原型、類、繼承的特性和用法,以及它們之間的關系。希望看完本文,能幫助大家對它們有更加清晰通透的認識和掌握!

分享到:
標簽:JS
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定