原型與原型鏈

逐夢song發表於2021-05-20

JavaScript 中,萬物皆物件!但物件也是有區別的。分為普通物件和函式物件,Object ,Function 是JS自帶的函式物件。

怎麼區分?其實很簡單,凡是通過 new Function() 建立的物件都是函式物件,其他的都是普通物件。Function Object 也都是通過 New Function()建立的。

建構函式

建構函式可用來建立特定型別的物件。像Object和Array這樣的原生建構函式,在執行時會自動出現在執行環境中。

function Person() {

}
var person = new Person();
person.name = 'Kevin';
console.log(person.name) // Kevin
複製程式碼

按照國際慣例,建構函式始終都應該以一個大寫字母開頭,而非建構函式則應該以一個小寫字母開頭。

要建立Person的新例項,必須使用new操作符。一般是經歷以下四個步驟:

  • 1、建立一個新物件;
  • 2、將建構函式的作用域賦給新物件(此this的指向了這個新物件)
  • 3、為這新物件新增建構函式的屬性
  • 4、返回新物件

上面例子中,person 就用有一個constructor (建構函式)屬性,該屬性是指向Person。

原型物件

無論什麼時候,只要建立一個新函式,就會根據一組特定的規則為該函式建立一個prototype屬性,這個屬性指向函式的原型物件。普通物件沒有 prototype,但有 __proto__ 屬性。

如上面例子,Person.prototype 指向了原型象,而 Person.prototype.constructor 又指回了Person。

而 person 只是一個物件例項。

而person可以訪問儲存在原型中的值,但卻不能重寫原型中的值。如果我們在 person 中新增一個屬性,而該屬性與例項原型中的一個屬性同名,那個person中的屬性會暫時遮蔽原型中的屬性,刪除後,還是讀回原型中的屬性

function Person() {

}
Person.prototype.name = 'Perty';
var person = new Person();
person.name = 'Kevin';
console.log(person.name) // Kevin
delete person.name
console.log(person.name) // Perty
複製程式碼

解析器的操作是:

  • “例項person有name屬性嗎?”
  • “有” —— 於是就讀取例項中的name屬性,輸出 Kevin

刪除後,再執行時:

  • “例項person有name屬性嗎?”
  • “沒有”
  • “person的原型有name屬性嗎?”
  • “有” —— 於是就讀取原型中的name的屬性,輸出 Perty

原型鏈

在建立物件(不論是普通物件還是函式物件)的時候,都有一個叫做 __proto__ 的內建屬性,用於指向建立它的函式物件的原型物件 prototype。以上面的例子為例:

console.log(person.__proto__ === Person.prototype)  // true
複製程式碼

同樣的,Person.prototype 也同樣有 proto 屬性,它指向建立它的函式物件(Object)的prototype

console.log(Person.prototype === Object.prototype) // true
複製程式碼

繼續,Object.prototype物件也有__proto__屬性,但它比較特殊,為null

console.log(Object.prototype.__proto__) // null
複製程式碼

我們把這個有 __proto__ 串起來的直到 Object.prototype.__proto__ 為null的鏈叫做原型鏈。

person.__proto__ ==> Person.prototype.__proto__ ==> Object.prototype.__proto__ ==> null

最佳例項zepto

讀過zepto的原始碼都瞭解到$其實是一個函式,同時在 $ 身上又掛了很多屬性和方法。

以下程式碼:

<p id="p1">段落1</p>
<p id="p2">段落2</p>
<p id="p3">段落3</p>
<script type="text/javascript" src="js/zepto-1.1.6.js"></script>
複製程式碼

$('p')返回的是類陣列的一樣東西。

我們先一步步來分析一下它究竟是不是陣列:

var arr = [1,2,3];
var $p = $('p');

// 對比1
arr.__proto__.constructor === Array;  // true
$p.__proto__.constructor === Array;  // false

// 對比2
arr instanceof Array;  // true
$p instanceof Array;  // false
複製程式碼

從原型指向中,可以看出$p真的只是類似陣列一樣的陣列。而陣列是沒有addClass,removeClass等等的屬性。

我們可以看console中,執行console.log($p.__proto__),就可以看$p可呼叫的方法(這也是平時在使用zepto時,忘記api方法名時,可以即時查出來)

原型與原型鏈

同理,我們檢視陣列下面的方法console.log(arr.__proto__)

原型與原型鏈

可以看到在arr中是沒有addClass,removeClass等等的屬性的。

那我們也為arr建立一個addClass方法試試:

var arr = [1,2,3]
arr.__proto__ = {
    addClass: function () {
      console.log(123);
    }
};
arr.addClass();   // 123
複製程式碼

此時我們再去拿arr來做第一次的那幾個驗證,得到的結果就和之前的$p一樣了,即arr此時也稱了一個不是陣列的陣列

原型與原型鏈

這樣就可以成功建立了addClass方法,而這個解析器的操作是:

  • 建立一個新的陣列例項[1,2,3]
  • 新例項新增一個屬性方法addClass

執行arr.addClass()時:

  • “例項arr有addClass屬性嗎?”
  • “有” —— 於是就讀取例項中的addClass屬性,輸出 123

這裡只是淺析zepto的基本設計,也是瞭解zepto設計原理的開始,想要更深入理解zepto,請移步zepto

相關文章