Javascript之其實我覺得原型鏈沒有難的那麼誇張!

Zaking發表於2020-08-17

  原型鏈、閉包、事件迴圈等,可以說是js中比較複雜的知識了,複雜的不是因為它的概念,而是因為它們本身都涉及到很多的知識體系。所以很難串聯起來,有一個完整的思路。我最近想把js中有點意思的知識都總結整理一下,雖然逃不開一些一模一樣的內容,但是自己造一下輪子,按照自己的思路。也別有一番味道。

  這篇文章總體來說,是講原型鏈的,但是並不涉及到繼承。繼承的問題,後面會專門拿出來一篇文章來說。但是這篇文章可能很大一部分。也不完全是“原型”,還涉及到很多前置的知識。文章有點長,希望你能耐心讀完,肯定會有不小的收穫!那麼,我們就先從一個問題開始吧!

 

一、請描述一下js的資料型別有哪些?

  就這?這麼簡單的麼?哈哈哈...,我們先從這個問題開始。

  答:js的資料型別有字串String數值Number布林值BooleanNullUndefined物件Object、還要加上SymbolBigInt。一共就這些,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.)

  什麼意思呢?簡單來說,規範型別就是在語言背後執行時,所使用的、我們無法獲取或“見到”的資料型別。規範型別大概有以下幾種:

  1. The Set and Relation Specification Types

  2. The List and Record Specification Types

  3. The Completion Record Specification Type

  4. The Reference Specification Type

  5. The Property Descriptor Specification Type

  6. The Environment Record Specification Type

  7. The Abstract Closure Specification Type

  8. Data Blocks

  一共就這八種,那具體這些規範型別是做什麼的,以及怎麼用,這裡不詳細說,有興趣的可以在連結中找到,免得說多了就有點主次不分了,我們僅僅只是在聊資料型別的時候,把規範型別帶一下。

  ok,我們現在知道了js的語言型別有哪些,但是這裡又出現了一個問題,就是我怎麼判斷一個資料是什麼型別呢?也就是傳說中的“我是誰”的問題?

 

二、我是誰之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),所以,這絕壁是一個物件啊。沒毛病!

  之前在物件的部分,我們給物件做了簡單的分類。那麼實際上,函式也是有各種不同的分類的。為什麼呢?其實這裡可以理解的很簡單:物件是如何產生的?理論上講,物件是通過函式,也即建構函式建立的,無論我們以何種形式得到的物件,本質都是如此。即便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之間的關係。

  然後這裡要強調幾個關鍵點:

  1. 物件可以分為“函式物件”和“普通物件”,函式物件就是函式,它在js中也算是一種物件,可以擁有行為和屬性,並且具有唯一性。普通物件就是通過new 建構函式或字面量等方法建立的物件。
  2. 只有函式物件擁有prototype原型物件,但是所有的物件(當然就是函式物件和普通物件)都有__proto__隱式原型。
  3. 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(),用來檢測一個物件是否存在於另一個物件的原型鏈上。

var obj = {}
console.log(Object.prototype.isPrototypeOf(obj))
// 類似於
console.log(Object.prototype === obj.__proto__)

  實際上,它就相當於是通過恆等運算子來判斷下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

  其實這個更常用、更迷惑一些(主要用於繼承的建立和判斷)。到那時只要你理解了,其實也沒啥。

 

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
});
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)

 

6、Object.setPrototypeOf(),方法設定一個指定的物件的原型到另一個物件或null。應儘量避免更改原型的屬性,而是使用Object.create建立一個擁有你需要的屬性的物件。

  如果物件的原型屬性被修改成不可擴充套件(通過 Object.isExtensible()檢視),就會丟擲 TypeError異常。如果prototype引數不是一個物件或者null(例如,數字,字串,boolean,或者 undefined),則什麼都不做。否則,該方法將obj原型修改為新的值。

Object.setPrototypeOf(obj1, { m: 1 })
console.log(obj1.__proto__)
console.log(Object.prototype)
console.log(Object.prototype === obj1.__proto__)
console.log(obj2.__proto__)
console.log(Object.prototype === obj2.__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)

  該方法還有第二個可選引數。

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

  // 以上第十一小節內容,幾乎完全來自MDN。摘抄至此。

   

十二、課後作業之原型小練習

 

 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);
 console.log(p.__proto__.hasOwnProperty('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中摘抄的現代原型操作方法,由於這些並不屬於本章核心內容,所以我只是做了簡單的摘抄和潦草的分析,如果大家有興趣,可以自己去學一下,後面我也會寫一篇關於繼承的相關文章。一定會包括這些內容。實際上在本篇內容裡,多多少少都帶了一些“繼承”,沒辦法,誰讓原型和繼承,本身就很難分割呢。

 

本文參考及借鑑:

  1. 最詳盡的 JS 原型與原型鏈終極詳解,沒有「可能是」——Yi罐可樂
  2. 深入理解javascript原型和閉包(完結)《原型部分》——王福朋
  3. ECMAScript® 2018 Language Specification 
  4. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object 

 

  感謝以上作者的文章,學以致用,道阻且長。

 

 

附、補充:

  1、值型別到底是物件麼?(更正第三小節、三點三小節對應內容)

  答:其實通過包裝函式建立的值型別是物件!這點毋庸置疑。但是通過字面量建立的值型別是物件麼?如果是,那它可以新增屬性麼?如果不是,為什麼可以使用原型鏈上的方法比如1..toString()(沒寫錯,1..toString())呢?實際上,通過字面量建立的值型別並不能完全的稱之為“物件”。因為它沒有屬性和行為,也不唯一。但是它卻可以使用原型鏈上的方法,究其原因,是因為在js執行時給值型別做了一層包裝,使其可以使用原型鏈上的方法。而並不是因為值型別本身就是物件。

相關文章