自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深度解讀 JS 構(gòu)造函數(shù)、原型、類與繼承

開發(fā) 前端
本文深入淺出地討論了 JavaScript 構(gòu)造函數(shù)、原型、類、繼承的特性和用法,以及它們之間的關(guān)系。希望看完本文,能幫助大家對它們有更加清晰通透的認(rèn)識和掌握!

01、前言

眾所周知,JavaScript 是一門面向?qū)ο蟮恼Z言,而構(gòu)造函數(shù)、原型、類、繼承都是與對象密不可分的概念。在我們?nèi)粘G岸藰I(yè)務(wù)開發(fā)中,系統(tǒng)和第三方庫已經(jīng)為我們提供了大部分需要的類,我們的關(guān)注點更多是在對象的使用和數(shù)據(jù)處理上,而比較少需要去自定義構(gòu)造函數(shù)和類,對原型的直接接觸就更少了。

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

本文旨在用最通俗易懂的解釋和簡單生動的代碼示例,來徹底捋清對象、構(gòu)造函數(shù)、原型、類與繼承。我們會以問答對話的形式,層層遞進,從構(gòu)造函數(shù)談起,再引出原型與原型鏈,分析類為什么是語法糖,最后再推理出 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、構(gòu)造函數(shù)

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

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

老白:沒錯,這些是系統(tǒng)自帶的一些構(gòu)造函數(shù),那你可以自己寫個構(gòu)造函數(shù)嗎?

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

// 定義個構(gòu)造函數(shù)
function Person(name) {
    this.name = name;
}
// new構(gòu)造函數(shù),創(chuàng)建對象
let person = new Person("張三");

小Q:看吧 person 就是對象,Person 就是構(gòu)造函數(shù),清晰明了!

老白:那我要是單純寫這個方法算不算構(gòu)造函數(shù)?

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

小Q:這不是吧,這明顯就是個普通函數(shù)???

老白:可是它也可以 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:誒?

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

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

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

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

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

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

老白:這就涉及到函數(shù)內(nèi) this 指向問題了,可以簡單總結(jié)以下 5 種場景。

1. 通過 new 調(diào)用,this 指向創(chuàng)建的新對象;

2. 直接當(dāng)做函數(shù)調(diào)用,this 指向 window(嚴(yán)格模式下為 undefined);

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

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

3.作為對象的方法調(diào)用,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 的第一個參數(shù)
Person.call(Math, "張三");
// 張三
console.log(Math.name);

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

function Person(name) {
    this.name = name;
    
    // 普通函數(shù),this 取決于調(diào)用者,即上述的 4 種情況
    setTimeout(function() {
        console.log(this);
    }, 0)
    
    // 箭頭函數(shù),this 取決于上下文,我們可以忽略箭頭函數(shù)的存在
    // 即同上面 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);

老白:沒錯,你注意到了,構(gòu)造函數(shù)是不需要 return 的,函數(shù)中的 this 就是創(chuàng)建并返回的新對象了。

但當(dāng) new 一個有 return 的構(gòu)造函數(shù)時,如果 return 的是基本類型,則 return 的數(shù)據(jù)直接被拋棄。

如果 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 (中國標(biāo)準(zhǔn)時間)
console.log(person);

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

小Q:新知識get√

要點歸納

1. 除箭頭函數(shù)外的所有函數(shù)都可以作為構(gòu)造函數(shù)被new

2. 函數(shù)內(nèi)this指向問題

3. 構(gòu)造函數(shù)return問題

4. 構(gòu)造函數(shù)命名規(guī)范

03、原型

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

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

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

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

// 通過構(gòu)造函數(shù)拿到原型
let proto3 = Person.prototype;

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

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

老白:當(dāng)你聲明一個函數(shù)時,系統(tǒng)就自動幫你生成了一個關(guān)聯(lián)的原型啦,當(dāng)然它也是一個普通對象,包含 constructor 字段指向構(gòu)造函數(shù),并且構(gòu)造函數(shù)的 prototype 屬性也會指向這個原型。

當(dāng)你用構(gòu)造函數(shù)創(chuàng)建對象時,系統(tǒng)又幫你把對象的 __proto__ 屬性指向原型。

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

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

小Q:它們的引用關(guān)系,稍微有點繞啊~

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

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

圖片圖片

圖片圖片

小Q:懂了!

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

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

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

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

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

老白:一句話總結(jié):當(dāng)訪問對象的屬性不存在時,就會去訪問原型的屬性。

圖片圖片

圖3

老白:我們可以通過代碼驗證下,person 對象是沒有 age 屬性的,所以 person.age 返回的其實是原型的 age 屬性值,當(dāng)原型的 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 還是原型?

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

后續(xù)當(dāng)你再訪問 person.age 時,因為 person 對象已經(jīng)存在 age 屬性了,就不會再檢索到原型上了。

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

圖片圖片

圖4

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

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

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 ,當(dāng) 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也會被影響,打?。?9
console.log(person2.info.age);

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

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

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

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

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

老白:不僅如此,很多系統(tǒng)自帶的構(gòu)造函數(shù)產(chǎn)生的對象,其方法都是掛載在原型上的。比如我們經(jīng)常用的數(shù)組方法,你以為是數(shù)組對象自己的方法嗎?不,是數(shù)組原型 Array.prototype 的方法,我們可以驗證下。

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

圖片圖片

小Q:【若有所思】

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

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

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

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

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

let array = [];
// 打?。罕O(jiān)聽push完成,執(zhí)行一些操作
array.push(1, 2);
// 打?。篬1, 2]
console.log(array);

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

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

小Q:又又漲知識了~

老白:原型相關(guān)的也說的差不多了,結(jié)合剛剛討論的構(gòu)造函數(shù),考你一個:手寫一個 new 函數(shù)。

小Q:啊啊,提示一下?

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

  1. 創(chuàng)建一個對象,綁定原型;
  2. 以這個對象為 this 指向執(zhí)行構(gòu)造函數(shù)。

小Q:我試試~

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

小Q:試驗通過!

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

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

老白:不錯不錯,讓我?guī)湍阍偕晕⑼晟埔幌潞俸賬

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

要點歸納

1. 對象,構(gòu)造函數(shù),原型三者的引用關(guān)系

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

3. 手寫new函數(shù)

04、原型鏈

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

小Q:返回 undefined?

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

圖片圖片

小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 (中國標(biāo)準(zhǔn)時間)
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 ?

老白:完全正確。最后再考你一道和原型鏈相關(guān)的題,手寫 instanceOf 函數(shù)。提示一下,instanceOf 的原理是判斷構(gòu)造函數(shù)的 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函數(shù)

05、類

小Q:好不容易把構(gòu)造函數(shù)和原型都弄懂,怎么 ES6 又推出類呀,學(xué)不動了 T_T。

老白:不慌,類其實只是種語法糖,本質(zhì)上還是”構(gòu)造函數(shù)+原型“。

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

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

我們再將這種 class 語法糖寫法還原成構(gòu)造函數(shù)寫法。

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

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

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

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

// Person類本質(zhì)是個構(gòu)造函數(shù),打?。篺unction
console.log(typeof Person);

// Person的靜態(tài)屬性,打?。篹
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);
// 對象的構(gòu)造函數(shù)就是Person,打?。簍rue
console.log(person.constructor == Person);

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

小Q:不過我發(fā)現(xiàn)一個問題,在 class 寫法中的原型屬性只能是函數(shù),不能是數(shù)據(jù)?

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

要點歸納

1. 類的語法

2. 類還原成構(gòu)造函數(shù)寫法

06、繼承

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

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

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

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

小Q:讓 Animal 對象充當(dāng) 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);

老白:沒錯,這是我們學(xué)完原型之后,最直觀的一種繼承實現(xiàn)方式,這種繼承又叫原型鏈?zhǔn)嚼^承。但是這種繼承方式存在 2 個缺點:

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

// 創(chuàng)建子類對象時無法向父類構(gòu)造函數(shù)傳參
let cat = new Cat("哺乳動物");

// type屬性來自原型,被所有Cat對象共享,打?。簎ndefined
console.log(cat.type);

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

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

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

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

老白:這種繼承方式叫 構(gòu)造函數(shù)式繼承,確實解決了 原型鏈?zhǔn)嚼^承 帶來的問題,不過這種繼承方式因為沒有用到原型,又有產(chǎn)生了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("哺乳動物");

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

小Q:看來還要再改進,不如我把 原型鏈?zhǔn)?nbsp;和 構(gòu)造函數(shù)式 這 2 種繼承方式都用上,讓它們互補。

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

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

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

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

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

// 子類對象屬于父類的實例,打?。簍rue
console.log(cat instanceof Animal);

老白:非常聰明,你又道出了第三種繼承方式,組合式繼承。即 原型鏈?zhǔn)?+ 構(gòu)造函數(shù)式 = 組合式。問題確實都解決了,但是有沒有發(fā)現(xiàn),這種方式執(zhí)行了 2 遍父類構(gòu)造函數(shù)。

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

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

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

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

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

// 改進:創(chuàng)建一個空對象做子類原型,并且這個空對象的原型是父類原型
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 = "貓";
}
// 寄生組合式,改進了組合式,少執(zhí)行了一遍沒必要的父類構(gòu)造函數(shù)
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

老白:這種繼承方式又叫 寄生組合式繼承,相當(dāng)于在組合式繼承的基礎(chǔ)上進一步優(yōu)化。回顧上面的幾種繼承方式的演變過程,原型鏈?zhǔn)?→ 構(gòu)造函數(shù)式 → 組合式 → 寄生組合式, 其實就是不斷優(yōu)化的過程,最終我們才推理出比較完美的繼承方式。

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

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

class Animal {
    eat() {
        console.log("吃");
    }
    constructor(type) {
        this.type = type;
    }
}
// Cat繼承Animal
class Cat extends Animal {
    constructor(type) {
        // 執(zhí)行父類構(gòu)造函數(shù),相當(dāng)于 Animal.call(this, type);
        super(type);
        
        // 執(zhí)行完super(),子類對象就有父類屬性了,打?。翰溉閯游?        console.log(this.type);
        
        this.name = "貓";
    }
}

創(chuàng)建對象試驗一下:

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

// 子類原型的原型就是父類原型,打?。簍rue
console.log(Cat.prototype.__proto__ == Animal.prototype);

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

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

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

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

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

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

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

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

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

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

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

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

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

要點歸納

  1. 原型鏈?zhǔn)嚼^承
  2. 構(gòu)造函數(shù)式繼承
  3. 組合式繼承
  4. 寄生組合式繼承
  5. extends 繼承

07、總結(jié)

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

責(zé)任編輯:武曉燕 來源: 搜狐技術(shù)產(chǎn)品
相關(guān)推薦

2011-08-24 13:56:27

JavaScript

2022-06-20 09:22:55

js原型鏈前端

2009-08-13 18:26:35

C#繼承構(gòu)造函數(shù)

2011-08-31 14:48:33

JavaScript

2009-09-18 13:40:40

繼承關(guān)系

2009-08-13 18:15:06

C#繼承構(gòu)造函數(shù)

2022-03-29 09:15:55

Javascript函數(shù)屬性

2010-02-02 17:39:31

C++構(gòu)造函數(shù)

2010-01-27 16:10:32

C++靜態(tài)構(gòu)造函數(shù)

2009-08-13 18:36:36

C#繼承構(gòu)造函數(shù)

2013-09-18 14:01:46

JavaScript

2022-04-14 20:43:24

JavaScript原型鏈

2009-12-10 13:37:16

PHP parent

2010-01-27 10:13:22

C++類對象

2009-12-11 10:42:00

Scala講座類定義構(gòu)造函數(shù)

2010-01-25 14:00:27

C++類

2011-08-24 13:51:56

JavaScript

2020-04-29 14:40:19

JavaScript繼承編程語言

2021-07-16 04:56:03

NodejsAddon

2020-09-10 07:04:30

JSJavaScript 原型鏈
點贊
收藏

51CTO技術(shù)棧公眾號