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

對象基礎

對象介紹

  • 什麼是對象面試

    • 多個數據(屬性)的集合;
    • 用來保存多個數據(屬性)的容器;
  • 屬性組成數組

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

    • 通常:屬性值不是function,描述對象的狀態;
    • 方法:屬性值爲function的屬性,描述對象的行爲;
  • 特別的對象函數

    • 數組:屬性名是0,1,2,3之類的索引;
    • 函數:可執行的;
  • 對象是一種複合數據類型,能夠保存不一樣類型的屬性;ui

  • 建立對象this

    var obj = new object();
    複製代碼
  • 向對象中添加屬性spa

    • .屬性名;
    • ['屬性名']:屬性名有特殊字符/屬性名是一個變量;
    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)
複製代碼

對象高級

原型與原型鏈

什麼是原型

一、prototype本質上仍是一個JavaScript對象; 二、每一個函數都有一個默認的prototype屬性; 三、經過prototype咱們能夠擴展Javascript的內建對象prototype

原型的擴展

  • 全部函數都有一個特別的屬性: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運算符
let newObj = function(func){
  //建立對象,錯誤示範:Object.create()方法建立一個新對象,使用現有的對象的prototype指向括號中的對象func.prototype。
  // let obj = Object.create(func.prototype)
  // 因此應該以下創造對象,是爲了使新建立的對象的__proto__指向構造函數的原型func.prototype
  let obj = new Object()
  obj.__proto__=func.prototype
  // 將構造函數的做用域給新的對象,而且執行構造函數
  // 若是構造函數有返回值,那就返回返回值,若是沒有,會返回undefined
  let k = func.call(obj)
  if(typeof k === 'object'){
    // 若是返回的類型是一個對象,那就返回該對象
    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(name,friends){
	this.name = name;
	this.friends = friends;
}
Parent.prototype.test = function(){
	console.log('原型方法', this.friends)
};
// 定義子構造函數
function Child(name,friends,age){
    this.age = '12'
}
// 將子構造函數的原型指定父函數的實例
Child.prototype = new Parent('parentName',['a','b','c']);
// 可是
console.log(Child.prototype.constructor); 
//輸出:function Parent(){this.name = 'me';this.sex = ['male','female']}
// 因此,把Child的原型的構造函數修復爲child
Child.prototype.constructor = Child
var childObj = new Child('childName',[3,4,'ddd'],24);//有test()

// 問題一:子實例沒法向父類傳值
console.log(childObj.name,childObj.friends) // parentName和["a", "b", "c"]
// 問題二:若是其中一個子類修改了父類中的引用數據類型的屬性,那麼就會影響其餘的子類
var childObj2 = new Child('childName',[3,4],24);
childObj2.friends.push('additem')
console.log(childObj1.friends,childObj2.friends)//  ["a", "b", "c", "additem"], ["a", "b", "c", "additem"]
複製代碼
  • 重點:讓新實例(繼承對象childObj)的構造函數(Child)的原型等於父類的實例(被繼承的實例 new Parent()),或者說將父類的實例做爲子類的原型;
  • 特色:
    • 實例可繼承的屬性有:實例的構造函數的屬性,父類構造函數屬性,父類原型的屬性。
  • 缺點:
    • 一、新實例沒法向父類的構造函數中傳遞參數。
    • 二、繼承單一。
    • 三、全部新實例都會共享父類實例的屬性。(原型上的屬性是共享的,一個實例修改了原型屬性,另外一個實例的原型屬性也會被修改!)

  

借用構造函數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()
// 問題:
console.log(child.test);// undefined
複製代碼
  • 特色
    • 建立子類實例時,能夠向父類傳遞參數
    • 能夠實現多繼承(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); console.log(childObj,childObj.name,childObj.sex,childObj.test)
// 輸出:childObj.name:'zhangsan';childObj.sex:["male", "female"];childObj.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()。
  • 開發人員廣泛認爲寄生式組合式繼承是引用類型最理想的繼承範式。
相關文章
相關標籤/搜索