一文徹底理解js原生語法prototype,__proto__和constructor

1 前言

寫瞭幾篇vue的源碼註釋(並不算解析…), 感覺到瞭對原型的理解是不夠的, 在js中, 原型是非常重要的, 隻要你想在js這座山上往上爬, 它就會嘲笑你, 你把我搞會瞭麼? 如果沒有, 它就給你加個十倍重力. 如果搞懂瞭, 那肯定是能月薪過萬, 贏取白富美, 走向人生巔峰的啦~~~

這篇文章講的都是我自己的理解, 應該是原創的(我有99%把握, 除非是我之前看過文章記到腦子裡瞭, 沒法給到引用瞭, 聯系我可以加上), 但是如果有人借鑒我的這篇文章, 希望給到一個這篇文章的鏈接. 其實我隻是想我的文章能有更多的閱讀量, 我想月薪過萬, 贏取白富美, 走向人生巔峰~~~

2 前置知識點

2.1 數據類型

js共有7種數據類型

從可不可以讀取屬性, 可以分為兩類

  • 可以讀取屬性:
    • 自身可以有屬性: object
    • 自身不可以有屬性: string,number,boolean,symbol
  • 不可以讀取屬性: null,undefined

null,undefined類型, 讀取和設置屬性都是非法的, 直接報錯.

隻有object能有自有屬性, 可以讀取屬性和設置屬性

string,number,boolean,symbol類型可以讀取屬性, 其實是先構造成包裝對象, 再讀取屬性, 設置屬性也是一樣, 可以理解設置到瞭會立即銷毀的包裝對象上, 就是可以設置, 但是沒有任何實質效果.

2.2 判斷是否是自身屬性(hasOwnProperty)

hasOwnProperty方法是繼承來的, 用來判斷該對象自身上是否有這個屬性, 有就行, 不管是什麼值

const obj = { a: 1 }
const o = Object.create(obj)
o.b = 1
o.c = void 0
console.log('a', o.a, o.hasOwnProperty('a')) // 可以讀取到值, 繼承而來, 但不是自身屬性
console.log('b', o.b, o.hasOwnProperty('b')) // 可以讀取到值, 自身屬性
console.log('c', o.c, o.hasOwnProperty('c')) // 讀取到undefined, 自身屬性
console.log('d', o.d, o.hasOwnProperty('d')) // 讀取到undefined, 不是自身屬性, 也沒有繼承到這個屬性

3 一點小思考

程序就是數據結構與算法, 好的程序最好是使用最小的內存存儲數據, 使用最快的時間完成運行得到結果.

復用數據可以達到減少內存使用的目的, 例如a和b需要完成一樣的功能, 就可以復用同一個方法(屬性).

那麼就需要解決一個問題, 這個復用的方法存在哪裡, a和b怎樣能找到它.

在js中的解決方案是, a和b都由函數(這裡先叫Function吧)構造而來, 復用的方法存放在函數身上(prototype屬性裡).

(因為隻有構造函數身上需要存放復用的方法, 所以prototype隻有可構造的函數上才有, 箭頭函數不是用來構造的, 它就沒有, 其它對象, 如果連函數都不是, 就更不會有這個屬性瞭)

那麼需要給a,b 和Function建立起聯系, 因為a,b需要到Function身上找它們可以用的復用方法

在js中的實現是通過constructor屬性,即a.constructor, b.constructor可以找到Function

所以通過a.constructor.prototype可以找到它可以復用的方法的存放地址, 為瞭快速找到js提供瞭一種快捷方法a.__proto__一步到位找到, 即a.constructor.prototype和a.__proto__找到的是同一個對象, 當然它倆是全等的啦.

// 它倆都不是自有屬性, 我也不知道怎麼從這倆屬性上找到原型對象的瞭, 肯定是魔法.....
const obj = {}
console.log(obj.hasOwnProperty('__proto__')) // false
console.log(obj.hasOwnProperty('constructor')) // false

(所以, 如果手動修改瞭constructor,prototype,__proto__的指向, 那麼你得清楚你在幹什麼)

(我不知道js的設計者是不是這樣想的, 哈哈, 我就這樣認為, 這樣好理解多瞭)

(這個過程稱之為繼承, 而且是一個鏈式過程, 即可以a.constructor.prototype.constructor.prototype.constructor.prototype這樣查找, 直到找到最頂端, 這個過程可以由a.__proto__.__proto__.__proto__加速, 這個就叫做原型鏈, js的繼承隻有這一種實現方式.)

(上面隻是引導思考過程, 其實查找原型對象並不會通過a.constructor.prototype去找, 而是直接通過__proto__查找)

3.1 修改 constructor

const Dog = function () {}
const dog = new Dog()

dog.constructor = 0

console.log(dog.hasOwnProperty('constructor')) // true
console.log(dog.constructor) // 0

console.log(dog.__proto__.constructor) // [Function: Dog]

總結, 修改瞭這個屬性, 增加瞭找到構造它的構造函數的難度, 不能直接獲取瞭, 需要到原型對象上去讀取.

如果它自身的這個屬性和原型上的這個屬性都被修改瞭, 那麼也隻是找不到它的構造函數瞭而已, 不會有別的影響.

3.1.1 instanceof

instanceof關心的是原型鏈, 跟constructor沒有關系

印證上面的點, 修改constructor屬性, 除瞭讓實例找不到構造它的構造函數, 沒有別的影響瞭. 如果需要通過實例找到它的構造函數, 就需要維護好它倆的關系.

// 語法是
// a instanceof b
// 這個操作符是判斷 a 的原型鏈上是否有  b.prototype, 因為要判斷 b.prototype 所以 b 必需是一個 可構造的函數, 否則會報錯
const fn = function () {}
const o = Object.create(fn.prototype)
// 此時 o 的原型鏈上有 fn.prototype, 因為 o.__proto__ === fn.prototype
console.log(o instanceof fn) // true
const emptyObj = {}
fn.prototype = emptyObj
// 此時 o 的原型鏈上已經沒有 fn.prototype 瞭, 因為此時 o.__proto__ 已經不再和 fn.prototype 相等瞭
console.log(o instanceof fn) // false
o.__proto__ = emptyObj
// 修正瞭 o.__proto__ 就好瞭
console.log(o instanceof fn) // true

3.1.2 isPrototypeOf

現在有個新的api, 實現的功能和instanceof一致, 但是更加語義化一些, 直接判斷對象是否在另一個對象的原型鏈上

const fn = function () {}
const o = Object.create(fn.prototype)
console.log(fn.prototype.isPrototypeOf(o)) // true

3.2 修改__proto__|prototype

先說一個總結, 在構造實例的時候, 會將這個實例的__proto__指向此時的構造函數的prototype, 然後實例實際是繼承的是__proto__.(為什麼強調此時, 因為構造函數的prototype可能會被修改指向, 修改之後隻會影響修改之後構造的實例, 修改之前構造的實例還會使用修改之前的prototype)

所以, 就可以理解到修改__proto__和prototype會有哪些影響瞭

1.修改__proto__的指向

隻會影響它自己的繼承

const Dog = function () {}
const dog = new Dog()
const d = new Dog()
Dog.prototype.name = 'Dog'
dog.__proto__ = {
  name: '__proto__',
}
console.log(d.name) // Dog
console.log(dog.name) // __proto__

2.修改__proto__的屬性

會影響這一波段構造的實例

const Dog = function () {}
const dog = new Dog()
const d = new Dog()
Dog.prototype.name = 'Dog'
console.log(d.name) // Dog
console.log(dog.name) // Dog
Dog.prototype = {
  name: 'after',
}
const dog1 = new Dog()
const d1 = new Dog()
console.log(d1.name) // after
console.log(dog1.name) // after
dog1.__proto__.name = '__proto__'
// 可以看到隻影響瞭當前這一段構造的實例, 之前和之後的都不會被影響到, 因為這一段內的是同一個 Dog.prototype , 它們的 __proto__ 都是指向它的
console.log(d1.name) // __proto__
console.log(dog1.name) // __proto__
Dog.prototype = {
  name: 'new',
}
const dog2 = new Dog()
const d2 = new Dog()
console.log(d2.name) // new
console.log(dog2.name) // new

3.修改prototype的指向

會影響這一波段構造的實例

4.修改prototype的屬性

會影響這一波段構造的實例, 同修改 __proto__的屬性

4 修改和獲取原型對象的方式

4.1 修改

上面已經講瞭修改prototype和__proto__

4.1.1 Object.create

const obj = {
  name: 'objName',
}

const o = Object.create(obj)
// 它相當於 o.__proto__ = obj, 但是推薦使用`Object.create`

console.log(o.name) // objName
console.log(o.__proto__ === obj) // true

4.1.2 Object.setPrototypeOf

const obj = {
  name: 'objName',
}

const o = {}

Object.setPrototypeOf(o, obj)
// 它相當於 o.__proto__ = obj, 但是推薦使用`Object.setPrototypeOf`
const proto = Object.getPrototypeOf(o)
console.log(proto === obj && proto === o.__proto__) // true
const obj1 = {}
o.__proto__ = obj1
const proto1 = Object.getPrototypeOf(o)
console.log(proto1 === obj1 && proto1 === o.__proto__) // true

總結, 在什麼時候使用Object.create, 在什麼時候使用Object.setPrototypeOf呢, 首先它倆都是標準api, 都是建議使用的, 在創建對象的時候就要指定原型時使用Object.create, 需要動態修改原型對象時, 使用Object.setPrototypeOf

4.2 獲取

之前已經講瞭, 通過 constructor.prototype和__proto__獲取瞭

4.2.1 Object.getPrototypeOf

const obj = {
  name: 'objName',
}

const o = {}

Object.setPrototypeOf(o, obj)

const proto = Object.getPrototypeOf(o)
console.log(proto === obj && proto === o.__proto__) // true

5 js 內置原生構造函數

這些原生的構造函數的prototype屬性是不可寫, 不可枚舉, 不可配置的

console.log(Object.getOwnPropertyDescriptor(Object, 'prototype'))
// {
//   value: [Object: null prototype] {},
//   writable: false,
//   enumerable: false,
//   configurable: false
// }

5.1 js 繼承的最頂端是什麼

null, 必須是這傢夥, 不然隻能無限套娃瞭

然後其它所有對象都是從Object構造而來, 所以所有的對象都可以繼承到Object.prototype.

const obj = {}
const o = new Object()

5.2 js 繼承的二等公民(Function)

在上面的小思考中, 說到, js對象都是函數構造而來, 所以包括Object也是由Function構造來的, 甚至它自己都是由自己構造而來

console.log(Object.constructor === Function) // true
// 這就離譜瞭, 第一個Function是從哪裡來的呢????
console.log(Function.constructor === Function) // true

我再來一點小理解, 可能是在js內部做瞭小處理, 第一個Function是憑空變出來的…. 然後這個Function構造出瞭Object, 然後這個Object構造出瞭第一個原型對象Object.prototype, 然後再去修改一些引用關系.

其實最復雜的是Object和Function的關系

console.log(Object.__proto__ === Function.prototype) // true
console.log(Function.constructor === Function) // true
console.log(Function.__proto__ === Function.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true

5.3 js 繼承的三等公民(內置的其他構造函數)

const arr = [
  String,
  Array,
  Boolean,
  Number,
  Date,
  RegExp,
  Error,
  Promise,
  Map,
  Set,
  Symbol,
  Proxy,
]
// 都是由Function構造而來

6 用戶定義的特定公民構造函數

這個才是重點, 根據上面的理解, 我會再開一篇文章寫一下我理解的js的繼承, 這裡就先留個坑

7. 總結

這篇文章跟網上大多講constructor,prototype,__proto__的文章都有所不同, 我的立足點是從給定的一個可以讀取屬性的值開始, 在js中, 除瞭null和undefined, 其它所有的值都可以成為立足點. 從這個立足點開始, 它的__proto__屬性記錄瞭它的原型對象, 這個原型對象是構造它時, 它的構造函數的prototype屬性的值.

const a = 1
console.log(a.__proto__.constructor) // [Function: Number]

讀取一個值的屬性的值時, 如果它自身有這個屬性, 那麼直接返回這個屬性的值, 否則就會到它的__proto__對象上去找, 一直遞歸下去, 直到找到頂部null, 找到就返回它的值, 沒找到就返回undefined

這篇文章有三個理解點,讓我茅塞頓開, 都是在我試驗瞭好久突然得到的結論

  1. 以一個值為立足點開始分析
  2. 在構造實例的時候, 會將這個實例__proto__指向此時的構造函數的prototype
  3. 查找原型對象時, 以__proto__為準

8 最後

到此這篇關於js原生語法prototype,__proto__和constructor的文章就介紹到這瞭,更多相關js原生語法prototype,__proto__和constructor內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: