原型鏈、閉包、事件循環等,可以說是js中比較復雜的知識了,復雜的不是因為它的概念,而是因為它們本身都涉及到很多的知識體系。所以很難串聯起來,有一個完整的思路、脈絡。我最近想把js中有意思的知識點都總結整理一下,雖然逃不開一些一模一樣的內容,但是自己造一下輪子,按照自己的思路,也別有一番味道。
這篇文章總體來說,是講原型鏈的,但是並不涉及到繼承。繼承的問題,后面會專門拿出來一篇文章來說。這篇文章中的很大一部分,也並不完全是“原型”,還涉及到很多前置的知識。文章有點長,希望你能耐心讀完,吸收之后肯定會有不小的收獲!那么,我們就先從一個簡單的問題開始這篇萬字(確實差不多有1w字,別怕,我在)長文吧!
一、請描述一下js的數據類型有哪些?
就這?這么簡單的么?哈哈哈...我們先從這個問題開始。
答:js的數據類型有字符串String、數值Number、布爾值Boolean、Null、Undefined、對象Object、還要加上Symbol和BigInt。一共就這些,Symbol不用說,大家都比較熟悉了,BigInt是后來又加上的“大數”類型。現代瀏覽器也是支持的。這些數據類型中,又分成了兩類,我比較喜歡叫做值類型(String、Number、BigInt、Boolean、Symbol、Null、Undefined)和引用類型(Object)。也或許有人喜歡叫做簡單類型和復雜類型。但是我覺得這樣形容比較模糊。“值”和“引用”或許更貼切一些。
到這里,本該告一段落,但這里我挖了一個小小的坑,我問的是js的數據類型,實際上,我上面所說的這些數據類型,在js的規范里,叫做語言類型。語言類型是什么意思呢?我們大膽猜測一下,語言類型就是指,我們在日常開發的代碼中所書寫的基本的數據類型,就叫做語言類型,它是我們在使用這門語言的時候,所采用、依照的數據類型。
那...你的意思是說,還有另外一種類型?是的,在js的規范中,還有一種類型叫做規范類型,規范類型是干什么用的呢?規范類型對應於算法中用於描述ECMAScript語言構造和ECMAScript語言類型語義的單元。(A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types.)
什么意思呢?簡單來說,規范類型就是在語言背后運行時,所使用的、我們無法獲取或“見到”的數據類型。規范類型大概有以下幾種:
-
The Set and Relation Specification Types
-
The List and Record Specification Types
-
The Completion Record Specification Type
-
The Reference Specification Type
-
The Property Descriptor Specification Type
-
The Environment Record Specification Type
-
The Abstract Closure Specification Type
-
Data Blocks
一共就這八種,那具體這些規范類型是做什么的,以及怎么用,這里不詳細說,有興趣的可以在鏈接中找到,免得說多了就有點主次不分了,我們僅僅只是在聊數據類型的時候,把規范類型提一下。
ok,我們現在知道了js的語言類型有哪些,但是這里又出現了一個問題,就是我怎么判斷一個數據是什么類型呢?也就是傳說中的“我是誰”的問題!
二、我是誰之typeof
typeof想必大家都比較熟悉了,它能判斷一個“數據”的類型,但是大家也知道,typeof並不能判斷所有的“類型”(其實,typeof是可以判斷所有的類型的,當然,這個“所有類型”的前提是“基本數據類型”,而並不包括構造函數、包裝函數等創建的對象)。我們先來看張表,下面是typeof運算符的所有的結果集:
typeof val | Result |
Undefined | "undefined" |
Null | "object" |
Boolean | "boolean" |
Number | "number" |
String | "string" |
Symbol | "symbol" |
BigInt | "bigint" |
Object (does not implement [[Call]]) | "object" |
Object (implements [[Call]]) | "function" |
我們再看下實際的運算結果,畢竟我逼逼逼逼,也不如show 下 code:
console.log(typeof 1); console.log(typeof "1"); console.log(typeof true); console.log(typeof Symbol("我是Symbol")); console.log(typeof null); console.log(typeof undefined); console.log(typeof BigInt('1111111111111111')); console.log(typeof function () { }); console.log(typeof {});
上面打印的結果是這樣的:
number string boolean symbol object undefined bigint function object
null為啥是object我不想說了。要強調的是,以上的“結果”,都是字符串。console.log(typeof typeof {})。這句話,會告訴你typeof的所有結果的類型都是字符串。
不知道你們從結果和上面的表格中發沒發現一個問題,就是Object (does not implement [[Call]])和Object (implements [[Call]])的結果竟然是不一樣的,一個是function,一個是object?這是怎么回事?從英文的翻譯來看,解釋為:如果在該對象上(或其原型鏈上)可以找到[[call]]私有方法,那么就是typeof的結果就是function,如果找不到,那么結果就是object。
哦...原來是這樣,也就是說,實際上Object有兩種結果...一個object,一個function。那我怎么區分呢?我怎么知道到底是object還是function。我怎么知道它是對象還是函數?我們繼續往下看。
三、萬物皆對象
想必無論是js的初學者還是資深大師,都一定聽說過,在js里,一切皆對象。是嘛?那前面說的值類型的數據也是對象么?是的,它們也可以算是一種對象!我們后面會詳聊。現在我只想專心的聊聊對象。
三點一:什么是對象?
這個有點復雜,我所理解的對象是這樣的:使用new運算符,通過構造函數創建的一個包含一系列屬性集合的數據類型。但是這只是一個片面的解釋,實際上,如果忽略對象的創建過程,即我們不去糾結它是怎么來的,只是專注於它是什么,那么對象就是具有唯一性的、屬性和行為的集合,僅此而已。
三點二:對象的種類有哪些?
其實對象的種類有很多,而且大多數在我們開發的時候已經經常使用了,只是我們從未真正的去做一個比較區分罷了。要知道,從不同的角度看待同一個問題,結果也會有所區別,所以,從不同的角度去區分類別,結果也不盡相同的。比如,按照構造函數的角度區分,可以分為函數對象(具有[[call]]私有字段,表面上來看,就是可以調用call方法的函數)和構造器對象(具有[[constuctor]]私有字段,表面上來看,就是具有constructor屬性的函數)。我們僅從比較大眾和公認的角度,對象大概的分類如下(其實不想要復雜的理解的話,就是宿主對象和內置對象兩種,再往細了分,實際上沒有特別巨大的區別,它們本質上極其類似):
下面,我們都簡單解釋下,弄清楚對象的分類,對於后面的學習,會更加深入和清晰。
簡單來說,宿主即JavaScript代碼所運行的載體,大多數時候是瀏覽器,但是也可能是node或其他復雜的環境上。而JavaScript是可以使用“該環境”的相關對象的,即稱為宿主對象。宿主對象本身又分為固有和用戶可創建兩種。無需多說。
而內置對象,則是JavaScript本身內置(built-in)的對象,與運行載體無關。其中內置對象又可以分為三種,即:固有對象、原生對象、普通對象。
普通對象最好理解,就是我們通過對象字面量、Object構造器、Class關鍵字定義類創建的對象,它能夠被原型繼承,換句話說,就是我們使用的最簡單的直接的對象形式。
固有對象由標准規定,隨着JavaScript運行時創建而自動創建的對象實例。固有對象在任何JavaScript代碼執行前就已經創建了,它們通常扮演着基礎庫的角色。類其實就是固有對象的一種,固有對象目前有150多種。
原生對象,即可以通過原生構造器創建的對象。標准中,提供了30多個構造器,通過這些構造器,可以使用new 運算創建新的對象,所以我們把這些對象稱作原生對象。這些構造器創建的對象多數使用了私有字段:
* Error:[[ErrorData]]
* Boolean:[[BooleanData]]
* Number:[[NumberData]]
* Date:[[DateValue]]
* RegExp:[[RegExpMatcher]]
* Symbol:[[SymbolData]]
* Map:[[MapData]]
這些字段使得原型繼承方法無法正常工作,所以,我們可以認為這些原生對象都是為了特定能力或者性能,而設計出來的特權對象。
三點三:值類型也是Object?么?
那么值類型?值也是對象么?下面的代碼就可以解釋這個問題。
var objNum = new Number(1); console.log(objNum) // objNum.a = 1; console.log(typeof objNum) // console.log(objNum.a) // console.log(objNum)
把上面的代碼,復制到你的現代瀏覽器里,就會發現,實際上,objNum是一個對象,而我們通過字面量所創建的數字,本質上,也是通過上面的方法創建的。所以,值類型,其實也是對象,只是它被隱藏起來了罷了。
那么函數呢?typeof的結果里不是還有個function么?其實函數也是對象。
注意:這里有一個問題,就是值類型到底算不算是對象!首先,我覺得值類型也算是對象的。原因上面說過了,但是這里有一個問題就是,通過字面量創建的值類型,它的表現形式確實不是對象,而且也無法添加屬性。那么,這里我猜測,為了便於開發者使用,通過字面量創建的值類型,經過了一定的轉換。所以,並不是值類型不是對象,而是通過字面量創建的值類型,拋除了一部分對象的特性,使其只專注於自身的“值”。(以上純屬個人理解)
四、函數
上一小節我說了,對象是對象,值也是對象,在結尾的時候又說了,函數也是對象?
var fun = function () { }; var fun1 = function () { }; console.log(fun === fun1) // fun.a = 1; // fun.b = function () { // console.log(this.a) // } // console.log(fun.a) // fun.b()
其實上面的代碼我偷懶了,但是我覺得你們看得懂。不懂的話...就...留言吧。(其實就是注釋啦)。
通過以上的代碼,我們發現,fun具有唯一性,兩個空函數是不相等的,且可以擁有屬性(a)和行為(b),所以,這絕壁是一個對象啊。沒毛病!
之前在對象的部分,我們給對象做了簡單的分類。那么實際上,函數也是有各種不同的分類的。為什么呢?其實這里可以理解的很簡單:對象是如何產生的?理論上講,對象是通過函數,也即構造函數創建的(當然有一些原生對象是JS生成的),無論我們以何種形式得到的對象,本質都是如此。即便var obj = {};這樣的代碼,實際上也是var obj = new Object()生成的。所以,對象有不同的種類,函數其實也有,通過對象的分類,可以簡單推算出(這里是我結合ECMAScript標准和對象的分類整理的):函數只有內置函數(其實這里說,只有內置函數是片面的,但是方向是沒問題的,其實還有一些比如bound function,strict function,但是我覺得這些實際上並不完全的屬於一個獨立的分類或者體系,它更像是內置函數的一個子集,所以我們這里簡單理解成內置函數就可以了,比如我們自己通過字面量創建的函數,實際上也是通過new Function得到的函數)。
內置函數大概有以下幾種:Number、Date、String、Boolean、Object、Function、Error、Array等常用的八種。還有Global不能直接訪問,Arguments僅在函數調用時由JS引擎創建,Math和JSON是以對象的形式存在的。
這么多構造器可以創建對象,我怎么知道它是由誰創建的?我怎么知道我是誰呢?typeof在此刻好像就不那么靈光了。
五、我是誰之instanceof
之前說了,內置構造器有很多種,那么我怎么區分“我是誰”呢?這時instanceof就派上用場了。instanceof的作用就是:判斷a 與 b的原型上游,是否存在指向的相同的引用(假設是a instanceof b,也就是分別判斷a.__proto__和b.prototype上游)。isntanceof不僅僅可以使用在實例與構造函數之間,也可以用在父類與子類之間(反正就是判斷a、b能否在原型鏈上找到同一個引用)。
function Person() { }; var p = new Person(); console.log(p instanceof Person) function Zaking() { }; Zaking.prototype = p; var z = new Zaking(); console.log(z instanceof Person)
六、函數與對象間關系
前面說了基本的數據類型、對象、函數等的分類,下面我們就來詳細的說一下函數與對象間的關系,我們先來看一個簡單的代碼:
function Person() { }; var p = new Person();
就是這樣,很簡單,我們創建一個函數(構造函數),然后生成一個對應的實例(對象)。那他倆之間有什么關系呢?又是如何體現的呢?
我們可以通過constructor屬性,來判斷:
console.log(p.constructor === Person) //true
我們發現實例對象p的構造函數指針正是Person,但是有一個奇怪的地方,就是:
console.log(p.hasOwnProperty('constructor')) //false
就是,p本身並沒有constructor屬性,那這個constructor是從哪來的呢?
七、prototype
我們先暫時忘記實例上的constructor是從哪來的這個問題。我們先來看一下prototype這個東西。
在此之前,我們先要了解另外一種對象的分類方式,即把對象分為函數對象和普通對象,那這樣分類的依據是什么呢?從規范上來說,即該對象是否擁有call方法,從表象一點的方向來看,可以用typeof的結果是function還是object來區分。typeof的結果是function的就是函數對象,typeof結果是object,就是普通對象。
我們之前說過了,函數也是一種對象,所以函數本身也是有一些屬性和方法的,而JavaScript自己就給函數對象添加了一些屬性,其中就有prototype。每一個函數對象都有prototype原型對象。
console.log(Person.prototype)
打印的結果是這樣的:
唉?這里有個constructor屬性,它指向了Person自己?是的
console.log(Person.prototype.constructor === Person);//true
那結合之前的代碼p.constructor === Person,不就是說:
console.log(Person.prototype.constructor === p.constructor);// true
沒錯,我們此時,找到了對象(實例)與函數(構造函數)之間的關系了!
八、__proto__
上面一小節,我們驗證了對象與函數間的關系,但是仍舊遺留了一個問題,就是實例p本身並沒有constructor屬性,那它是從哪來的呢?這就不得不說一下,__proto__這個東西了,它叫做隱式原型。每一個對象都有一個__proto__隱式原型(原型對象,也是對象,所以它也有__proto__,即A.prototype.__proto__)(但是__proto__並不是規范,它只是瀏覽器的實現而已)。
那,之前說過實例p沒有constructor屬性,那p的__proto__是不是可能會有constructor呢?我們猜測一下唄?
console.log(p.__proto__.hasOwnProperty('constructor')); //true
唉?它是在實例的隱式原型上的,沒問題!那這樣的話,是不是說...
console.log(p.__proto__ === Person.prototype);//true
沒錯!就是這樣的,實例的隱式原型和構造函數的原型是相等的,指向同一個指針的!
九、原型鏈
上一小節,我們初步的看到了原型與隱式原型間的關系,實際上,這就是原型鏈的初步形成。但是,我相信大家想知道的肯定不單單是這些。嗯...當然。我們下面就一點點剖析。
通過之前的代碼,我們知道了實例的隱式原型是等於構造函數的原型的。那之前又說過,構造函數的原型也是一個對象,那它也有隱式原型:
console.log(Person.prototype.__proto__)
沒錯,但是這里首先有一個問題,就是Person.prototype是什么?其實它就是一個對象啊。所以它才有__proto__啊。那Person.prototype.__proto__的結果是什么呢?
我猜是Object.prototype:
console.log(Person.prototype.__proto__ === Object.prototype); // true
那依此類推,Object.prototype也有__proto__啊。
console.log(Object.prototype.__proto__); // null
唉?null?是的,到這里實際上,Object.prototype就沒有隱式原型了,因為到頂了。
ok,到這里我們原型鏈第一階段的問題已經解決了,下面我們開始第二階段的問題。
還記不記得我之前說過,函數對象擁有prototype原型, 每一個對象都擁有__proto__隱式原型,所以!函數對象,也是對象!也有__proto__隱式原型。即:
console.log(Person.__proto__);
那Person.__proto__又是從哪來的呢?那根據前面第一階段的代碼,假設,Person是一個對象,那它肯定是由某個構造函數創建出來的,那在js中是誰創建出一個Person函數的呢?換句話說,我們在function Person(){}的時候,實際上Person是這樣創建的var Person = new Function()。(注意!絕對不推薦這樣創建函數,這里只是演示它從哪來的。)
哦吼?原來是這樣,那也就是說。
console.log(Person.__proto__ === Function.prototype); // true
那Function.prototype也是一個對象。也就是說:
console.log(Function.prototype.__proto__ === Object.prototype); // true
到了這里,是不是有點破案的味道了?
到此就結束了么?沒有,其實到這里我們才剛開始。
先簡單總結一下,剛開始,我們從一個對象的角度(即構造函數生成的實例),然后我們又從函數的角度(即構造函數)分為兩條線來捋了一下,其實這就是原型鏈了,只是function和object互相的關系有點煩人罷了。
之前說過,函數有幾種內置構造函數,也可以稱之為包裝函數,大概我們可以用的有那么幾種Number、Date、String、Boolean、Object、Function、Error、Array。一些比如:Number、Date、String、Boolean、Error、Array,Regex這些,對應的對象都是由這些包裝函數生成的。其實,我們可以把這些(Number、Date、String、Boolean、Error、Array)在原型鏈的概念中,當作是我們自己通過Function創建的Person構造函數。因為它們在這里的體現形式和作用、使用方式都是一模一樣的。不信你看:
console.log(Person.__proto__ === Function.prototype);//true console.log(Number.__proto__ === Function.prototype);//true console.log(String.__proto__ === Function.prototype);//true console.log(Boolean.__proto__ === Function.prototype);//true console.log(Date.__proto__ === Function.prototype);//true console.log(Array.__proto__ === Function.prototype);//true console.log(Error.__proto__ === Function.prototype);//true console.log(RegExp.__proto__ === Function.prototype);//true
毫無疑問,都是true。並且,你再看:
console.log(Function.__proto__ === Function.prototype);//true console.log(Object.__proto__ === Function.prototype);//true
因為Function和Object在這里都是作為包裝函數所出現的,所以,它們必然都是由Function創建的(在這里,不要多想,把Function和Object都當作包裝函數就好了)。所以,我們可以得到:所有的構造函數的__proto__都指向Function.prototype,Function.prototype是一個空函數(自己去console)。Function.prototype也是唯一一個typeof結果為function的prototype(特殊記憶一下,其實如果拋去對象的話,說Function是萬物之源也沒錯,記住,是拋除Object的話!)。其他所有的構造器的prototype都是object。
相信到了這里,大家有一絲絲的頓悟,但是又有些混亂。混亂的主要原因就在於Object和Function這兩個東西(構造函數)。因為它們本身即作為構造函數出現,又作為對象出現。導致它們之間有循環調用的存在。
console.log(Function.__proto__ === Function.prototype);//true console.log(Object.__proto__ === Function.prototype);//true console.log(Function.prototype.__proto__ === Object.prototype);//true
我從之前的代碼中,摘出了這三句。當Function和Object都作為“對象”時,它們的隱式原型都指向Function.prototype,沒問題,因為它們都是作為構造函數存在的“函數對象”。而,這里Function.prototype本質上又是一個對象,所以它的隱式原型Function.prototype.__proto__就指向了Object.prototype。也沒問題。然后就是Object.prototype.__proto__ === null就結束到頂了。
十、總結
其實到這里,原型鏈的部分就結束了。我們來復習,整理一下之前我們說過的內容。
首先,我們聊了js的數據類型,分為規范類型和語言類型,規范類型有8種,主要用於標准的描述和內部的實現,語言類型也有8種(Number、String、Boolean、Undefined、Null、BIgInt、Symbol、Object)。
然后,通過typeof 運算符對於Object運算時產生的不同結果,引出了對象和函數。並對對象和函數都做了類別的區分。
再然后,通過簡單描述對象與函數間關系,我們引出了Object和Function之間復雜的原型關系。
最后,我們分別講了prototype和__proto__,然后我們聊了下__proto__和prototype之間的關系。
然后這里要強調幾個關鍵點:
- 對象可以分為“函數對象”和“普通對象”,函數對象就是函數,它在js中也算是一種對象,可以擁有行為和屬性,並且具有唯一性。普通對象就是通過new 構造函數或字面量等方法創建的對象。
- 只有函數對象擁有prototype原型對象,但是所有的對象(當然就是函數對象和普通對象)都有__proto__隱式原型。
- Object和Function這兩個構造器比較特殊,由於它們本身是函數對象,所以即擁有prototype又擁有__proto__。所以,實際上,一切復雜的源頭都在這里。
十一、現代原型操作方法
實際上,__proto__這個東西,在現代標准(指最新的提案或已納入標准的某個ES版本,具體哪個版本的好難找,就先這么叫吧)中,已經不推薦使用。那我們如何操作原型呢?
下面,我們就學習一下操作或涉及到原型的一些方法:
1、Object.prototype.hasOwnProperty,(現在知道obj.hasOwnProperty這樣的用法從哪來的了吧),方法會返回一個布爾值,指示對象自身屬性(即不是從自己或祖先的原型中存在的)中是否具有指定的屬性(也就是,是否有指定的鍵)。
const object1 = {}; object1.property1 = 42; console.log(object1.hasOwnProperty('property1')); // true console.log(object1.hasOwnProperty('toString')); // false console.log(object1.hasOwnProperty('hasOwnProperty')); // false console.log(object1.__proto__.hasOwnProperty('toString')); // true console.log(object1.__proto__.hasOwnProperty('hasOwnProperty')); // true console.log(Object.prototype.hasOwnProperty('toString')); // true console.log(Object.prototype.hasOwnProperty('hasOwnProperty')); // true
2、Object.prototype.isPrototypeOf(),用來檢測一個對象是否存在於另一個對象的原型鏈上。
實際上,它就相當於是通過恆等運算符來判斷下obj的隱式原型是否和構造函數Object的原型指向同一個指針,當然,這只是一個簡單的例子,如果我們自定義某一個原型的指向也是可以的:
function Foo() { } function Bar() { } function Baz() { } Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype); var baz = new Baz(); console.log(Baz.prototype.isPrototypeOf(baz)); // true console.log(Bar.prototype.isPrototypeOf(baz)); // true console.log(Foo.prototype.isPrototypeOf(baz)); // true console.log(Object.prototype.isPrototypeOf(baz)); // true
// 上面的代碼實際上相當於這樣:
哦對,在開始解釋、類比之前,還得多說一句,就是Object.create(后面有其實)是干啥玩意的的。其實看一段代碼你就知道了:
...這里承接上面的代碼哦... console.log(new Foo().constructor === Object.create(Foo.prototype).constructor)
你猜結果是啥,是true唄,其實Object.create就是new Foo()。那,我們再來看個有意思的...算了,這里不多說了,不是地方,具體到Object.create的時候再說吧。這里你只需要記住上面的console結果的意義就可以了。
那么,我們繼續按照傳統的、我們前面學過的那種方式來重寫一下這些代碼:
function Foo() { } function Bar() { } function Baz() { } Bar.prototype = new Foo(); Baz.prototype = new Bar(); var baz = new Baz(); console.log(Baz.prototype === baz.__proto__); // true console.log(Bar.prototype === baz.__proto__); // false console.log(Foo.prototype === baz.__proto__); // false console.log(Object.prototype === baz.__proto__); // false // 唉?咋不對,咋跟之前的代碼結果不一樣?你不是說了類似的么? // 我們再來看這句話:用來檢測一個對象是否存在於另一個對象的原型鏈上。 // 看!是原型鏈上!!!不是這個對象的原型上。 // 所以,最開始的代碼可以是這樣的 console.log(Baz.prototype === baz.__proto__); console.log(Bar.prototype === baz.__proto__.__proto__); console.log(Foo.prototype === baz.__proto__.__proto__.__proto__); console.log(Object.prototype === baz.__proto__.__proto__.__proto__.__proto__);
唉?我擦嘞,好像有點意思誒?其實我覺得到這里,大家就都懂了。但是我還是說一下吧,我們仍舊來看代碼:
// 我們先從頭看,要注意,這里的頭,是從var baz = new Baz();開始的 var baz = new Baz(); // 這句話所導致的結果就是 console.log(Baz.prototype === baz.__proto__); // 這毋庸置疑的對吧。 // 那我們看下一句: Baz.prototype = new Bar(); // 實際上這句話的意思就是 var bar = new Bar(); // 所以 console.log(Bar.prototype === bar.__proto__); Baz.prototype = bar; // 我覺得到這里差不多你就懂了 // 我們繼續,同上 Bar.prototype = new Foo(); // 也即 var foo = new Foo(); console.log(Foo.prototype === foo.__proto__); Bar.prototype = foo; // 其實后面還可以有點,但是我不想說了,再不懂,我真的傷心了,再有,你自己寫下Object的那個console是怎么來的吧。
好了,說的有點多了,我們看下一個吧。
3、Object.getOwnPropertyNames(),方法返回一個由指定對象的所有自身屬性(不包括原型鏈上)的屬性名(包括不可枚舉屬性但不包括Symbol值作為名稱的屬性)組成的數組。換句話說,它會返回除Symbol作為key的所有自身屬性的數組。
let sym = Symbol('sym'); var obj = { a: 1, b: 2, c: { a: 1 } } obj[sym] = 1 Object.defineProperty(obj, 'p1', { value: 42, writable: false, enumerable: false }); Object.prototype.m = 'abcd'; obj.__proto__.n = '1234'; console.log(obj[sym]); console.log(Object.getOwnPropertyNames(obj));
4、Object.getOwnPropertySymbols(),方法返回一個給定對象自身的所有 Symbol
屬性的數組。上一個不能返回symbol的,這回這個只能返回symbol的。
let sym = Symbol('sym'); var obj = { a: 1, b: 2, c: { a: 1 } } obj[sym] = 1 Object.defineProperty(obj, 'p1', { value: 42, writable: false, enumerable: false }); console.log(obj[sym]) console.log(Object.getOwnPropertySymbols(obj))
5、Object.getPrototypeOf(),該方法返回指定對象的原型的值。其實個人理解,就相當於Object.prototype,或者obj.__proto__。
console.log(Object.getPrototypeOf({})) console.log(Object.getPrototypeOf({}) === {}.__proto__) console.log(Object.getPrototypeOf({}) === Object.prototype)
還有這:
var num = new Number(); console.log(Object.getPrototypeOf(num)) console.log(Object.getPrototypeOf(num) === num.__proto__) console.log(Object.getPrototypeOf(num) === Number.prototype)
這個挺簡單的,我感覺沒啥問題,就不多說了。
6、Object.setPrototypeOf(),方法設置一個指定的對象的原型到另一個對象或null。應盡量避免更改原型的屬性,而是使用Object.create創建一個擁有你需要的原型屬性的對象。
如果對象的原型屬性被修改成不可擴展(通過 Object.isExtensible()
查看),就會拋出 TypeError
異常。如果prototype
參數不是一個對象或者null
(例如,數字,字符串,boolean,或者 undefined
),則什么都不做。否則,該方法將obj
的原型
修改為新的值。
要注意的是,只是“設置”了某個指定的對象的原型,而不是更改了整個原型鏈,很好理解吧。其實說是改變了原型鏈也行,因為若是上游的原型變了,下游的原型鏈自然也就變了。
其實上面的代碼等同於:
var obj3 = new Object(); var obj4 = new Object(); obj3.__proto__ = { m : 3 }; console.log(obj3.__proto__) console.log(Object.prototype) console.log(Object.prototype === obj3.__proto__) console.log(obj4.__proto__) console.log(Object.prototype === obj4.__proto__) // 所以我們可以知道,本質上,Object.prototype它們指向的對象內容相同,但是不代表它們指向的是同一個對象。 // 所以,我們更改了對象的原型后,自然就不想等了。 // 那要是想要想等怎么辦?其實也不難,方法有很多。 // 比如: Object.prototype = {z:9}; var a = new Object(); console.log(Object.prototype === a.__proto__); console.log(Object.prototype); console.log(a.__proto__) // 其實,這算是繼承啦,不多說。
7、Object.create(),
方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__。換句話說,就是給新創建的對象指定其原型對象。
var obj = { a: 1 }; var objx = Object.create(obj); console.log(objx.__proto__); var objy = { a: 2 }; var objz = {}; objz.__proto__ = objy; console.log(objz.__proto__); var objm = Object.create(null); console.log(objm.__proto__ === Object.prototype)
該方法還有第二個可選參數。詳情可於MDN查看。
8、Object.assign(),方法用於將所有自身可枚舉屬性的值從一個或多個源對象復制到目標對象。它將返回目標對象。注意,是所有可枚舉屬性,包括Symbol,但不包括原型上的屬性。
let sym = Symbol('sym'); var obj = { a: 1, b: 2, c: { a: 1 } } obj[sym] = 1 Object.defineProperty(obj, 'p1', { value: 42, writable: false, enumerable: false }); obj.__proto__.m = 1; var a = Object.assign({}, obj) console.log(a)
9、Object.keys(),方法會返回一個由一個給定對象的自身可枚舉屬性組成的數組,數組中屬性名的排列順序和正常循環遍歷該對象時返回的順序一致。Symbol無法被枚舉出來。
let sym = Symbol('sym'); var obj = { a: 1, b: 2, c: { a: 1 } } obj[sym] = 1 Object.defineProperty(obj, 'p1', { value: 42, writable: false, enumerable: false }); obj.__proto__.m = 1; var a = Object.assign({}, obj) console.log(Object.keys(a), '---')
10、Object.values(),方法返回一個給定對象自身的所有可枚舉屬性值的數組,值的順序與使用
for...in
循環的順序相同(區別在於 for-in 循環會把原型鏈中的屬性也枚舉出來)。
let sym = Symbol('sym'); var obj = { a: 1, b: 2, c: { a: 1 } } obj[sym] = 1 Object.defineProperty(obj, 'p1', { value: 42, writable: false, enumerable: false }); obj.__proto__.m = 1; var a = Object.assign({}, obj) console.log(Object.values(a), '---') for (var k in obj) { console.log(k, '--k--') }
11、Object.entries(),方法返回一個給定對象自身可枚舉屬性的鍵值對數組。
let sym = Symbol('sym'); var obj = { a: 1, b: 2, c: { a: 1 } } obj[sym] = 1 Object.defineProperty(obj, 'p1', { value: 42, writable: false, enumerable: false }); obj.__proto__.m = 1; console.log(Object.entries(a))
12、Object.is(),該方法判斷兩個值是否為同一個值。這個好像沒啥好說的,但是想要說的又很多。去看MDN吧。
13、Object.seal(),
該
方法封閉一個對象,阻止添加新屬性並將所有現有屬性標記為不可配置。當前屬性的值只要原來是可寫的就可以改變。
14、Object.freeze(),
該
方法可以凍結一個對象。一個被凍結的對象再也不能被修改;凍結了一個對象則不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結一個對象后該對象的原型也不能被修改。freeze()
返回和傳入的參數相同的對象。
15、Object.preventExtensions(),
該
方法讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性。
16、Object.isExtensible(),
該
方法判斷一個對象是否是可擴展的(是否可以在它上面添加新的屬性)。
17、Object.isFrozen(),
該
方法判斷一個對象是否被凍結。
18、Object.isSealed(),
該
方法判斷一個對象是否被密封。
19、Object.fromEntries(),
該
方法把鍵值對列表轉換為一個對象。
const entries = new Map([ ['foo', 'bar'], ['baz', 42] ]); const obj = Object.fromEntries(entries); console.log(obj);
20、Object.
defineProperty
(),方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。注意:應當直接在 Object
構造器對象上調用此方法,而不是在任意一個 Object
類型的實例上調用。默認情況下,使用 Object.defineProperty()
添加的屬性值是不可修改。
var objc = {}; Object.defineProperty(objc, 'property1', { value: 42 }); objc.property1 = 77; // throws an error in strict mode console.log(objc.property1); // expected output: 42
21、Object.
defineProperties
(),方法直接在一個對象上定義新的屬性或修改現有屬性,並返回該對象。該方法可以定義多個屬性。
var obj = {}; Object.defineProperties(obj, { 'property1': { value: true, writable: true }, 'property2': { value: 'Hello', writable: false } // etc. etc. });
22、Object.getOwnPropertyDescriptor(),方法返回指定對象上一個自有屬性對應的屬性描述符。
var objc = {}; Object.defineProperty(objc, 'property1', { value: 42 }); objc.property1 = 77; // throws an error in strict mode console.log(objc.property1); // expected output: 42 var desc1 = Object.getOwnPropertyDescriptor(objc, 'property1'); console.log(desc1.configurable) console.log(desc1.writable) console.log(desc1)
23、Object.getOwnPropertyDescriptors(),方法用來獲取一個對象的所有自身屬性的描述符。所指定對象的所有自身屬性的描述符,如果沒有任何自身屬性,則返回空對象。
Object.assign()
方法只能拷貝源對象的可枚舉的自身屬性,同時拷貝時無法拷貝屬性的特性們,而且訪問器屬性會被轉換成數據屬性,也無法拷貝源對象的原型,該方法配合 Object.create()
方法可以實現上面說的這些。
Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
24、Object.prototype.toString(),每個對象都有一個
toString()
方法,當該對象被表示為一個文本值時,或者一個對象以預期的字符串方式引用時自動調用。默認情況下,toString()
方法被每個 Object
對象繼承。如果此方法在自定義對象中未被覆蓋,toString()
返回 "[object type]",其中 type
是對象的類型。以下代碼說明了這一點:
var o = new Object(); console.log(o.toString()); // returns [object Object]
console.log(Object.prototype.toString.call(new Array())); // returns [object Array]
25、Object.prototype.valueOf(),這個東西,用到的不多。自己去看吧
// Array:返回數組對象本身 var array = ["ABC", true, 12, -5]; console.log(array.valueOf() === array); // true // Date:當前時間距1970年1月1日午夜的毫秒數 var date = new Date(2013, 7, 18, 23, 11, 59, 230); console.log(date.valueOf()); // 1376838719230 // Number:返回數字值 var num = 15.26540; console.log(num.valueOf()); // 15.2654 // 布爾:返回布爾值true或false var bool = true; console.log(bool.valueOf() === bool); // true
十二、課后作業之原型小練習
1、以下代碼中的p.constructor的constructor屬性是從哪來的?Person.prototype.constructor呢?
function Person() { }; var p = new Person(); console.log(p.constructor); console.log(Person.prototype.constructor);
答:首先p.constructor是p.__proto__中來的。
console.log(p.constructor === p.__proto__.constructor);
其次,Person.prototype.constructor,是Person.prototype自身的。
console.log(Person.prototype.hasOwnProperty('constructor'))
2、typeof Function.prototype的結果是什么?typeof Object.prototype的結果又是什么?
console.log(typeof Function.prototype); console.log(typeof Object.prototype);
答:typeof Function.prototype的結果是function,typeof Object.prototype是object。Function.prototype比較特殊,因為它的typeof 結果說明它是函數對象,但是它本身又是沒有prototype屬性的。也就是說
console.log(Function.prototype.prototype); // undefined
那,為什么Function.prototype會是一個函數對象呢?解釋是:為了兼容以前的舊代碼...好吧。這就是為什么會有奇葩的:Function.prototype是一個函數,但是Function.prototype的隱式原型又是Object.prototype。即:
console.log(Function.prototype.__proto__ === Object.prototype); // true
3、Object.prototype?Object.__proto__?Function.prototype?Function.__proto__?
答:
console.log({}.__proto__ === Object.prototype); console.log(Object.__proto__ === Function.prototype); console.log(Function.__proto__ === Function.prototype); console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);
其實這里唯一無法理解的是,Function.prototype是一個空函數;
console.log(Function.prototype);
換句話說,Function.prototype是一個函數對象。之前說過,函數對象的原型都是Function.prototype。但是實際上特殊的Function.prototype的原型卻是Object。原因,就是為了兼容舊代碼。所以這里特殊記憶一下吧。
最后,這篇文章到這里就基本上結束了,回過頭來看發現原型的概念似乎並不復雜,也確實如此。復雜的是變化的場景,但是萬變不離其宗。還有一些特殊的情況,可能是歷史原因,也可能是為了兼容,不管怎么樣,這種特殊情況就特殊的記憶一下就好了。
其實最開始寫這篇文章是忐忑的,我看了一些文章,總覺得對原型鏈的描述不夠清晰詳盡,恰好自己最近也在學習一些js的深入內容。所以,就想當作是整理自己的學習思路,來造一造輪子。但是通篇下來,也還是沒有達到我想要的滿意的程度。一些邏輯的承接,一些細節的深入也都還是不夠。
最后,希望這篇文章能給你帶來些許的收獲,也希望你發現了什么不解或者疑問可以留言交流。
其實個人覺得這里有點問題的地方在於MDN中摘抄的現代原型操作方法,由於這些並不屬於本章核心內容,所以我只是做了簡單的摘抄和潦草的分析,如果大家有興趣,可以自己去學一下,后面我也會寫一篇關於繼承的相關文章。一定會包括這些內容。實際上在本篇內容里,多多少少都帶了一些“繼承”,沒辦法,誰讓原型和繼承,本身就很難分割呢。
本文參考及借鑒:
- 最詳盡的 JS 原型與原型鏈終極詳解,沒有「可能是」——Yi罐可樂
- 深入理解javascript原型和閉包(完結)《原型部分》——王福朋
- ECMAScript® 2018 Language Specification
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
感謝以上作者的文章,學以致用,道阻且長。
附、補充(文中未表述清晰或遺漏的重要內容):
1、值類型到底是對象么?(更正第三小節、三點三小節對應內容)
答:其實通過包裝函數創建的值類型是對象!這點毋庸置疑。但是通過字面量創建的值類型是對象么?如果是,那它可以添加屬性么?如果不是,為什么可以使用原型鏈上的方法比如1..toString()(沒寫錯,1..toString())呢?實際上,通過字面量創建的值類型並不能完全的稱之為“對象”。因為它沒有屬性和行為,也不唯一。但是它卻可以使用原型鏈上的方法,究其原因,是因為在js運行時給值類型做了一層包裝,使其可以使用原型鏈上的方法。而並不是因為值類型本身就是對象。
2、我總覺得這篇文章還差點什么,不夠我想要的那種感覺,我其實想要在文章做到由淺入深,但是整理后發現,淺是淺了,淺着淺着就發現浮上來來,一點都不深了。所以,為了不破壞文章的結構和思路(思路是沒問題的),翻來覆去之后,還是把這張圖貼上來了。
想必這張圖在大家學習原型鏈的過程中一定見過不少次,不得不說,這也確實是我覺得最為貼切詳細的一張圖。所以,最后的最后,我們還是用代碼來仔細的過一遍這張圖吧。
function Foo() {}; var f1 = new Foo(); var f2 = new Foo(); var o1 = new Object(); var o2 = new Object(); console.log(f1.__proto__ === Foo.prototype); console.log(Foo.prototype.__proto__ === Object.prototype); console.log(Object.prototype.__proto__ === null); // 這條線到這里就完事了! // 我們來看第二條 console.log(o1.__proto__ === Object.prototype); console.log(Object.prototype.__proto__ === null); // 這個也完事了 // 我們再從另外一個角度來看 console.log(Foo.__proto__ === Function.prototype); console.log(Function.prototype.__proto__ === Object.prototype);// 又來了,下面的不寫了 // 所以,同理 console.log(Object.__proto__ === Function.prototype); // 所以,又來了 console.log(Function.prototype.__proto__ === Object.prototype); // 還有一個 console.log(Function.__proto__ === Function.prototype); // 后面不寫了 // 最后 console.log(Object.prototype.constructor === Object); console.log(Function.prototype.constructor === Function); console.log(Foo.prototype.constructor === Foo);
那么我們來看幾個小問題吧:
console.log(typeof Function.prototype) console.log(typeof Function.__proto__) console.log(Function.__proto__) console.log(Function.prototype) console.log(typeof Object.prototype) console.log(Object.__proto__ === Function.prototype)
最后的最后,其實原型鏈的精髓就是:
- o1.__proto__ === Object.prototype
-
o1.__proto__.constructor === Object.prototype.constructor
- Object.__proto === Function.prototype