js深入學習繞不開的原型知識

keenjaan發表於2018-01-23

最近在看underscore原始碼,涉及到js原型相關的知識,於是重溫了一遍,在此做下記錄。

js原型是其語法的一個難點,也是一個重點,要深入學習js必須掌握的點。要想讀懂別人的框架和庫,瞭解這些基礎知識是必不可少的。 js原型主要為了提取公共屬性和方法,實現物件屬性和方法的繼承。說到原型,可能就有幾個相關的詞:prototype、__proto__、constructor、instanceof。下面通過這幾個關鍵詞來講解原型。

1、基礎概念

說到原型,先說一個概念,js裡函式(function)是一種特殊的物件,有個說法是js裡一切都是物件,這個說法不正確,要排除一些特殊型別,如:undefined, null (雖然null的typeof, toString返回型別是object,歷史遺留bug)。

js裡所有的物件都有__proto__屬性,可以稱為隱式原型,這個屬性在低版本ie瀏覽器中無法讀取到。一個物件的隱式原型指向構造該物件的建構函式的原型,使得該物件能夠繼承其建構函式原型上的屬性和方法。

函式物件除了具有__proto__這個屬性外,還有一個專有屬性prototype。這個屬性指向一個物件(命名a物件),從該函式例項化的物件(命名b物件)。b物件能共享a物件的所有屬性和方法。反過來a物件的constructor屬性又指向這個函式。

constructor 屬性是專門為 function 而設計的,它存在於每一個 function 物件的prototype 屬性中。這個 constructor 儲存了指向 function 的一個引用。

constructor和prototype被設計時是建構函式和原型間相互指向,可以看成互逆的,由於例項繼承了prototype物件上的屬性(包括constructor),故例項的constructor也是指向建構函式。雖然他們倆是互逆,但是兩者沒有必然聯絡,修改其中一個的指向,另一個並不會變。所以在js中通過原型來繼承時,一般替換原型時,會附帶替換掉constructor的指向。

// constructor與prototype
Object.prototype === Object.prototype
Object.prototype.constructor === Object

// 例項指向建構函式
var a = new Object({});
a.constructor === Object;

// 修改一個指向
function a() {}
a.prototype = {
  say: function () {
    console.log('hello world');
  }
}
a.prototype.constructor === Object //true
// 因為a.prototype重新賦值時,直接是賦值的一個物件,
// 這個物件沒有通過建構函式來生成,預設就會以new Object方式。故建構函式就是Object。
// 所以一般要手動重新指向建構函式
a.prototype.constructor = a;
複製程式碼

constructor設計初是被用來判斷物件型別的,由於其易變性,一般不使用它來做判斷,使用instanceof來替代它。

instanceof運算子,它用來判斷一個建構函式的prototype屬性所指向的物件是否存在另外一個要檢測物件的原型鏈上。一般用來判斷一個例項是否從一個建構函式例項化過來,用一個函式模擬instanceof函式:

function _instanceof(A, B) {
  var O = B.prototype;
  A = A.__proto__;
  while (true) {
    if (A === null) // 迴圈查詢原型鏈,一直到Object.prototype.__proto__ = null
      return false; // 退出迴圈
    if (O === A)
      return true;
    A = A.__proto__;
  }
}
複製程式碼

說了這麼多是不是覺得有點繞,拿出我的殺手鐗,祭出我收藏的一張圖,該圖很清晰的解釋了這些關係。看了這張圖後,瞬間理清了原型,廢話不多說,上圖:

js原型

2、兩個特殊物件

看了上面的圖後相信整個原型比較清晰了,下面說說整個原型中幾個特殊物件。

2.1、第一個特殊的物件就是Function

js裡的內建物件Object、Array、Function、Date、Math、Number、String、Boolean、RegExp等都是建構函式物件,可以通過new例項化物件出來。其__proto__屬性都指向Function.prototype。Function這個特殊物件,是上面其他函式物件的建構函式。

這裡有一條鏈,以Array為例:

123.png

js中上面寫的這些物件可以看成是從Function建構函式new出來的物件(例項),只不過與Object,Array建構函式 new出來的物件有點不同,例項化出來的物件是函式物件。所以有以下等式成立。

Array.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true

Array.constructor === Function // true
Object.constructor === Function // true
複製程式碼

由於例項化的Array、Object等屬於函式物件,它就有prototype屬性,故給每個函式物件配了個原型,如:Array.prototype、Object.prototype,從Array、Object等例項化的物件可以完成一些相同的功能,故給這些物件內建了很多方法,讓所有例項化的物件都具備這些方法,故在原型上掛載了很多方法。比如Array.prototype的方法:push、shift、unshift、concat等。 還有個特例如下:

Function.__proto__ === Function.prototype
複製程式碼

Function 這個函式既可以看成建構函式,也可以看成例項後的函式物件。

2.2、第二個個特殊的物件就是Object.prototype

不管是建構函式、原型、還是例項化物件,其都屬於物件,物件的原型最初來源都是Object.prototype這個原型物件,故:

Function.prototype.__proto__ === Object.prototype
Array.prototype.__proto__ === Object.prototype

Object.prototype.__proto__ === null
複製程式碼

而Object.prototype這個物件的__proto__屬性就為null了。 最後上一張我自己畫的關於原型的圖:

window畫圖畫的,忽略它的醜.png


3、原型相關方法

既然說到了原型鏈,來說一下幾個相關屬性,hasOwnProperty、isPrototypeOf、in。這幾個屬性在原型概念中經常用到。

js的原型主要實現了屬性和方法的繼承,既然有繼承,屬性和方法就有自己的和繼承來的之分。那麼怎麼去區分呢?

3.1、hasOwnProperty()方法用來判斷某個物件是否含有指定的自身屬性。不包括原型鏈上的屬性

var a = {name: 'li'};
a.hasOwnProperty('hasOwnProperty'); // false
a.hasOwnProperty('name'); // true
複製程式碼

hasOwnProperty屬性繼承自Object.prototype,故返回false, name則是建立時,自帶的,故返回false

3.2、isPrototypeOf方法測試一個物件是否存在另一個物件的原型鏈上。

var o = {}
var a = function () {}
var b = new a();
a.prototype = o;
var c = new a();
o.isPrototypeOf(b); // false
o.isPrototypeOf(c); // true
複製程式碼

3.3、in方法也是檢測一個物件中是否有每個屬性。

與hasOwnProperty不同的是,它會遍歷該物件上所有可列舉屬性,包括原型鏈上的屬性,有就返回true,沒有就返回false;

function a() {
    this.name = 'li'
}
a.prototype = {age: 20};
var b = new a();

for (var key in b) {
    if (b.hasOwnProperty(key)) {
        console.log('自身屬性'+ key);
    } else {
        console.log('繼承屬性'+ key);
    }
}
上面的方法經常區分一個物件中的屬性是自身屬性還是繼承屬性。
複製程式碼

for...in 迴圈只遍歷可列舉屬性,使用內建建構函式,像 Array、Object、Number、Boolean、String等建構函式的原型上的屬性都不可列舉。 如:Object.prototype.toString方法。 當然如果toString方法被重寫,還是可以遍歷的,如:

function animal() {
  this.name = 'lilei'
}
animal.prototype.toString = function () {
  console.log('animal say');
}
var cat = new animal();
for (var key in cat) {
  console.log(key); // name, toString
}
複製程式碼

但是在 IE < 9 瀏覽器中(萬惡的 IE),Object、Array等建構函式的原型上的屬性即使被重寫了,還是不能被列舉到。

3.3.1物件的可列舉

(1)、說到可列舉,你可能想到了一個函式,沒錯就是propertyIsEnumerable函式,他是Object.prototype上的一個方法,他能檢測一個屬性在其物件上是否可以列舉。 該方法只能檢測物件的自有屬性,對於其原型鏈上的屬性始終返回false,這一點要與for ... in 中的可列舉區分開。

function a() {
 this.name = 'liming';
}

a.prototype.say = function () {
 console.log(1);
}

var b = new a();

b.propertyIsEnumerable('name') // true
b.propertyIsEnumerable('say') // false
複製程式碼

(2)、物件的屬性分為可列舉和不可列舉之分,說了這麼多,其實它們是由屬性的enumerable值決定的。如通過Object.defineProperty函式建立的屬性,可以新增該欄位來決定該屬性是否可列舉。

var a = {name: 'xiao ming'}
Object.defineProperty(a, "gender", {
    value: "male",
    enumerable: false
});
a.propertyIsEnumerable('name') // true
a.propertyIsEnumerable('gender') // false
複製程式碼

(3)、到此應該已經結束了,但是我還是想提到一個函式,Object.keys。該函式返回一個物件的key的陣列。看個例子

function q() {
   this.name = 'lilei'
}
q.prototype.say = function () {
   console.log('say');
}
var a = new q();
Object.defineProperty(a, "gender", {
    value: "male",
    enumerable: false
});
Object.keys(a) // ['name']
複製程式碼

說明該方法返回該物件自有的可列舉屬性。

(4)、我還想說一下JSON.stringify方法,我們都只到JSON.stringify方法可以序列化物件。你有通過它克隆物件沒?

var a = {name: 'liming'}

var b = JSON.parse(JSON.stringify(a)) // {name: 'liming'}
複製程式碼

沒錯對於簡單的物件,我們可以這樣克隆,但是他能儲存物件裡的所有屬性嗎? 我們來看一下:

function f() {
  this.name = 'lilei';
  this.like= undefined;
  this.say = function () {}
}
f.prototype.age = 20;
var a = new f();
Object.defineProperty(a, "gender", {
    value: "male",
    enumerable: false
});
var b = JSON.parse(JSON.stringify(a)) // {name: 'lilei'}
複製程式碼

顯然該方法並不能儲存物件裡所有屬性,事實上stringify只能儲存該物件自己的可列舉屬性,不能儲存其原型上的屬性,並且自己的屬性也必須滿足以下要求: 1、stringify只能儲存基礎型別的:數字、字串、布林值、null四種,不支援undefined。 2、stringify方法不支援函式; 3、除了RegExp、Error物件,JSON語法支援其他所有物件; 關於其詳細內容,請看這篇文章傳送門

4、結語:

你可能感覺文章後面說了一堆方法好像跟原型沒多大關聯,確實關聯性不是很大,但是它們方法內部都涉及到了物件的屬性遍歷,物件屬性遍歷自然就聯絡到原型鏈上的屬性是否可遍歷,屬性的可列舉性等一系列概念,所以就把它們都提了一下。

本人能力有限,以上內容為個人理解,如有錯誤,歡迎指正。

相關文章