深入淺出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屬性!
莫慌,除這個之外,都是適用的。記住這個特例即可。