構(gòu)造函數(shù)、原型鏈、繼承簡單理解

1. 創(chuàng)建對象的三個(gè)方法

創(chuàng)建一個(gè)對象一般有三種方法:

  1. 字面量創(chuàng)建, var obj = {};
  2. 通過Object創(chuàng)建, var obj = new Object();
  3. 通過構(gòu)造函數(shù)創(chuàng)建:
function Person(name, age) {
    this.name = name;
    this.age = age;
}
const jack = new Person('Jack', 18);

上面代碼中的 new 在執(zhí)行時(shí)會做四件事情:

  1. 在內(nèi)存中創(chuàng)建一個(gè)新的空對象。
  2. 讓 this 指向這個(gè)對象。
  3. 執(zhí)行構(gòu)造函數(shù)的代碼。
  4. 返回這個(gè)對象(所以構(gòu)造函數(shù)不需要 return)。

2. 靜態(tài)成員與實(shí)例成員

function Person(name, age) {
  this.name = name; // 實(shí)例成員
  this.age = age;
  this.sing = function () {
    console.log('我在唱歌');
  };
}

Person.height = 180; // 靜態(tài)成員

const jack = new Person('Jack', 22);
const lily = new Person('Lily', 22);

console.log(Person.height); // 180
console.log(jack.height); // undefined
console.log(jack.sing === lily.sing); // false  這里可以看出,多個(gè)實(shí)例調(diào)用相同的方法,會造成內(nèi)存浪費(fèi)

上面代碼可以看出:
靜態(tài)成員只能使用構(gòu)造函數(shù)調(diào)用,不能通過實(shí)例調(diào)用。
多個(gè)實(shí)例調(diào)用同樣的實(shí)例成員函數(shù),會各自存儲一份,造成內(nèi)存浪費(fèi)。
那么,如何解決呢?

3. 原型和原型鏈

構(gòu)造函數(shù)存在內(nèi)存浪費(fèi)的情況,可以通過原型對象上定義屬性、方法解決。構(gòu)造函數(shù)原型上的方法,能夠被構(gòu)造函數(shù)實(shí)例調(diào)用。
每一個(gè)構(gòu)造函數(shù)都有一個(gè) prototype 屬性,指向一個(gè)對象。這個(gè)對象的所有屬性和方法都會被構(gòu)造函數(shù)所擁有。
我們可以把那些不變的方法直接定義到 prototype 上,這樣所有的實(shí)例都可以共享這些方法。
代碼如下:

Person.prototype.say = function () {
  console.log('我在說話');
};
jack.say();
lily.say();
console.log(jack.say === lily.say);

每個(gè)對象都會有一個(gè)__proto__屬性,指向其構(gòu)造函數(shù)的原型對象。也就是說,對象的__proto__屬性和構(gòu)造函數(shù)的 prototype 屬性是等價(jià)的。
實(shí)例對象和原型對象都有一個(gè)constructor屬性,指向構(gòu)造函數(shù)本身。
我們調(diào)用一個(gè)函數(shù)的屬性時(shí),編譯器會先看對象本身是否有這個(gè)屬性,如果沒有就到對象的__proto__屬性上去找,如果還找不到,繼續(xù)找__proto____proto__屬性,直到 null 為止。
把構(gòu)造函數(shù)、實(shí)例對象、原型對象的關(guān)系用圖畫出來,如下:


這就是原型鏈。

其實(shí)構(gòu)造函數(shù)是Function的實(shí)例,Function的原型是一個(gè)對象,是Object的實(shí)例。我們繼續(xù)拓展,把圖畫下來。如下:

4. 使用原型擴(kuò)展內(nèi)置對象方法

我們可以使用原型來擴(kuò)展內(nèi)置對象的方法。比如,我們可以這樣擴(kuò)展數(shù)組的內(nèi)置方法:

Array.prototype.sum = function () {
  let sum = 0;
  for (let i = 0; i < this.length; i++) {
    sum += this[i]; // prototype 里的 this 指向調(diào)用原型方法的那個(gè)對象。
  }
  return sum;
};

console.log([2, 3, 4].sum()); // 9

5. 繼承

    1. 構(gòu)造函數(shù)繼承:只能繼承構(gòu)造函數(shù)里的屬性和方法
      其實(shí)就是在子構(gòu)造函數(shù)里面調(diào)用父構(gòu)造函數(shù)(需要修改this指向),這樣就把父構(gòu)造函數(shù)里的屬性和方法放到了子構(gòu)造函數(shù)里了。
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.say = function () {
    console.log('你好');
  };
}
Person.prototype.sing = function () {
  console.log('唱歌');
};

function Student(name, age, score) {
  Person.call(this, name, age); // 由于Person里的this指向?yàn)镻erson的實(shí)例,這里修改this指向?yàn)镾tudent的實(shí)例
  this.score = score;
}

const jack = new Student('Jack', 18, 100);
console.log(jack.name); // Jack
jack.say(); // 你好
jack.sing(); // 無結(jié)果,Student沒有繼承Person原型上的方法
    1. 原型鏈繼承
      通過上面的例子我們可以看出,構(gòu)造函數(shù)繼承并不能繼承原型鏈上的方法。那我們怎么才能繼承父構(gòu)造函數(shù)原型上的方法呢?
      我們可以把子構(gòu)造函數(shù)原型指向父構(gòu)造函數(shù)的原型。
Student.prototype = Person.prototype; // - 原始版 缺陷:Student的原型改變導(dǎo)致Person原型改變,因此不可取

原型一樣,自然能夠繼承原型上的方法。
但是這樣一來就會出現(xiàn)問題,原型是個(gè)對象,是引用類型數(shù)據(jù),我們再修改Student.prototype會導(dǎo)致Person.prototype的變化。這是不合理的。比如:

Student.prototype.dance = function () {
  console.log('跳舞');
};

const jack = new Person('Jack', 18);
jack.dance(); // 跳舞    Person的原型上是沒有dance方法的。這里是因?yàn)槲覀償U(kuò)展了Student的原型導(dǎo)致Person原型變化

常用的解決辦法是,我們將子構(gòu)造函數(shù)的原型指向福構(gòu)造函數(shù)的一個(gè)實(shí)例。

Student.prototype = new Person();

前面說過,調(diào)用一個(gè)函數(shù)的屬性時(shí),編譯器會先看對象本身是否有這個(gè)屬性,如果沒有就到對象的__proto__屬性上去找,如果還找不到,繼續(xù)找__proto____proto__屬性,直到 null 為止。這樣一來,Student實(shí)例就能夠調(diào)用Person實(shí)例的方法,從而調(diào)用Person原型的方法。

    1. 組合繼承
      把構(gòu)造函數(shù)繼承和組合繼承結(jié)合起來就是組合繼承了。整體代碼如下:
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.say = function () {
    console.log('你好');
  };
}
Person.prototype.sing = function () {
  console.log('唱歌');
};

function Student(name, age, score) {
  Person.call(this, name, age);
  this.score = score;
}
Student.prototype = new Person();
Student.prototype.constructor = Student; // 修改constructor指向

組合繼承調(diào)用了兩次父構(gòu)造函數(shù),且每創(chuàng)建一個(gè)子構(gòu)造函數(shù),都會生成一個(gè)父構(gòu)造函數(shù)實(shí)例。還是不夠完美。

    1. 寄生組合繼承
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.say = function () {
    console.log('你好');
  };
}
Person.prototype.sing = function () {
  console.log('唱歌');
};

function Student(name, age, score) {
  Person.call(this, name, age);
  this.score = score;
}
Student.prototype = Object.create(Person.prototype); // Object.create(proto)創(chuàng)建一個(gè)對象,這個(gè)對象的__proto__屬性為proto
Student.prototype.constructor = Student; // 修改constructor指向

Student實(shí)例能夠調(diào)用Student原型上的方法,而Student原型又可以通過__proto__獲取Person原型上的方法。這樣就實(shí)現(xiàn)了Student繼承Person原型上的方法。
也可以自己寫一個(gè)類似 Object.create(proto) 的方法。

function objectCreate(proto) {
  function Temp() {}
  Temp.prototype = proto;
  return new Temp();
}
Student.prototype = objectCreate(Person.prototype);
Student.prototype.constructor = Student; // 修改constructor指向

6. 類的本質(zhì)

當(dāng)然,要實(shí)現(xiàn)繼承最好寫最好用的還是ES6里面的類。探究一下會發(fā)現(xiàn):

  • 類的本質(zhì)還是函數(shù)。
  • 類也有prototype屬性
  • 類創(chuàng)建的實(shí)例,也有__proto__屬性,且指向類的 prototype 屬性
  • 類創(chuàng)建的實(shí)例的constructor指向類本身
  • 類的prototype的constructor也是指向類本身
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  say() {
    console.log('說話');
  }
}

const jack = new Person('Jack', 18);

console.log(typeof Person); // function
console.log(Person.prototype); // {constructor: ?, say: ?}
console.log(jack.__proto__ === Person.prototype); // true
console.log(jack.constructor); // class Person{ ... }
console.log(Person.prototype.constructor); // class Person{ ... }

說白了,ES6 的類就是ES5繼承的語法糖。


參考資料:
傳智教育 - JavaScript進(jìn)階面向?qū)ο驟S6

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容