這些js原型及原型鏈面試題你能做對幾道

腹黑的可樂發表於2023-01-12

一、前言

在面試過程中,頻頻被原型相關知識問住,每次回答都支支吾吾。後來有家非常心儀的公司,在二面時,果不其然,又問原型了!

我痛下決心用了兩天時間鑽研了下原型,弄明白後發現世界都明亮了,原來這麼簡單 ~

有些理解還比較淺薄,隨著時間的推移和理解的深入,以後還會補充。如果大家發現我理解的有問題,歡迎大家在評論中指正。話不多說,切入正題。

二、建構函式

講原型則離不開建構函式,讓我們先來認識下建構函式。

2.1 建構函式分為 例項成員 和 靜態成員

讓我們先來看看他們分別是什麼樣子的。

例項成員: 例項成員就是在建構函式內部,透過this新增的成員。例項成員只能透過例項化的物件來訪問。

靜態成員: 在建構函式本身上新增的成員,只能透過建構函式來訪問

    function Star(name,age) {
        //例項成員
        this.name = name;
        this.age = age;
    }
    //靜態成員
    Star.sex = '女';

    let stars = new Star('小紅',18);
    console.log(stars);      // Star {name: "小紅", age: 18}
    console.log(stars.sex);  // undefined     例項無法訪問sex屬性

    console.log(Star.name); //Star     透過建構函式無法直接訪問例項成員
    console.log(Star.sex);  //女       透過建構函式可直接訪問靜態成員

2.2 透過建構函式建立物件

該過程也稱作例項化

2.2.1 如何透過建構函式建立一個物件?
 function Father(name) {
     this.name = name;
 }
 let son = new Father('Lisa');
 console.log(son); //Father {name: "Lisa"}

此時,son就是一個新物件。

2.2.2 new一個新物件的過程,發生了什麼?

(1) 建立一個空物件 son {}
(2) 為 son 準備原型鏈連線 son.__proto__ = Father.prototype
(3) 重新繫結this,使建構函式的this指向新物件 Father.call(this)
(4) 為新物件屬性賦值 son.name
(5) 返回this return this,此時的新物件就擁有了建構函式的方法和屬性了

2.2.3 每個例項的方法是共享的嗎?

這要看我們如何定義該方法了,分為兩種情況。

方法1:在建構函式上直接定義方法(不共享)
    function Star() {
        this.sing = function () {
            console.log('我愛唱歌');
        }
    }
    let stu1 = new Star();
    let stu2 = new Star();
    stu1.sing();//我愛唱歌
    stu2.sing();//我愛唱歌
    console.log(stu1.sing === stu2.sing);//false

很明顯,stu1 和 stu2 指向的不是一個地方。
所以 在建構函式上透過this來新增方法的方式來生成例項,每次生成例項,都是新開闢一個記憶體空間存方法。這樣會導致記憶體的極大浪費,從而影響效能。

方法2:透過原型新增方法(共享)

建構函式透過原型分配的函式,是所有物件共享的。

    function Star(name) {
        this.name = name;
    }
    Star.prototype.sing = function () {
        console.log('我愛唱歌', this.name);
    };
    let stu1 = new Star('小紅');
    let stu2 = new Star('小藍');
    stu1.sing();//我愛唱歌 小紅
    stu2.sing();//我愛唱歌 小藍
    console.log(stu1.sing === stu2.sing);//true
2.2.4 例項的屬性為基本型別是,它們是共享的嗎?

屬性儲存的是如果儲存的是基本型別,不存在共享問題,是否相同要看值內容。

    let stu1 = new Star('小紅');
    let stu2 = new Star('小紅');
    console.log(stu1.name === stu2.name);//true

    let stu1 = new Star('小紅');
    let stu2 = new Star('小藍');
    console.log(stu1.name === stu2.name);//false
2.2.5 定義建構函式的規則

公共屬性定義到建構函式里面,公共方法我們放到原型物件身上。

三、原型

前面我們在 例項化 和 例項共享方法 時,都提到了原型。那麼現在聊聊這個神秘的原型到底是什麼?

3.1 什麼是原型?

Father.prototype 就是原型,它是一個物件,我們也稱它為原型物件。

3.2 原型的作用是什麼?

原型的作用,就是共享方法。
我們透過 Father.prototype.method 可以共享方法,不會反應開闢空間儲存方法。

3.3 原型中this的指向是什麼?

原型中this的指向是例項。

四、原型鏈

4.1 什麼是原型鏈?

原型與原型層層相連結的過程即為原型鏈。

4.2 原型鏈應用

物件可以使用建構函式prototype原型物件的屬性和方法,就是因為物件有__proto__原型的存在
每個物件都有__proto__原型的存在

參考 前端進階面試題詳細解答

function Star(name,age) {
    this.name = name;
    this.age = age;
}
Star.prototype.dance = function(){
    console.log('我在跳舞',this.name);
};
let obj = new Star('張萌',18);
console.log(obj.__proto__ === Star.prototype);//true

4.3 原型鏈圖

4.4 原型查詢方式

例如:查詢obj的dance方法

    function Star(name) {
        this.name = name;

        (1)首先看obj物件身上是否有dance方法,如果有,則執行物件身上的方法
        this.dance = function () {
            console.log(this.name + '1');
        }
    }

    (2)如果沒有dance方法,就去建構函式原型物件prototype身上去查詢dance這個方法。
    Star.prototype.dance = function () {
        console.log(this.name + '2');
    };

    (3)如果再沒有dance方法,就去Object原型物件prototype身上去查詢dance這個方法。
    Object.prototype.dance = function () {
        console.log(this.name + '3');
    };
    (4)如果再沒有,則會報錯。
    let obj = new Star('小紅');
    obj.dance();

(1)首先看obj物件身上是否有dance方法,如果有,則執行物件身上的方法。

(2)如果沒有dance方法,就去建構函式原型物件prototype身上去查詢dance這個方法。

(3)如果再沒有dance方法,就去Object原型物件prototype身上去查詢dance這個方法。

(4)如果再沒有,則會報錯。

4.5 原型的構造器

原型的構造器指向建構函式。

    function Star(name) {
        this.name = name;
    }
    let obj = new Star('小紅');
    console.log(Star.prototype.constructor === Star);//true
    console.log(obj.__proto__.constructor === Star); //true
4.5.1 在原型上新增方法需要注意的地方

方法1:建構函式.prototype.方法在原型物件上直接新增方法,此時的原型物件是有constructor構造器的,構造器指向建構函式本身

    function Star(name) {
        this.name = name;
    }
    Star.prototype.dance = function () {
        console.log(this.name);
    };
    let obj = new Star('小紅');
    console.log(obj.__proto__);  //{dance: ƒ, constructor: ƒ}
    console.log(obj.__proto__.constructor);  // Star

方法2:Star.prototype = {}給原型重新賦值,此時會丟失構造器,我們需要手動定義構造器,指回建構函式本身

    function Star(name) {
        this.name = name;
    }
    Star.prototype = {
        dance: function () {
            console.log(this.name);
        }
    };
    let obj = new Star('小紅');
    console.log(obj.__proto__); //{dance: ƒ}
    console.log(obj.__proto__.constructor); //  ƒ Object() { [native code] }
    Star.prototype.constructor = Star;
4.5.2 一般不允許直接改變原型prototype指向

改變原型指向,會使原生的方法都沒了,所以Array、String這些內建的方法是不允許改變原型指向的。如果改變了,就會報錯。

    Array.prototype.getSum = function (arr) {
        let sum = 0;
        for (let i = 0; i < this.length; ++i) {
            sum += this[i];
        }
        return sum;
    };
    let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    console.log(arr.getSum());//45

如果改變prototype指向,會報錯!

    Array.prototype = {
        getSum: function (arr) {
            let sum = 0;
            for (let i = 0; i < this.length; ++i) {
                sum += this[i];
            }
            return sum;
        }
    };
    let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    console.log(arr.getSum());//45

五、繼承 - ES5方法

ES6之前並沒有給我們提供extends繼承,我們可以透過建構函式+原型物件模擬實現繼承。

繼承屬性,利用call改變this指向。但該方法只可以繼承屬性,例項不可以使用父類的方法。

    function Father(name) {
        this.name = name;
    }
    Father.prototype.dance = function () {
      console.log('I am dancing');
    };
    function Son(name, age) {
        Father.call(this, name);
        this.age = age;
    }
    let son = new Son('小紅', 100);
    son.dance();   //報錯

如何繼承父類的方法呢?

解決方法1:利用Son.prototype = Father.prototype改變原型指向,但此時我們給子類增加原型方法,同樣會影響到父類。

    function Father(name) {
        this.name = name;
    }
    Father.prototype.dance = function () {
        console.log('I am dancing');
    };
    function Son(name, age) {
        Father.call(this, name);
        this.age = age;
    }
    Son.prototype = Father.prototype;
    //為子類新增方法
    Son.prototype.sing = function () {
        console.log('I am singing');
    };
    let son = new Son('小紅', 100);
    //此時父類也被影響了
    console.log(Father.prototype) //{dance: ƒ, sing: ƒ, constructor: ƒ}

解決方法2:子類的原型指向父類的例項,這樣就可以順著原型鏈共享父類的方法了。並且為子類新增原型方法的時候,不會影響父類。

    function Father(name) {
        this.name = name;
    }
    Father.prototype.dance = function () {
        console.log('I am dancing');
    };
    function Son(name, age) {
        Father.call(this, name);
        this.age = age;
    }
    Son.prototype = new Father();
    Son.prototype.sing = function () {
        console.log('I am singing');
    };
    let son = new Son('小紅', 100);
    console.log(Father.prototype) //{dance: ƒ, constructor: ƒ}

七、類

什麼是類?

類的本質還是一個函式,類就是建構函式的另一種寫法。

function Star(){}
console.log(typeof Star); //function

class Star {}
console.log(typeof Star); //function

ES6中類沒有變數提升

透過建構函式建立例項,是可以變數提升的。
es6中的類,必須先有類,才可以例項化。

類的所有方法都定義在類的prototype屬性上面

讓我們來測試一下。

    class Father{
        constructor(name){
            this.name = name;
        }
        sing(){
            return this.name;
        }
    }
    let red = new Father('小紅');
    let green = new Father('小綠');
    console.log(red.sing === green.sing); //true

向類中新增方法

透過Object.assign,在原型上追加方法。

    class Father{
        constructor(name){
            this.name = name;
        }
        sing(){
            return this.name;
        }
    }
    //在原型上追加方法
    Object.assign(Father.prototype,{
        dance(){
            return '我愛跳舞';
        }
    });
    let red = new Father('小紅');
    let green = new Father('小綠');
    console.log(red.dance());//我愛跳舞
    console.log(red.dance === green.dance); //true

constructor方法

constructor方法是類的預設方法,透過new命令生成物件例項時,自動呼叫該方法。一個類必須有constructor方法,如果沒有顯式定義,一個空的constructor方法會被預設新增。

八、繼承 - ES6方法

    class Father {
        constructor(name){
            this.name = name;
        }
        dance(){
            return '我在跳舞';
        }
    }
    class Son extends Father{
        constructor(name,score){
            super(name);
            this.score = score;
        }
        sing(){
            return this.name +','+this.dance();
        }
    }
    let obj = new Son('小紅',100);

九、類和建構函式的區別

(1) 類必須使用new呼叫,否則會報錯。這是它跟普通建構函式的一個主要區別,後者不用new也可以執行。

(2) 類的所有例項共享一個原型物件。

(3) 類的內部,預設就是嚴格模式,所以不需要使用use strict指定執行模式。

十、總結

建構函式特點:

1.建構函式有原型物件prototype。

2.建構函式原型物件prototype裡面有constructor,指向建構函式本身。

3.建構函式可以透過原型物件新增方法。

4.建構函式建立的例項物件有__proto__原型,指向建構函式的原型物件。

類:

1.class本質還是function

2.類的所有方法都定義在類的prototype屬性上

3.類建立的例項,裡面也有__proto__指向類的prototype原型物件

4.新的class寫法,只是讓物件原型的寫法更加清晰,更像物件導向程式設計的語法而已。

5.ES6的類其實就是語法糖。

十一、什麼是語法糖

什麼是語法糖?加糖後的程式碼功能與加糖前保持一致,糖在不改變其所在位置的語法結構的前提下,實現了執行時的等價。

語法糖沒有改變語言功能,但增加了程式設計師的可讀性。

十二、面試題分享

面試題1

Object.prototype.__proto__    //null
Function.prototype.__proto__  //Object.prototype
Object.__proto__              //Function.prototype

講解:
這裡涉及到Function的原型問題,附一張圖,這圖是一個面試官發給我的,我也不知道原作者在哪裡~

面試題2

給大家分享那道我被卡住的面試題,希望大家在學習完知識後,可以回顧一下。

按照如下要求實現Person 和 Student 物件
 a)Student 繼承Person 
 b)Person 包含一個例項變數 name, 包含一個方法 printName
 c)Student 包含一個例項變數 score, 包含一個例項方法printScore
 d)所有Person和Student物件之間共享一個方法

es6類寫法

    class Person {
        constructor(name) {
            this.name = name;
        }
        printName() {
            console.log('This is printName');
        }
        commonMethods(){
            console.log('我是共享方法');
        }
    }

    class Student extends Person {
        constructor(name, score) {
            super(name);
            this.score = score;
        }
        printScore() {
            console.log('This is printScore');
        }
    }

    let stu = new Student('小紅');
    let person = new Person('小紫');
    console.log(stu.commonMethods===person.commonMethods);//true

原生寫法

    function Person (name){
        this.name = name;
        this.printName=function() {
            console.log('This is printName');
        };
    }
    Person.prototype.commonMethods=function(){
        console.log('我是共享方法');
    };

    function Student(name, score) {
        Person.call(this,name);
        this.score = score;
        this.printScore=function() {
            console.log('This is printScore');
        }
    }
    Student.prototype = new Person();
    let person = new Person('小紫',80);
    let stu = new Student('小紅',100);
    console.log(stu.printName===person.printName);//false
    console.log(stu.commonMethods===person.commonMethods);//true

相關文章