【準備面試】- JS - 原型鏈


構造函數內部有一個名爲 prototype 的屬性,經過這個屬性就能訪問到原型.
bash

Person 就是構造函數,Person.prototype 就是原型.
app

有個構造函數,咱們就能夠在原型上建立能夠「繼承」的屬性,並經過 new 操做符建立實例.
函數

實例經過 __proto__ 訪問到原型,因此若是是實例,那麼就能夠經過這個屬性直接訪問到原型.
ui

既然構造函數經過 prototype 來訪問到原型,那麼原型也應該可以經過某種途徑訪問到構造函數,這就是 constructor.
this

原型一樣也能夠經過 __proto__ 訪問到原型的原型,,就會經過 __proto__ 做爲橋樑鏈接起來的一系列原型、原型的原型,直到盡頭null爲止。
spa

new

new 運算符建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。
prototype

當代碼 new Foo(...) 執行時,會發生如下事情
code

1. 新生成了一個對象 cdn

2. 連接到原型 對象

3. 綁定 this

4. 返回新對象

function create() { 
   // 建立一個空的對象
   let obj = new Object()
   // 得到構造函數
   let Con = [].shift.call(arguments)
   // 連接到原型
   obj.__proto__ = Con.prototype
   // 綁定 this,執行構造函數
   let result = Con.apply(obj, arguments)
   // 確保 new 出來的是個對象
   return typeof result === 'object' ? result : obj
}
複製代碼

返回值可能由三種狀況

  • 一、返回一個對象 只能訪問到返回對象中的屬性
  • 二、沒有 return,即返回 undefined 只能訪問到構造函數中的屬性
  • 三、返回undefined 之外的基本類型 只能訪問到構造函數中的屬性

因此須要判斷下返回的值是否是一個對象,若是是對象則返回這個對象,否則返回新建立的 obj對象。

對於 new 來講,還須要注意下運算符優先級。

function Foo() {
 return this;
}
Foo.getName = function () {
 console.log('1');
};
Foo.prototype.getName = function () {
 console.log('2');
};
new Foo.getName(); // -> 1
new Foo().getName(); // -> 2 
複製代碼

從上圖能夠看出,new Foo() 的優先級大於 new Foo ,因此對於上述代碼來講可 以這樣劃分執行順序

new (Foo.getName());

(new Foo()).getName();

對於第一個函數來講,先執行了 Foo.getName() ,因此結果爲 1;對於後者來講, 先執行 new Foo() 產生了一個實例,而後經過原型鏈找到了 Foo 上的 getName 函數, 因此結果爲 2。

instanceof

instanceof 能夠正確的判斷對象的類型,由於內部機制是經過判斷對象的原型鏈 中是否是能找到類型的 prototype。

咱們也能夠試着實現一下 instanceof

function instanceof(left, right) {
   // 得到類型的原型
   let prototype = right.prototype
   // 得到對象的原型
   left = left.__proto__
   // 判斷對象的類型是否等於類型的原型
   while (true) {
       if (left === null)
           return false
       if (prototype === left)
           return true
       left = left.__proto__ //不存在沿着原型鏈繼續找
   }
}複製代碼

繼承

1. 原型鏈繼承

function Father(){
    this.property = true;
}
Father.prototype.getFatherValue = function(){
    return this.property;
}
function Son(){
    this.sonProperty = false;
}
//繼承 Father
Son.prototype = new Father();//Son.prototype被重寫,致使Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
    return this.sonProperty;
}
var instance = new Son();
console.log(instance.getFatherValue());//true
複製代碼

此時instance.constructor指向的是Father,這是由於Son.prototype中的constructor被重寫的緣故.

原型鏈並不是十分完美, 它包含以下兩個問題.

問題一: 當原型鏈中包含引用類型值的原型時,該引用類型值會被全部實例共享;

問題二: 在建立子類型(例如建立Son的實例)時,不能向超類型(例如Father)的構造函數中傳遞參數.

2.借用構造函數

基本思想:即在子類型構造函數的內部調用超類型構造函數.

function Father(){
	this.colors = ["red","blue","green"];
}
function Son(){
	Father.call(this);//繼承了Father,且向父類型傳遞參數
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"

var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green" 可見引用類型值是獨立的 
複製代碼

很明顯,借用構造函數一舉解決了原型鏈的兩大問題:

其一, 保證了原型鏈中引用類型值的獨立,再也不被全部實例共享;

其二, 子類型建立時也可以向父類型傳遞參數.

隨之而來的是, 若是僅僅借用構造函數,那麼將沒法避免構造函數模式存在的問題--方法都在構造函數中定義, 所以函數複用也就不可用了.並且超類型(如Father)中定義的方法,對子類型而言也是不可見的. 考慮此,借用構造函數的技術也不多單獨使用.

3.組合繼承

是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式.

基本思路: 使用原型鏈實現對原型屬性和方法的繼承,經過借用構造函數來實現對實例屬性的繼承.

function Father(name){
	this.name = name;
	this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
	alert(this.name);
};
function Son(name,age){
	Father.call(this,name);//繼承實例屬性,第一次調用Father()
	this.age = age;
}
Son.prototype = new Father();//繼承父類方法,第二次調用Father()
Son.prototype.sayAge = function(){
	alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5

var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10複製代碼

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲 JavaScript 中最經常使用的繼承模式.

同時咱們還注意到組合繼承其實調用了兩次父類構造函數, 形成了沒必要要的消耗

相關文章
相關標籤/搜索