前端面試必殺技:原型、原型鏈以及繼承(一張圖搞定面試)

木頭房子發表於2019-04-15

物件基礎

物件介紹

  • 什麼是物件

    • 多個資料(屬性)的集合;
    • 用來儲存多個資料(屬性)的容器;
  • 屬性組成

    • 屬性名:字串(標識);
    • 屬性值:任意型別;
  • 屬性的分類:

    • 一般:屬性值不是function,描述物件的狀態;
    • 方法:屬性值為function的屬性,描述物件的行為;
  • 特別的物件

    • 陣列:屬性名是0,1,2,3之類的索引;
    • 函式:可執行的;
  • 物件是一種複合資料型別,可以儲存不同型別的屬性;

  • 建立物件

    var obj = new object();
    複製程式碼
  • 向物件中新增屬性

    • .屬性名;
    • ['屬性名']:屬性名有特殊字元/屬性名是一個變數;
    obj.屬性名 = 屬性值;
    obj[‘屬性名’] = 屬性值;
    複製程式碼
    • 使用[]去操作屬性時,[]中傳遞的是一個字串
    • 能傳字串的地方就能傳變數
    • 如果我們物件的屬性名過於奇怪,則必須使用[]來操作。

物件建立模式

1.物件字面量模式

  • 套路: 使用{}建立物件, 同時指定屬性/方法;
  • 適用場景: 起始時物件內部資料是確定的;
  • 問題: 如果建立多個物件, 有重複程式碼;
var p = {
 name: 'Tom',
  age: 23,
  setName: function (name) {
    this.name = name
  }
}
console.log(p.name, p.age)
p.setName('JACK')
console.log(p.name, p.age)
var p2 = {
  name: 'BOB',
  age: 24,
  setName: function (name) {
    this.name = name
  }
}
複製程式碼

2.Object建構函式的模式

  • 套路: 先建立空Object物件, 再動態新增屬性/方法
  • 適用場景: 起始時不確定物件內部資料;
  • 問題: 語句太多;
// 一個人: name:"Tom", age: 12
var p = new Object()
p = {}
p.name = 'Tom'
p.age = 12
p.setName = function (name) {
  this.name = name
}
p.setaAge = function (age) {
  this.age = age
}
console.log(p)
複製程式碼

3.工廠模式

  • 套路: 通過工廠函式動態建立物件並返回;
  • 適用場景: 需要建立多個物件;
  • 問題: 物件沒有一個具體的型別,都是Object型別;
// 工廠函式: 返回一個需要的資料的函式
  function createPerson(name, age) {
    var p = {
      name: name,
      age: age,
      setName: function (name) {
        this.name = name
      }
    }
    return p
  }
  var p1 = createPerson('Tom', 12)
  var p2 = createPerson('JAck', 13)
  console.log(p1)
  console.log(p2)
複製程式碼

4.自定義建構函式模式;

  • 套路: 自定義建構函式,通過new建立物件;
  • 適用場景: 需要建立多個型別確定的物件;
  • 問題: 每個物件都有相同的資料, 浪費記憶體;將屬性和方法新增到各個例項物件上去,但是每個例項都有相同的方法,重複了,我們可以將相同的方法放到他的建構函式的原型物件上去;
function Person(name, age) {
	  this.name = name
	  this.age = age
	  this.setName = function (name) {
	  this.name = name
    }
}
var p1 = new Person('Tom', 12)
var p2 = new Person('Tom2', 13)
console.log(p1, p1 instanceof Person)
複製程式碼

物件高階

原型與原型鏈

什麼是原型

1、prototype本質上還是一個JavaScript物件; 2、每個函式都有一個預設的prototype屬性; 3、通過prototype我們可以擴充套件Javascript的內建物件

原型的擴充套件

  • 所有函式都有一個特別的屬性:prototype顯式原型屬性(只有函式有prototype,物件是沒有的。);
  • 所有例項物件都有一個特別的屬性:__proto__隱式原型屬性;
  • 原型是用於儲存物件的共享屬性和方法的,原型的屬性和方法並不會影響函式本身的屬性和方法。
  • 顯式原型與隱式原型的關係
    • 函式的prototype:定義函式時被自動賦值,值預設為{},即原型物件;
    • 例項物件的_proto_: 在建立例項物件時被自動新增, 並賦值為建構函式的prototype值;
    • 原型物件即為當前例項物件的父物件;

原型鏈

  • 所有的例項物件都有__proto__屬性, 它指向的就是原型物件
  • 這樣通過__proto__屬性就形成了一個鏈的結構---->原型鏈;
  • 當查詢物件內部的屬性/方法時, js引擎自動沿著這個原型鏈查詢;
  • 當給物件屬性賦值時不會使用原型鏈, 而只是在當前物件中進行操作;

原型鏈圖

面試必畫圖

  • 圖片是個人畫的圖,面試官但凡問到原型鏈問題,就可以畫出此圖,並且邊畫邊敘述,會給你蹭蹭地加分哦;
  • 圖片上有必說的語句,並且標明瞭結合畫圖時說這些語句的時機

原型鏈回答必殺圖

new關鍵字做了什麼

  • 示例:
// 建構函式
function Base(){}
var baseObj = new Base()
複製程式碼
  • 建立了一個空物件;
  • 將這個空物件的隱式原型_proto_\指向建構函式的顯示原型prototype;如例,是將空物件的__proto__成員指向了Base函式物件prototype成員物件;
  • 將建構函式的this指向例項(即空物件),並呼叫Base函式;
var obj  = {}; 
obj.__proto__ = Base.prototype; 
Base.call(obj);  
複製程式碼
  • 根據new的工作原理手動實現一下new運算子
var new2 = function (func) {
  //建立物件,Object.create()方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__。
  var obj = Object.create(func.prototype); 
  //改變this指向,把結果付給k   
  var k = func.call(obj);
  obj.constructor = func;
  //判斷k的型別是不是物件
  if (typeof k === 'object') {
	 //是,返回k
     return k;
  } else {
	 //不是返回返回建構函式的執行結果
     return obj;
  }
}    
複製程式碼

物件的繼承

複製屬性式繼承

// 建立父物件
var parentObj = {
	name: 'parentName',
	age: 25,
	showName:function(){
        console.log(this.name);
    }
}
// 建立需要繼承的子物件
var childrenObj= {}
// 開始拷貝屬性(使用for...in...迴圈)
for(var i in parentObj){
	childrenObj[i] = parentObj[i]
}
console.log(childrenObj); //{ name: 'parentName', age: 25, showName: [Function: showName] }
console.log(parentObj); // { name: 'parentName', age: 25, showName: [Function: showName] }
複製程式碼
  • 重點:將父物件的函式和方法迴圈進行復制,複製到子物件裡;
  • 缺點:如果繼承過來的成員是引用型別的話,那麼這個引用型別的成員在父物件和子物件之間是共享的,也就是說修改了之後, 父子物件都會受到影響。

原型繼承://TODO

  • 原型式繼承就是借用建構函式的原型物件實現繼承,即 子建構函式.prototype = 父建構函式.prototype;
// 建立父建構函式
function Parent(){}
// 設定父建構函式的原型物件
Parent.prototype.age = 25;
Parent.prototype.friends = ['小名','小麗'];
Parent.prototype.showAge = function(){
    console.log(this.age);
};
// 建立子建構函式
function Child(){}
// 設定子構造器的原型物件實現繼承
Child.prototype = Parent.prototype
// 因為子建構函式的原型被覆蓋了, 所以現在子建構函式的原型的構造器屬性已經不再指向Child,而是Parent。此時例項化Child和例項化parent的區別是不大的,所以再次建立Child是沒有意義的,並且Child.prototype新增屬性,也是會影響到Parent.prototype;
console.log(Child.prototype.constructor == Parent);// true
console.log(Parent.prototype.constructor == Parent);// true

// 問題就在這裡!!!!
// 所以我們需要修正一下
Parent.prototype.constructor = Child;
// 上面這行程式碼之後, 就實現了繼承
var childObj = new Child();
console.log(childObj.age);// 25
console.log(childObj.friends);// ['小名','小麗']
childObj.showAge();// 25
複製程式碼
  • 問題:
    • 只能繼承父建構函式的原型物件上的成員, 不能繼承父建構函式的例項物件的成員;
    • 父建構函式的原型物件和子建構函式的原型物件上的成員有共享問題;

原型鏈繼承 : 得到方法

// 定義父建構函式
function Parent(){
	this.name = 'me';
	this.sex = ['male','female']
}
  
Parent.prototype.test = function(){};
// 定義子建構函式
function Child(){
    this.age = '12'
}
// 將子建構函式的原型指定父函式的例項
Child.prototype = new Parent();
// 但是
console.log(Child.prototype.constructor); 
//function Parent(){
//	this.name = 'me';
//	this.sex = ['male','female']
//}
// 所以,把Child的原型的建構函式修復為child
Child.prototype.constructor = Child
var childObj = new Child(); //有test()
複製程式碼
  • 重點:讓新例項(繼承物件childObj)的建構函式(Child)的原型等於父類的例項(被繼承的例項 new Parent()),或者說將父類的例項作為子類的原型;
  • 特點:
    • 例項可繼承的屬性有:例項的建構函式的屬性,父類建構函式屬性,父類原型的屬性。
  • 缺點:
    • 1、新例項無法向父類的建構函式中傳遞引數。
    • 2、繼承單一。
    • 3、所有新例項都會共享父類例項的屬性。(原型上的屬性是共享的,一個例項修改了原型屬性,另一個例項的原型屬性也會被修改!)

  

借用建構函式call(經典繼承) : 得到屬性

  • 使用父類的建構函式來增強子類例項,等於是複製父類的例項屬性給子類(沒用到原型);
  • Child無法繼承Parent原型上的物件,並沒有真正的實現繼承(部分繼承);
function Parent(xxx){this.xxx = xxx}
Parent.prototype.test = function(){};
function Child(xxx,yyy){
    Parent.call(this, xxx);
}
var child = new Child('a', 'b');  //child.xxx為'a', 但child沒有test()
複製程式碼
  • 特點
    • 建立子類例項時,可以向父類傳遞引數
    • 可以實現多繼承(call多個父類物件)

組合式繼承

  • 借用建構函式 + 原型式繼承
// 建立父建構函式
// 父類屬性
function Parent(name){
	this.name = name;
	this.sex = ['male','female']
}
// 父類原型方法
Parent.prototype.test = function(){
	console.log(this.name)
};
// 定義子建構函式
function Child(name,age){
	// 複製父級建構函式的屬性和方法
	// 使得每一個子物件都能複製一份父物件的屬性且不會相互影響
    Parent.call(this,name);//繼承例項屬性,第一次呼叫Parent()
    this.age = age
}
// 將子建構函式的原型物件指向父級例項
var parentObj = new Parent();//繼承父類方法,第二次呼叫Parent()
Child.prototype = parentObj; //得到test()
// 將子建構函式Child原型的建構函式修復為Child
Child.prototype.constructor = Child; 
var childObj = new Child('zhangsan',15); //childObj.xxx為'a', 也有test()
複製程式碼
  • 相當重要的一步:Child.prototype.constructor = Child;
    • 1.任何一個Prototype物件都有一個constructor指標,指向它的建構函式;
    • 2.每個例項中也會有一個constructor指標,這個指標預設呼叫Prototype物件的constructor屬性。
    • 結果:當替換了子類的原型之後,即 Child.prototype = new Parent()之後,Child.prototype.constructor 就指向了Parent(),Child的例項的constructor也指向了Parent(),這就出現問題了。
    • 因為這造成了繼承鏈的紊亂,因為Child的例項是由Child建構函式建立的,現在其constructor屬性卻指向了Parent,為了避免這一現象,就必須在替換prototype物件之後,為新的prototype物件加上constructor屬性,使其指向原來的建構函式。
  • 缺點:通過將子建構函式的原型指向父建構函式的例項,會兩次呼叫父類建構函式;

寄生組合式繼承

  • 原理:通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。
  • 思路:不必為了指定子類的原型而呼叫超型別的建構函式,我們所需要的無非就是超型別原型的一個副本而已。
  • 寄生組合式繼承就是為了降低呼叫父類建構函式的開銷而出現的 ;
  • 本質上,就是使用寄生式繼承來繼承父型別的原型,然後再將結果指定給子型別的原型。
  • 解決方法是在中間架一座橋樑,加一個空的建構函式;
// 建立父建構函式
function Parent(){
	this.name = 'me';
	this.sex = ['male','female']
}
Parent.prototype.test = function(){};
// 定義子建構函式
function Child(){
	// 複製父級建構函式的屬性和方法
	// 使得每一個子物件都能複製一份父物件的屬性且不會相互影響
    Parent.call(this);
    this.age = '12'
}

// 定義空函式
function F(){}
// 把空函式的原型指向Parent.prototype
// 寄生式組合繼承
F.prototype = Parent.prototype

// 將子建構函式的原型物件指向空函式F的例項物件fObj
var fObj = new F();
Child.prototype = fObj; 

// 將子建構函式Child原型的建構函式修復為Child
Child.prototype.constructor = Child; 
var childObj = new Child(); 
複製程式碼
  • 優點:高效率體現在只呼叫了一次Parent建構函式,並且因此避免了在Child.prototype上面建立不必要的,多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能正常使用instanceof 和 isPrototypeOf()。
  • 開發人員普遍認為寄生式組合式繼承是引用型別最理想的繼承正規化。

相關文章