1. 程式人生 > 實用技巧 >深入淺出JS原型鏈

深入淺出JS原型鏈

定義

我們先來看看它的定義

當js在一個物件上查詢屬性的時候,首先查詢物件本身的屬性(即宣告時候定義的屬性),如果在這些屬性中沒有找到目標屬性,那麼js會找它的__proto__物件,如果這個物件沒有,那麼繼續找__proto__物件的__proto__物件,直到__proto__為null或者找到目標屬性為止...... 這個__proto__的關係鏈,就是我們說的原型鏈

特性

其實原型鏈真的很簡單,你只要記住下面這三個點就完了。

  • 每一個物件都有__proto__屬性
  • 每一個函式都有prototype屬性,這個屬性值也是一個物件
  • 每一個物件的__protp__都會指向它的建構函式的prototype

實踐

俗話說實踐出真知,我們一步步來驗證這幾個特性.

驗證第一點

大家可以分段複製我的程式碼貼上到瀏覽器控制檯中

let obj = {name:"leelei"};
console.log(obj.__proto__);

let arr = [1,2,3]
console.log(arr.__proto__);

let str = "http://www.leelei.info";
console.log(str.__proto__);

let num = 100;
console.log(num.__proto__);

當你全部列印完以後,你會發現一個問題,誒,我的str和num不是基礎型別嗎?不是物件也有proto屬性?

這裡我們要提一下js的隱式轉化規則了,當你訪問基礎型別的一些屬性或者呼叫一些方法的時候,你的基礎型別會被js給包裝起來,就像下面這樣

str.__proto__ => new String(str).__proto__;
num.__proto__ => new Number(num).__proto__;

那麼這個new String和new Number哪裡來的噶?

它們都是js原生的建構函式,總共有以下幾種

  • Object
  • Number
  • String
  • Boolean
  • RegExp
  • Error
  • Function
  • Array
  • Date
我們現在已經驗證了第一點了,每個物件(基礎型別是通過原生建構函式轉成了物件)都有__proto__屬性

驗證第二點

請一行一行復制以下程式碼到控制檯中

console.log(Object.prototype);
console.log(Number.prototype);
console.log(String.prototype);
console.log(Boolean.prototype);
console.log(RegExp.prototype);
console.log(Error.prototype);
console.log(Function.prototype);
console.log(Array.prototype);
console.log(Date.prototype);

如果我們不是建構函式呢

function fn(){}
console.log(fn.prototype);

可以看到,無論是建構函式,還是我們宣告的函式都有prototype的屬性。
區別只是原生的建構函式他們的prototype物件上已經自帶了一些方法和屬性。

那麼綜上我們可以驗證第二個特性,每一個函式都有prototype屬性

驗證第三點

我們只要判斷下第一步的__proto__是否指向第二步對應建構函式的prototype即可。

let obj = {name:"leelei"};
obj.__proto__ === Object.prototype; //true

let arr = [1,2,3]
arr.__proto__ === Array.prototype; //true

let str = "http://www.leelei.info";
str.__proto__ === String.prototype; //true

let num = 100;
num.__proto__ === Number.prototype; //true
結果已經顯而易見了,第三個論點也可得到論證

廣州設計公司https://www.houdianzi.com 我的007辦公資源網站https://www.wode007.com

從定義出發

我們根據定義來感受一下這個鏈條

let obj = {name:"leelei"};

console.log(obj.__proto.valueOf); //有這個方法嗷

obj.valueOf(); //{name:"leelei"};

我們可以看到,在我們宣告obj的時候並沒有這個屬性方法,但是我們卻可以呼叫,我們列印了obj的__proto__物件,發現這裡存在這個方法,所以可以呼叫。

這裡就是定義所講的當物件本身沒有這個屬性的時候,我們會去它的__proto__物件上找

這裡才只有一層噶?如果隔多幾層會不會調用不了了阿?
我們可以編寫一個建構函式,然後給它的原型物件prototype新增自定義方法來試驗。

function Foo(age){this.age = age};
Foo.prototype.say = function(){console.log('hello')};

let foo = new Foo(18);

根據特性2,物件的__proto__物件指向建構函式的prototype,所以這樣是沒什麼問題的

foo.say()
=> foo.__proto__.say()  //hello
相當於=> Foo.prototype.say()  //hello

那我們還能不能使用Object上面的方法呢?當然可以

foo.valueOf()
=> foo.__proto__.valueOf 
相當於=> Foo.prototype.valueOf 怎麼沒有嗷?

//第一層找不到喔,那就往上一層
//不要忘了prototype也是物件!
//所以它也有__proto__屬性

==> Foo.prototype.__proto__.valueOf 
相當於==> Object.prototype.valueOf 找到啦

你可能會有點好奇為什麼這兩個會等於?
Foo.prototype.__proto__ === Object.prototype

因為Foo.prototype是物件,Foo是函式

//我們可以很顯然直到Foo的建構函式是Function
Foo.__proto__ === Function.prototype;

//但是Foo.prototype的建構函式是誰?

Foo.prototype.__proto__ === 它的建構函式.prototype;

我們這裡需要了解一個constructor的屬性,
如果你在前面的步驟列印過自己定義的函式的prototype值,你會發現裡面自帶了一個constructor的屬性,當你列印constructor的時候會返回這個函式本身,如下所示

let fn = function(){console.log('hello')};
console.log(fn.prototype) //{constructor: ƒ}
fn.prototype.constructor === fn //true

那麼這個東西有什麼用呢?
就是為了告訴我們建構函式是誰。
在上面我們說到

Foo.prototype.__proto__=== 它的建構函式.prototype;

那麼顯然這樣也是相等的

Foo.prototype.__proto__.constructor=== 它的建構函式.prototype.constructor

又因為

它的建構函式.prototype.constructor === 它的建構函式

可以推出

Foo.prototype.__proto__.constructor === 它的建構函式
console.log(Foo.prototype.__proto__.constructor)
//ƒ Object() { [native code] }

所以我們可以求得Foo.prototype的建構函式是Object

一點需要注意的

typeof (Function.prototype); //function

這個居然不是物件?!驚了
根據特性2,每個函式都有一個prototype屬性。
既然Function.prototype不是物件而是方法,那麼它也有prototype屬性才對

Function.prototype.prototype//undefined

驚了!它居然沒有prototype屬性!

莫慌,除這個之外,都是適用的。記住這個特例即可。