javascript物件包含哪些要素_JavaScript引擎基礎知識
技術標籤:javascript物件包含哪些要素
原文連結:
https://mathiasbynens.be/notes/shapes-ics
JavaScript引擎管道
從編寫的JavaScript程式碼開始。JavaScript引擎解析原始碼,並將其轉換為抽象語法樹(AST)。基於該AST,直譯器可以開始做它的事情併產生位元組碼。大!那時引擎實際上正在執行JavaScript程式碼。 為了使其執行更快,可以將位元組碼與分析資料一起傳送到優化編譯器。優化編譯器根據其具有的效能分析資料進行某些假設,然後生成高度優化的機器程式碼。如果某個假設在某個時候被證明是不正確的,則優化編譯器將進行反優化,然後返回到直譯器。JavaScript引擎中的直譯器/編譯器管道
現在,讓我們放大該管道中實際執行JavaScript程式碼的部分,即解釋和優化程式碼的位置,並介紹一些主要JavaScript引擎之間的差異。一般來說,有一個包含直譯器和優化編譯器的管道。直譯器會快速生成未優化的位元組碼,優化編譯器會花費更長的時間,但最終會生成高度優化的機器程式碼。這個通用管道幾乎與V8(Chrome和Node.js中使用的JavaScript引擎)的工作方式完全相同:
V8中的直譯器稱為Ignition,負責生成和執行位元組碼。在執行位元組碼時,它會收集效能分析資料,這些資料可用於稍後加快執行速度。當某個函式變 熱時(例如,當它經常執行時),所生成的位元組碼 和Safari和React Native中使用的Apple JavaScript引擎JavaScriptCore(縮寫為JSC)通過三種不同的優化編譯器將其發揮到了極致。LLInt(低階直譯器)優化到Baseline編譯器,然後可以優化到DFG(Data Flow Graph)編譯器,後者又可以優化到FTL(Fast Than Light)編譯器。
為什麼某些引擎比其他引擎具有更多的優化編譯器?這都是關於權衡的。直譯器可以快速生成位元組碼,但是位元組碼通常效率不高。另一方面,優化的編譯器會花費更長的時間,但最終會產生效率更高的機器程式碼。在快速執行程式碼(直譯器)或花費更多時間之間存在權衡,但是最終要以最佳效能(優化編譯器)執行程式碼。一些引擎選擇新增具有不同時間/效率特性的多個優化編譯器,從而以額外的複雜性為代價,對這些折衷方案進行更細粒度的控制。另一個折衷與記憶體使用有關。
我們剛剛強調了每個JavaScript引擎在直譯器和優化編譯器管道方面的主要區別。但是除了這些差異之外,在所有方面,所有JavaScript引擎都具有相同的體系結構:有一個解析器和某種直譯器/編譯器管道。
JavaScript的物件模型
通過放大某些方面的實現方式,讓我們看看JavaScript引擎還有哪些共同點。
例如,JavaScript引擎如何實現JavaScript物件模型,它們使用哪些技巧來加快對JavaScript物件的屬性的訪問?事實證明,所有主要引擎都非常類似地實現了這一點。ECMAScript規範實質上將所有物件定義為字典,並且字串鍵對映到 屬性attribute。 除了[[Value]]
本身,規範還定義了以下屬性:
[[Writable]]
確定是否可以將屬性重新分配給
[[Enumerable]]
確定該屬性是否顯示在
for
-
in
迴圈中,
並[[Configurable]]
確定是否可以刪除該屬性。
[[double square brackets]]
符號看起來很時髦,但這只是規範表示不直接向JavaScript公開的屬性的方式。您仍然可以使用
Object.getOwnPropertyDescriptor
API 來獲取JavaScript中任何給定物件和屬性的這些屬性屬性:
constobject={foo:42};
Object.getOwnPropertyDescriptor(object,'foo');
//→{value:42,writable:true,enumerable:true,configurable:true}
好的,這就是JavaScript定義物件的方式。陣列呢?您可以將陣列視為物件的特例。區別之一是陣列對陣列索引有特殊處理。在此,
陣列索引是ECMAScript規範中的一個特殊術語。在JavaScript中,陣列限制為2³²−1個專案。陣列索引是該限制內的任何有效索引,即0到2³²−2之間的任何整數。另一個區別是陣列還具有神奇的
length
屬性。
constarray=['a','b'];
array.length;//→2
array[2]='c';
array.length;//→3
在這個例子中,陣中擁有
length
的2
在建立時它。然後,我們將另一個元素分配給index 2
,然後length
自動更新。
JavaScript定義與物件相似的陣列。例如,所有包含陣列索引的鍵都明確表示為字串。陣列中的第一個元素儲存在key下
'0'
。
該'length'
屬性只是碰巧不可列舉和不可配置的另一個屬性。
將元素新增到陣列後,JavaScript會自動更新[[Value]]
屬性的'length'
property屬性。
一般來說,陣列的行為與物件非常相似。
優化屬性訪問
現在我們知道如何在JavaScript中定義物件,讓我們深入研究JavaScript引擎如何有效地使用物件。隨意瀏覽JavaScript程式,訪問屬性是迄今為止最常見的操作。對於JavaScript引擎來說,快速訪問屬性至關重要。
constobject={
foo:'bar',
baz:'qux',
};
//Here,we’reaccessingtheproperty`foo`on`object`:
doSomething(object.foo);
//^^^^^^^^^^
形狀
在JavaScript程式中,通常有多個具有相同屬性鍵的物件。這樣的物體具有相同的
形狀。
constobject1={x:1,y:2};
constobject2={x:3,y:4};
//`object1`and`object2`havethesameshape.
在具有相同形狀的物件上訪問相同屬性也很常見:
functionlogX(object){
console.log(object.x);
//^^^^^^^^
}
constobject1={x:1,y:2};
constobject2={x:3,y:4};
logX(object1);
logX(object2);
考慮到這一點,JavaScript引擎可以根據物件的形狀優化物件屬性訪問。這是這樣的。假設我們有一個具有屬性
x
和的物件
y
,並且使用了我們前面討論的字典資料結構:它包含作為字串的鍵,並且這些鍵指向各自的屬性屬性。
如果您訪問某個屬性,例如
object.y
,JavaScript引擎在中
JSObject
查詢鍵
'y'
,然後載入相應的屬性屬性,最後返回
[[Value]]
。但是這些屬性在哪裡儲存在記憶體中?我們應該將它們儲存為一部分
JSObject
嗎?如果我們假設以後會看到更多具有這種形狀的物件,那麼將包含屬性名稱和屬性的完整字典
JSObject
本身儲存起來是浪費的,因為對具有相同形狀的所有物件重複使用屬性名稱。那是很多重複,不必要地佔用了記憶體。作為優化,引擎分別儲存
Shape
物件的。
其中
Shape
包含所有屬性名稱和屬性,但不包含
[[Value]]
。而是在中
Shape
包含值的偏移量
JSObject
,以便JavaScript引擎知道在何處查詢值。
JSObject
具有相同形狀的每個物件都精確指向此
Shape
例項。現在,每個人
JSObject
只需儲存該物件唯一的值。
當我們有多個物件時,好處顯而易見。不管有多少個物件,只要它們具有相同的形狀,我們只需要儲存一次形狀和屬性資訊即可!
所有JavaScript引擎都使用形狀作為優化,但它們並不全都稱為形狀:
學術論文稱它們為“
隱藏類”
(使wrt JavaScript類混亂)
V8稱它們為
Maps
(混淆了wrt JavaScript
Map
)
Chakra稱它們為Types(混淆了JavaScript的動態型別和typeof
)
JavaScriptCore稱它們為結構
SpiderMonkey稱它們為Shapes
在整個本文中,我們將繼續使用shapes一詞。
過渡鏈和樹木
如果您有一個具有特定形狀的物件,但是隨後向其添加了屬性,會發生什麼情況?JavaScript引擎如何找到新形狀?
constobject={};
object.x=5;
object.y=6;
這些形狀在JavaScript引擎中形成了所謂的過渡鏈。這是一個例子:
該物件開始時沒有任何屬性,因此它指向空的形狀。next語句向該物件添加了一個'x'
帶有值的屬性
5
,因此JavaScript引擎轉換為包含該屬性的形狀,
'x'
並且在第一個offset處將一個值
5
新增到。下一行添加了一個屬性,因此引擎轉換為包含和的另一個形狀,並將該值附加到(偏移處)。
JSObject
0
'y'
'x'
'y'
6
JSObject
1
注意:新增屬性的順序會影響形狀。例如,
{ x: 4, y: 5 }
結果與的形狀不同
{ y: 5, x: 4 }
。我們甚至不需要為每個儲存完整的屬性表
Shape
。相反,每個人
Shape
都只需要瞭解它引入的新屬性。例如,在這種情況下,我們不必以
'x'
最後一個形狀儲存有關的資訊,因為可以在鏈中的較早位置找到它。為了使這項工作有效,每個
Shape
連結都返回其先前的形狀:
如果o.x
您使用JavaScript程式碼編寫程式碼,則JavaScript引擎將'x'
通過遍歷過渡鏈來查詢該Shape
屬性,直到找到該引入的property為止'x'
。
但是,如果沒有辦法建立過渡鏈會怎樣?例如,如果您有兩個空物件,併為每個物件新增一個不同的屬性,該怎麼辦?
constobject1={};
object1.x=5;
constobject2={};
object2.y=6;
在這種情況下,我們必須分支,而不是使用鏈,而最終需要一個過渡樹:
在這裡,我們建立一個空物件a
,然後向其新增屬性'x'
。我們最終得到一個,JSObject
其中包含一個值和兩個值Shapes
:空形狀和僅具有屬性的形狀x
。
第二個示例也從一個空物件開始b
,然後新增一個不同的property'y'
。我們最終得到兩個形狀鏈,總共三個形狀。
這是否意味著我們總是從空的形狀開始?不必要。引擎會對已經包含屬性的物件文字進行一些優化。假設我們要麼x
從空物件常量開始新增,要麼擁有已經包含的物件常量x
:
constobject1={};
object1.x=5;
constobject2={x:6};
在第一個示例中,我們從空的形狀開始過渡到也包含的形狀x
,就像我們之前看到的那樣。
在的情況下object2
,直接生成x
從頭開始已經具有的物件而不是從空物件開始並進行過渡是有意義的。
包含屬性的物件文字'x'
從一個'x'
從頭開始包含的形狀開始,有效地跳過了空形狀。這(至少)是V8和SpiderMonkey所做的。這種優化縮短了轉換鏈,使從文字構造物件的效率更高。
Benedikt關於React應用程式中令人驚訝的多型性的部落格文章討論了這些微妙之處如何影響現實世界的效能。
以下是包含性的3D點物件的一個例子'x'
,'y'
和'z'
。
constpoint={};
point.x=4;
point.y=5;
point.z=6;
如前所述,這將建立一個記憶體中具有3種形狀的物件(不計算空的形狀)。要訪問屬性'x'
該物件上,例如,如果你寫point.x
在你的程式中,JavaScript引擎需要遵循連結串列:它開始在Shape
底部,然後它的工作方式到Shape
介紹了'x'
頂部。
如果我們更頻繁地執行此操作,那將真的很慢,尤其是當物件具有許多屬性時。查詢屬性的時間為O(n)
,即物件上屬性的數量呈線性關係。為了加快搜索屬性,JavaScript引擎添加了ShapeTable
資料結構。這ShapeTable
是一個字典,將屬性鍵對映到Shape
引入給定屬性的各個。
等等,現在我們回到字典查詢…那是我們開始新增Shape
s 之前的樣子!那麼,為什麼我們完全不關心形狀呢?
原因是形狀可以實現另一種稱為“內聯快取”的優化。
內聯快取(IC)
形狀背後的主要動機是嵌入式快取記憶體或積體電路的概念。IC是使JavaScript快速執行的關鍵因素!JavaScript引擎使用IC來儲存有關在何處查詢物件屬性的資訊,以減少昂貴的查詢次數。
這是一個getX
接受物件並x
從中載入屬性的函式:
functiongetX(o){
returno.x;
}
如果我們在JSC中執行此函式,它將生成以下位元組碼:
第一get_by_id
條指令'x'
從第一個引數(arg1
)載入屬性,並將結果儲存到中loc0
。第二條指令返回我們儲存到的內容loc0
。
JSC還將嵌入式快取記憶體嵌入到get_by_id
指令中,該指令由兩個未初始化的插槽組成。
現在,假設我們getX
使用object進行呼叫{ x: 'a' }
。如我們所知,該物件具有帶屬性的形狀,'x'
並Shape
儲存該屬性的偏移量和屬性x
。首次執行該函式時,get_by_id
指令將查詢該屬性,'x'
並發現該值儲存在offset處0
。
嵌入到get_by_id
指令中的IC會儲存找到屬性的形狀和偏移量:
對於後續執行,IC僅需要比較形狀,如果與以前相同,則只需從儲存的偏移中載入值即可。具體來說,如果JavaScript引擎看到的物件具有IC之前記錄的形狀,則它根本不需要接觸屬性資訊,而是可以完全跳過昂貴的屬性資訊查詢。這比每次查詢屬性都要快得多。
有效地儲存陣列
對於陣列,通常儲存作為陣列索引的屬性。這些屬性的值稱為陣列元素。在每個單個數組中儲存每個陣列元素的屬性屬性會浪費記憶體。相反,JavaScript引擎使用以下事實:預設情況下,陣列索引屬性是可寫,可列舉和可配置的,並將陣列元素與其他命名屬性分開儲存。
考慮以下陣列:
constarray=[
'#jsconfeu',
];
引擎儲存陣列長度(1
),並指向,Shape
其中包含偏移量和屬性的'length'
屬性。
這類似於我們之前看到的內容……但是陣列值儲存在哪裡?
每個陣列都有一個單獨的元素後備儲存,其中包含所有陣列索引的屬性值。JavaScript引擎不必為陣列元素儲存任何屬性屬性,因為通常它們都是可寫的,可列舉的和可配置的。
但是,在異常情況下會發生什麼?如果更改陣列元素的屬性,該怎麼辦?
//Pleasedon’teverdothis!
constarray=Object.defineProperty(
[],
'0',
{
value:'Ohnoes!!1',
writable:false,
enumerable:false,
configurable:false,
}
);
上面的程式碼段定義了一個名為'0'
(恰好是陣列索引)的屬性,但將其屬性設定為非預設值。
在這種情況下,JavaScript引擎將整個元素後備儲存表示為字典,該字典將陣列索引對映到屬性屬性。
即使只有一個數組元素具有非預設屬性,整個陣列的後備儲存也會進入這種緩慢而低效的模式。避免Object.defineProperty
在陣列索引上!(我不確定您為什麼還要這樣做。這似乎很奇怪,沒有用。)
順帶一說
我們已經瞭解了JavaScript引擎如何儲存物件和陣列,以及Shapes和IC如何幫助優化它們的通用操作。
基於這些知識,我們確定了一些實用的JavaScript編碼技巧,可以幫助提高效能 始終以相同的方式初始化物件,以使它們最終不會具有不同的形狀 不要弄亂陣列元素的屬性,這樣它們就可以有效地儲存和操作。 文章就分享到這,歡迎關注“ 前端大神之路”