【機制】JavaScript的原型、原型鏈、繼承


1.原型和原型鏈的概念

js在創建一個對象時,比如叫 obj,都會給他偷偷的加上一個引用,這個引用指向的是一個對象,比如叫 yuanxing
這個對象可以給引用它的對象提供屬性共享,比如:yuanxing上有個屬性name,可以被 obj.name訪問到,
這個可以提供屬性共享的對象,就稱為前面對象的原型

而原型本身也是一個對象,所以它也會有一個自己的原型,這一層一層的延續下去,直到最后指向 null,這就形成的 原型鏈

那js的這一種是原型機制是怎么實現的呢?

2.原型的實現

我們先從一個例子來看:

//code-01
let obj = new Object({name:'xiaomin'})
console.log(obj.name)
console.log(obj.toString())

// xiaomin
// [object Object]

我們首先創建了一個對象obj,它有一個屬性name
屬性name是我們創建的,但是在創建obj的時候,並沒有給它創建toString屬性,為什么obj.toString()可以訪問的到呢?

prototype 屬性
我們先來看一下Object.prototype屬性

我們發現這里有toString屬性,所以其實Object.prototype就是obj的原型,按照原型的概念,它能夠為obj提供屬性共享
所以當obj.toString()的時候,先在obj的屬性里找,沒找到,就在obj的原型Object.prototype的屬性上找,可以找到,所以調用成功

proto 屬性
那么obj是怎么找到原型的呢?我們打印obj屬性看一下:

我們發現obj除了我們創建的name以外,還有一個__proto__屬性,它的值是一個對象,那么它等於什么呢?

我們發現obj.__proto__指向的就是Object.prototype

到此為止,我們可以簡單總結一下js語言實現原型的基本機制了

  • 在創建一個對象obj時,會給它加上一個__proto__屬性
  • __proto__屬性 指向obj構造函數prototype對象
  • 當訪問obj的某個屬性時,會先在它自己的屬性上找,如果沒找到,就在它的原型(其實就是__proto__指向的對象)的屬性上找

構造函數
這個有一個構造函數的概念,其實構造函數也就是普通函數,當這個函數被用來 new一個新對象時,它就被稱為新對象的 構造函數
就上面的例子而言,Objec就是obj的構造函數,
這里要區別一下Objectobject的區別,前者是一個js內置的一個函數,后者是js的基本數據類型(number,string,function,object,undefined)

3.new 實際上做了什么

上面有說到new關鍵字,那么在它實際上做了什么呢?
上面代碼code-01使用系統內置的函數Object來創建對象的,那么我們現在用自己創建的函數來創建一個新對象看看:

//code-02

function human(name){
  this.name = name
}
human.prototype.say = function(){
  alert('我叫'+this.name)
}
let xiaomin = new human('xiaoming')

console.log(xiaomin.name)
xiaomin.say()

這里的human就是新對象xiaoming的構造函數
我們把新創建的對象xiaoming打印出來看看:

我們看到xiaoming有一個屬性name,並且xiaoming.__proto__完全等於構造函數的human.prototype,這就是它的原型
從這里我們可以總結一下new的基本功能:

  • 構造函數的this指向新創建的對象xiaoming
  • 為新對象創建原型,其實就是把新對象的__proto__指向構造函數的prototype

手寫new
上面我們了解了new具體做了什么事情,那么它是怎么實現這些功能的呢?下面我們手寫一個函數myNew來模擬一下new的效果:

//code-03
    function human(name, age) {
      this.name = name;
      this.age = age;
    }
    human.prototype.say = function () {
      console.log("my name is " + this.name);
    };

    xiaoming = myNew(human, "xiaoming", 27);

    function myNew() {
      let obj = new Object();
      //取出函數的第一個參數,其實就是 human函數
      let argArr = Array.from(arguments);
      const constructor = argArr.shift();
      // 指定原型
      obj.__proto__ = constructor.prototype;
      //改變函數執行環境
      constructor.apply(obj, argArr);
      return obj;
    }

    xiaoming.say();

我們先把新對象xiaoming打印出來看一下:

我們發現這和上面的代碼code-02的效果是一樣的
上面代碼code-03里面如果對apply的作用不太熟悉的,可以另外了解一下,其實也很簡單,意思就是:在obj的環境下,執行constructor函數,argArr是函數執行時的參數,也就是指定了函數的this

4.繼承的實現

其實就上面的內容,就可以對js的原型機制有個基本的了解,但是一般面試的時候,如果有問到原型,接下來就會問 能不能實現 繼承的功能,所以我們來手寫一下 原型的繼承,其實所用到的知識點都是上面有提到的

繼承的概念
我們先來說下繼承的概念:
繼承其實就是 一個構造函數(子類)可以使用另一個構造函數(父類)的屬性和方法,這里有幾點注意的:

  • 繼承是 構造函數 對 另一個構造函數而言
  • 需要實現屬性的繼承,即 this的轉換
  • 需要實現方法的繼承,一般就是指 原型鏈的構建

繼承的實現
基於上面的3點要素,我們先直接來看代碼:

// code-04
   // 父級 函數
    function human(name) {
      this.name = name;
    }
    human.prototype.sayName = function () {
      console.log("我的名字是:", this.name);
    };

    // 子級 函數
    function user(args) {
      this.age = args.age;
      //1.私有屬性的繼承
      human.call(this, args.name); 
      //2.原型的繼承
      Object.setPrototypeOf(user.prototype, human.prototype); //原型繼承-方法1
      // user.prototype.__proto__ = human.prototype; // 原型繼承-方法2
    }
    // 因為重新賦值了prototype,所以放置 user 外部
    // user.prototype = new human();//原型繼承-方法3
    // user.prototype = Object.create(human.prototype);//原型繼承-方法4

    user.prototype.sayAge = function () {
      console.log("我的年齡是:", this.age);
    };
    let person = new human("人類");
    let xiaoming = new user({ name: "xiaoming", age: 27 });

    console.log("----父類-----");
    console.log(person);
    person.sayName();

    console.log("----子類-----");
    console.log(xiaoming);
    xiaoming.sayName();
    xiaoming.sayAge();

我們先來看下打印的結果:

從打印結果,我們可以看到xiaoming擁有person的屬性和方法(name,sayName),又有自己私有的屬性方法(age,sayAge),這是因為構造函數user實現了對human的繼承。
其實實現的方法無非也就是我們前面有說到的 作用域的改變和原型鏈的構造,其中作用域的改變(this指向的改變)主要是兩個方法:call和apply,原型鏈的構造原理只有一個,就是對象的原型等於其構造函數的prototype屬性,但是實現方法有多種,代碼code-04中有列出4種。
從上面的例子來看原型鏈的指向是:xiaoming->user.prototype->human.prototype

5.class和extends

我們可能有看到一些代碼直接用 classextends關鍵字來實現類和繼承,其實這是ES6的語法,其實是一種語法糖,本質上的原理也是相同的。我們先來看看基本用法:
用法

//code-05
   class human {
      //1.必須要有構造函數
      constructor(name) {
        this.name = name;
      }//2.不能有逗號`,`
      sayName() {
        console.log("sayName:", this.name);
      }
    }

    class user extends human {
      constructor(params) {
        //3.子類必須用`super`,調用父類的構造函數
        super(params.name);
        this.age = params.age;
      }
      sayAge() {
        console.log("sayAge:", this.age);
      }
    }

    let person = new human("人類");
    let xiaoming = new user({ name: "xiaoming", age: 27 });

    console.log("----<human> person-----");
    console.log(person);
    person.sayName();

    console.log("----<user> xiaoming-----");
    console.log(xiaoming);
    xiaoming.sayName();
    xiaoming.sayAge();

執行結果:

我們看到執行的結果和上面的代碼code-04是一樣的,但是代碼明顯清晰了很多。幾個注意的地方:

  • class類中必須要有構造函數constructor,
  • class類中的函數不能用 ,分開
  • 如果要繼承父類的話,在子類的構造函數中,必須先執行 super來調用的父類的構造函數

相同
上面有說class的寫法其實原理上和上面是一樣的,我們來驗證一下

  1. 首先看看userhuman是什么類型

    這里看出來了,所以雖然被class修飾,本質上還是函數,和代碼code-04中的user,human函數是一樣的

  2. 再來看看prototype屬性

    這里看出來sayName,sayAge都是定義在human.prototypeuser.prototype上,和代碼code-04中也是一樣的

  3. 我們再來看看原型鏈

    這與代碼code-04中的原型鏈的指向也是一樣:xiaoming->user.prototype->human.prototype

差異
看完相同點,現在我們來看看不同點:

  1. 首先寫法上的不同
  2. class聲明的函數,必須要用new調用
  3. class內部的成員函數沒有prototype屬性,不可以用new調用
  4. class 內的代碼自動是嚴格模式
  5. class聲明不存在變量提升,這一點和 let一樣,比如:
    //code-06
    console.log(name_var);
    var name_var = "xiaoming";
    //undefined,不會報錯,var聲明存在變量提升
    
    console.log(name_let);
    let name_let = "xiaoming";
    // Uncaught ReferenceError: Cannot access 'name_let' before initialization
    //報錯,let聲明不存在變量提升
    
    new user();
    class user {}
    // Uncaught ReferenceError: Cannot access 'user' before initialization
    //報錯,class聲明不存在變量提升
    
    
  6. class內的方法都是不可枚舉的,比如:
      //code-07
      class human_class {
      constructor(name) {
        this.name = name;
      }
      sayName() {
        console.log("sayName:", this.name);
      }
    }
    function human_fun(name) {
      this.name = name;
    }
    human_fun.prototype.sayName = function () {
      console.log("sayName:", this.name);
    };
    console.log("----------human_class-----------");
    console.log("prototype屬性", human_class.prototype);
    console.log("prototype 枚舉", Object.keys(human_class.prototype));
    
    console.log("----------human_fun-----------");
    console.log("prototype屬性", human_fun.prototype);
    console.log("prototype 枚舉", Object.keys(human_fun.prototype));
    
    運行結果:

6.總結

簡單總結一下:

  • 每個對象在創建的時候,會被賦予一個__proto__屬性,它指向創建這個對象的構造函數的prototype,而prototype本身也是對象,所以也有自己的__proto__,這就形成了原型鏈,最終的指向是 Object.prototype.__proto__ == null
  • 可以通過new,Object.create(),Object.setPrototypeOf(),直接賦值__proto__等方法為一個對象指定原型
  • new操作符實際做的工作是:創建一個對象,把這個對象作為構造函數的this環境,並把這個對象的原型(proto)指向構造函數的prototype,最后返回這個對象
  • 繼承主要實現的功能是:this指向的綁定,原型鏈的構建
  • ES6的語法classextends可以提供更為清晰簡潔的寫法,但是本質上的原理大致相同


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM