背景javascript
if(window.fetchData&&window.fetchDataSomeKeyLength=== window.fetchData.length) return
const data = fetch(`${url}`
window.fetchData = data
window.fetchDataSomeKeyLength=data.someKey.length
processData(data)//data.someKey的length作了處理
複製代碼
通俗易懂的話來說,js的基本類型使用用來存儲值得,它們分配大小是有限度 在定義基本類型變量的時候它們的內存都被分配完成,前端
string
、number
、boolean
和 symbol
這四種類型統稱爲原始類型(Primitive) ,表示不能再細分下去的基本類型;symbol
表示獨一無二的值,經過 Symbol
函數調用生成,因爲生成的 symbol
值爲原始類型,因此 Symbol
函數不能使用 new
調用;null
和 undefined
一般被認爲是特殊值,這兩種類型的值惟一,就是其自己。java
對象es6
數組web
函數面試
和基本類型區分開來。對象在邏輯上是屬性的無序集合或者有序集合,是存放各類值的容器。對象值存儲的是引用地址,因此和基本類型值不可變的特性不一樣,對象值是可變的。算法
實際上在引用字符串的屬性或方法時,會經過調用 new String()
的方式轉換成對象,該對象繼承了字符串的方法來處理屬性的引用,一旦引用結束,便會銷燬這個臨時對象,這就是包裝對象的概念。編程
不只僅只是字符串有包裝對象的概念,數字和布爾值也有相對應的 new Number()
和 new Boolean()
包裝對象。null
和 undefined
沒有包裝對象,訪問它們的屬性會報類型錯誤。數組
字符串、數字和布爾值經過構造函數顯式生成的包裝對象,既然屬於對象,和基本類型的值必然是有區別的,這點能夠經過 typeof
檢測出來。安全
typeof 'seymoe' // 'string'
typeof new String('seymoe') // 'object'
複製代碼
typeof
instanceof
Object.prototype.toString()
typeof 'seymoe' // 'string'
typeof true // 'boolean'
typeof 10 // 'number'
typeof Symbol() // 'symbol'
typeof null // 'object'
沒法斷定是否爲 null
typeof undefined // 'undefined'
複製代碼
若是使用 typeof
操做符對對象類型及其子類型,譬如函數(可調用對象)、數組(有序索引對象)等進行斷定,則除了函數都會獲得 object
的結果。
typeof {} // 'object'
typeof [] // 'object'
typeof(() => {})// 'function'
複製代碼
因爲沒法得知一個值究竟是數組仍是普通對象,顯然經過 typeof
判斷具體的對象子類型遠遠不夠。
注意:instanceof
也不是萬能的。其原理就是測試構造函數
var a={}
a.__proto__=[]
a instanceof Array //true
a instanceof Object //true
複製代碼
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(() => {}) // '[object Function]'
Object.prototype.toString.call('seymoe') // '[object String]'
Object.prototype.toString.call(1) // '[object Number]'
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call(Symbol()) // '[object Symbol]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(new Date()) // '[object Date]'
Object.prototype.toString.call(Math) // '[object Math]'
Object.prototype.toString.call(new Set()) // '[object Set]'
Object.prototype.toString.call(new WeakSet()) // '[object WeakSet]'
Object.prototype.toString.call(new Map()) // '[object Map]'
Object.prototype.toString.call(new WeakMap()) // '[object WeakMap]'
複製代碼
ToPrimitive 算法在執行時,會被傳遞一個參數 hint
,表示這是一個什麼類型的運算(也能夠叫運算的指望值),根據這個 hint
參數,ToPrimitive 算法來決定內部的執行邏輯。
hint
參數的取值只能是下列 3 者之一:
string
number
default
調用obj[Symbol.toPrimitive](hint)
- 帶有符號鍵Symbol.toPrimitive
(系統符號)的方法,若是存在這樣的方法,
不然若是提示是 "string"
obj.toString()
和obj.valueOf()
,不管存在什麼。不然,若是提示是"number"
或"default"
obj.valueOf()
和obj.toString()
,不管存在什麼。咱們提到了 ToPrimitive 算法中用到的 hint
參數,那怎樣肯定一次運算場景下的 hint
取值是什麼呢?很簡單----新建一個對象,打印各個運算場景下的 hint
值:
let obj = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`);
}
};
alert(obj) // hint: string
+obj // hint: number
obj + 500 // hint: default
// 一個沒有提供 Symbol.toPrimitive 屬性的對象,參與運算時的輸出結果
var obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"
// 接下面聲明一個對象,手動賦予了 Symbol.toPrimitive 屬性,再來查看輸出結果
var obj2 = {
[Symbol.toPrimitive](hint) {
if (hint == "number") {
return 10;
}
if (hint == "string") {
return "hello";
}
return true;
}
};
console.log(+obj2); // 10 -- hint 參數值是 "number"
console.log(`${obj2}`); // "hello" -- hint 參數值是 "string"
console.log(obj2 + ""); // "true" -- hint 參數值是 "default"
複製代碼
但要注意下面兩點:
Symbol.toPrimitive
和 toString
方法的返回值必須是基本類型值。valueOf
方法除了能夠返回基本類型值,也能夠返回其餘類型值。當咱們建立一個普通對象時({}
或 new Object()
的方式等),對象上是不具有 [Symbol.toPrimitive]
(方法)屬性的。因此,對於普通對象的到基本類型值的運算,通常按照具體場景:
hint
值爲 "string"
時,先調用 toString
,toString
若是返回一個基本類型值了,則返回、終止運算;不然接着調用 valueOf
方法。valueOf
,valueOf
若是返回一個基本類型值了,則返回、終止運算;不然接着調用 toString
方法。棧是一種特殊的列表,棧內的元素只能經過列表的一端訪問,這一端稱爲棧頂。 棧被稱爲是一種後入先出(LIFO,last-in-first-out)的數據結構。 因爲棧具備後入先出的特色,因此任何不在棧頂的元素都沒法訪問。 爲了獲得棧底的元素,必須先拿掉上面的元素。
在這裏,爲方便理解,經過類比乒乓球盒子來分析棧的存取方式。
這種乒乓球的存放方式與棧中存取數據的方式一模一樣。 處於盒子中最頂層的乒乓球 5,它必定是最後被放進去,但能夠最早被使用。 而咱們想要使用底層的乒乓球 1,就必須將上面的 4 個乒乓球取出來,讓乒乓球1處於盒子頂層。 這就是棧空間先進後出,後進先出的特色。
基本數據類型保存在棧內存中,由於基本數據類型佔用空間小、大小固定,經過按值來訪問,屬於被頻繁使用的數據。 爲了更好的搞懂基本數據類型變量與棧內存,咱們結合如下例子與圖解進行理解:
let num1 = 1;
let num2 = 1;
複製代碼
引用數據類型存儲在堆內存中,由於引用數據類型佔據空間大、大小不固定。 若是存儲在棧中,將會影響程序運行的性能; 引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。 當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中得到實體
// 基本數據類型-棧內存
let a1 = 0;
// 基本數據類型-棧內存
let a2 = 'this is string';
// 基本數據類型-棧內存
let a3 = null;
// 對象的指針存放在棧內存中,指針指向的對象存放在堆內存中
let b = { m: 20 };
// 數組的指針存放在棧內存中,指針指向的數組存放在堆內存中
let c = [1, 2, 3];
複製代碼
所以當咱們要訪問堆內存中的引用數據類型時,實際上咱們首先是從變量中獲取了該對象的地址指針, 而後再從堆內存中取得咱們須要的數據。
let a = 20;
let b = a;
b = 30;
console.log(a); // 此時a的值是50
複製代碼
在這個例子中,a、b 都是基本類型,它們的值是存儲在棧內存中的,a、b 分別有各自獨立的棧空間, 因此修改了 b 的值之後,a 的值並不會發生變化。
let m = { a: 10, b: 20 };
let n = m;
n.a = 15;
console.log(m.a) //此時m.a的值是多少,是10?仍是15?
複製代碼
在這個例子中,m、n都是引用類型,棧內存中存放地址指向堆內存中的對象, 引用類型的複製會爲新的變量自動分配一個新的值保存在變量中, 但只是引用類型的一個地址指針而已,實際指向的是同一個對象, 因此修改 n.a 的值後,相應的 m.a 也就發生了改變。
在JS中,基本數據類型變量大小固定,而且操做簡單容易,因此把它們放入棧中存儲。 引用類型變量大小不固定,因此把它們分配給堆中,讓他們申請空間的時候本身肯定大小,這樣把它們分開存儲可以使得程序運行起來佔用的內存最小。
棧內存因爲它的特色,因此它的系統效率較高。 堆內存須要分配空間和地址,還要把地址存到棧中,因此效率低於棧。
在C語言和C++語言中,咱們若是想要開闢一塊堆內存的話,須要先計算須要內存的大小,而後本身經過malloc函數去手動分配,在用完以後,還要時刻記得用free函數去清理釋放,不然這塊內存就會被永久佔用,形成內存泄露。
可是咱們在寫JavaScript的時候,卻沒有這個過程,由於人家已經替咱們封裝好了,V8引擎會根據你當前定義對象的大小去自動申請分配內存。
不須要咱們去手動管理內存了,因此天然要有垃圾回收,不然的話只分配不回收,豈不是沒多長時間內存就被佔滿了嗎,致使應用崩潰。
垃圾回收的好處是不須要咱們去管理內存,把更多的精力放在實現複雜應用上,但壞處也來自於此,不用管理了,就有可能在寫代碼的時候不注意,形成循環引用等狀況,致使內存泄露。
當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲「進入環境」。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記爲「離開環境」。
可使用任何方式來標記變量。好比,能夠經過翻轉某個特殊的位來記錄一個變量什麼時候進入環境,或者使用一個「進入環境的」變量列表及一個「離開環境的」變量列表來跟蹤哪一個變量發生了變化。如何標記變量並不重要,關鍵在於採起什麼策略。
目前,IE、Firefox、Opera、Chrome和Safari的JavaScript實現使用的都是標記清除式的垃圾回收策略(或相似的策略),只不過垃圾收集的時間間隔互有不一樣。
若是同一個值又被賦給另外一個變量,則該值的引用次數加1。相反,若是包含對這個值引用的變量改變了引用對象,則該值引用次數減1。
當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。
這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲0的值所佔用的內存。
循環引用是指對象A中包含一個指向對象B的指針,而對象B中也包含一個指向對象A的引用,看個例子:
複製代碼
function foo () {
var objA = new Object();
var objB = new Object();
objA.otherObj = objB;
objB.anotherObj = objA;
}
複製代碼
這個例子中,objA和objB經過各自的屬性相互引用,也就是說,這兩個對象的引用次數都是2。
在採用標記清除策略的實現中,因爲函數執行後,這兩個對象都離開了做用域,所以這種相互引用不是問題。
但在採用引用次數策略的實現中,當函數執行完畢後,objA和objB還將繼續存在,由於它們的引用次數永遠不會是0。加入這個函數被重複屢次調用,就會致使大量內存沒法回收
還要注意的是,咱們大部分人時刻都在寫着循環引用的代碼,看下面這個例子,相信你們都這樣寫過:
var el = document.getElementById('#el');
el.onclick = function (event) {
console.log('element was clicked');
}
複製代碼
咱們爲一個元素的點擊事件綁定了一個匿名函數,咱們經過event參數是能夠拿到相應元素el的信息的。
你們想一想,這是否是就是一個循環引用呢? el有一個屬性onclick引用了一個函數(其實也是個對象),函數裏面的參數又引用了el,這樣el的引用次數一直是2,即便當前這個頁面關閉了,也沒法進行垃圾回收。
若是這樣的寫法不少不少,就會形成內存泄露。咱們能夠經過在頁面卸載時清除事件引用,這樣就能夠被回收了
var el = document.getElementById('#el');
el.onclick = function (event) {
console.log('element was clicked');
}
// ...
// ...
// 頁面卸載時將綁定的事件清空
window.onbeforeunload = function(){
el.onclick = null;
}
複製代碼
因此,V8採用了一種代回收的策略,將內存分爲兩個生代:新生代(new generation)和老生代(old generation) 。
新生代中的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象,分別對新老生代採用不一樣的垃圾回收算法來提升效率,對象最開始都會先被分配到新生代(若是新生代內存空間不夠,直接分配到老生代),新生代中的對象會在知足某些條件後,被移動到老生代,這個過程也叫晉升,後面我會詳細說明。
默認狀況下,32位系統新生代內存大小爲16MB,老生代內存大小爲700MB,64位系統下,新生代內存大小爲32MB,老生代內存大小爲1.4GB。
新生代平均分紅兩塊相等的內存空間,叫作semispace,每塊內存大小8MB(32位)或16MB(64位)。
新生代存的都是生存週期短的對象,分配內存也很容易,只保存一個指向內存空間的指針,根據分配對象的大小遞增指針就能夠了,當存儲空間快要滿時,就進行一次垃圾回收。
新生代採用Scavenge垃圾回收算法,在算法實現時主要採用Cheney算法。 Cheney算法將內存一分爲二,叫作semispace,一塊處於使用狀態,一塊處於閒置狀態。
處於使用狀態的semispace稱爲From空間,處於閒置狀態的semispace稱爲To空間。
接下來我會結合流程圖來詳細說明Cheney算法是怎麼工做的。 垃圾回收在下面我統稱爲 GC(Garbage Collection) 。 step1. 在From空間中分配了3個對象A、B、C
step2. GC進來判斷對象B沒有其餘引用,能夠回收,對象A和C依然爲活躍對象
step3. 將活躍對象A、C從From空間複製到To空間
step4. 清空From空間的所有內存
step5. 交換From空間和To空間
step6. 在From空間中又新增了2個對象D、E
step7. 下一輪GC進來發現對象D沒有引用了,作標記
step8. 將活躍對象A、C、E從From空間複製到To空間
step9. 清空From空間所有內存
step10. 繼續交換From空間和To空間,開始下一輪
經過上面的流程圖,咱們能夠很清楚的看到,進行From和To交換,就是爲了讓活躍對象始終保持在一塊semispace中,另外一塊semispace始終保持空閒的狀態。
Scavenge因爲只複製存活的對象,而且對於生命週期短的場景存活對象只佔少部分,因此它在時間效率上有優異的體現。Scavenge的缺點是隻能使用堆內存的一半,這是由劃分空間和複製機制所決定的。
因爲Scavenge是典型的犧牲空間換取時間的算法,因此沒法大規模的應用到全部的垃圾回收中。但咱們能夠看到,Scavenge很是適合應用在新生代中,由於新生代中對象的生命週期較短,偏偏適合這個算法。
對象重新生代移動到老生代的過程叫做晉升。
對象晉升的條件主要有兩個:
因此,V8在老生代中主要採用了Mark-Sweep和Mark-Sweep相結合的方式進行垃圾回收。
與Scavenge不一樣,Mark-Sweep並不會將內存分爲兩份,因此不存在浪費一半空間的行爲。Mark-Sweep在標記階段遍歷堆內存中的全部對象,並標記活着的對象,在隨後的清除階段,只清除沒有被標記的對象。
也就是說,Scavenge只複製活着的對象,而Mark-Sweep只清除死了的對象。活對象在新生代中只佔較少部分,死對象在老生代中只佔較少部分,這就是兩種回收方式都能高效處理的緣由。
step1. 老生代中有對象A、B、C、D、E、F
step2. GC進入標記階段,將A、C、E標記爲存活對象
step3. GC進入清除階段,回收掉死亡的B、D、F對象所佔用的內存空間
能夠看到,Mark-Sweep最大的問題就是,在進行一次清除回收之後,內存空間會出現不連續的狀態。這種內存碎片會對後續的內存分配形成問題。
若是出現須要分配一個大內存的狀況,因爲剩餘的碎片空間不足以完成這次分配,就會提早觸發垃圾回收,而此次回收是沒必要要的。
**Mark-Compact是標記整理的意思,**是在Mark-Sweep的基礎上演變而來的。Mark-Compact在標記完存活對象之後,會將活着的對象向內存空間的一端移動,移動完成後,直接清理掉邊界外的全部內存。以下圖所示: step1. 老生代中有對象A、B、C、D、E、F(和Mark—Sweep同樣)
step2. GC進入標記階段,將A、C、E標記爲存活對象(和Mark—Sweep同樣)
step3. GC進入整理階段,將全部存活對象向內存空間的一側移動,灰色部分爲移動後空出來的空間
step4. GC進入清除階段,將邊界另外一側的內存一次性所有回收
在V8的回收策略中,Mark-Sweep和Mark-Conpact二者是結合使用的。
因爲Mark-Conpact須要移動對象,因此它的執行速度不可能很快,在取捨上,V8主要使用Mark-Sweep,在空間不足以對重新生代中晉升過來的對象進行分配時,才使用Mark-Compact。
V8的垃圾回收機制分爲新生代和老生代。
新生代主要使用Scavenge進行管理,主要實現是Cheney算法,將內存平均分爲兩塊,使用空間叫From,閒置空間叫To,新對象都先分配到From空間中,在空間快要佔滿時將存活對象複製到To空間中,而後清空From的內存空間,此時,調換From空間和To空間,繼續進行內存分配,當知足那兩個條件時對象會重新生代晉升到老生代。
老生代主要採用Mark-Sweep和Mark-Compact算法,一個是標記清除,一個是標記整理。二者不一樣的地方是,Mark-Sweep在垃圾回收後會產生碎片內存,而Mark-Compact在清除前會進行一步整理,將存活對象向一側移動,隨後清空邊界的另外一側內存,這樣空閒的內存都是連續的,可是帶來的問題就是速度會慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact二者共同進行管理的。
以上就是本文的所有內容,書寫過程當中參考了不少中外文章,參考書籍包括樸大大的《深刻淺出NodeJS》以及《JavaScript高級程序設計》等。咱們這裏並無對具體的算法實現進行探討,感興趣的朋友能夠繼續深刻研究一下。
最後,謝謝你們可以讀到這裏,若是文中有任何不明確或錯誤的地方,歡迎給我留言~~
標識符解析(變量查找),是按照做用域鏈一級一級的操做,查找順序是從當前變量對象開始,知道找到爲止,若是找不到就會一般會有異常
var color = "blue";
function changeColor() {
var otherColor = "red";
function swapColor() {
var tempColor = otherColor;
otherColor = color;
color = tempColor;
// 這裏能夠訪問 tempColor otherColor color
}
swapColor();
// 這裏能夠訪問 otherColor color swapColor
}
changeColor();
// 這裏能夠訪問 changeColor color
複製代碼
看圖
下面代碼中你們要理解函數的多面性,多個身份
接下來說用到函數的是兩個身份普通函數、普通對象, 看代碼()
function foo(){
this.count++
}
var count=0;
foo.count=0;
for(var i=0;i<5;i++){
foo()
}
console.log(foo.count)//0
console.log(count)//5
複製代碼
從打印的結果上來看顯然,this指向的不是自己函數,固然我們通常看到這類的問題我們就會繞道而行,看代碼
function foo(){
this.count++
}
var bar={
count:0
}
foo.count=0;
for(var i=0;i<5;i++){
foo.call(bar)
}
console.log(bar.count)//5
console.log(count)//0
複製代碼
雖然這種解決方案很好,也會有其餘的解決方案,可是咱們仍是不理解this的問題,內心仍是有種不安之感
接下來說用到函數的是兩個身份普通函數、普通對象, 看代碼()
function foo(){
var num=2;
console.log(this.num)
}
var num=0;
foo()//0
複製代碼
我們看到代碼的執行結果後,發現this指向的並非該函數的做用域。
圖中我們看到this是在函數執行的時候建立的。
前面幾步我們已經肯定的this的建立和this的指向的誤區,接下啦我們要看看this的綁定的規則,分爲4個規則。
function foo(){
var num=2;
this.num++
console.log(this.num)
}
var num=0;
foo()//1
複製代碼
上面代碼中就實現了默認綁定,在foo方法的代碼塊中操做的是window.num++。
function foo(){
console.log(this.name)
}
var bar={
name:'shiny',
foo:foo
}
bar.foo()//shiny
複製代碼
要須要補充一點,無論你的對象嵌套多深,this只會綁定爲直接引用該函數的地址屬性的對象,看代碼
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
var red={
name:'red',
obj:shiny
}
red.obj.foo()//shiny
複製代碼
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
function doFoo(fn){
fn()
}
doFoo(shiny.foo)//undefind
複製代碼
你們知道函數參數在函數執行的時候,其實有一個賦值的操做,我來解釋一下上面的,當函數doFoo執行的時候會開闢一個新的棧並被推入到全局棧中執行,在執行的過程當中會建立一個活動對象,這個活動對象會被賦值傳入的參數以及在函數中定義的變量函數,在函數執行時用到的變量和函數直接從該活動對象上面取值使用。 看圖 doFoo的執行棧
fn的執行棧
看下面原理和上面同樣經過賦值,致使隱式綁定的丟失,看代碼
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
var bar = shiny.foo
bar()//undefined
複製代碼
你們是否是已經明白了爲何是undefined,來解釋一波,其實shiny的foo屬性是引用了foo函數的引用內存地址,那麼有把foo的引用地址賦值給了 bar 那麼如今的bar的引用地址個shiny.foo的引用地址是一個,那麼執行bar的時候也會觸發默認綁定規則由於沒有其餘規則能夠匹配,bar函數執行時,函數內部的this綁定的是全局變量。
看下滿的引用地址賦值是出現的,奇葩 隱式綁定丟失,看代碼
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
var red={
name:'red'
}
(red.foo=shiny.foo)()//undefined
複製代碼
賦值表達式 p.foo = o.foo 的返回值是目標函數的引用,所以調用位置是 foo() 而不是 p.foo() 或者 o.foo()。根據咱們以前說過的,這裏會應用默認綁定。
看代碼
function foo(){
console.log(this.age)
}
var shiny={
age:20
}
foo.call(shiny)//20
function bar(){
console.log(this.age)
}
var red={
age:18
}
bar.apply(red)//18
複製代碼
這兩個方法都是顯式的綁定了tihs
function foo(b){
return this.a+b
}
var obj={
a:2
}
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments)
}
}
bind(foo,obj)(3)//5
複製代碼
語言解釋: 經過apply + 閉包機制 實現bind方法,實現強行綁定規則
API調用的「上下文」 第三方庫或者寄生在環境,以及js內置的一些方法都提供了一下 content 上下文參數,他的做用和 bind同樣,就是確保回調函數的this被綁定
function foo (el){
console.log(el,this.id)
}
var obj ={
id:'some one'
};
[1,2,4].forEach(foo,obj)
// 1 some one 2 some one 4 some one
複製代碼
傳統面向類的語言中的構函數,是在使用new操做符實例化類的時候,會調用類中的一些特殊方法(構造函數)
不少人認爲js中的new操做符和傳統面向類語言的構造函數是同樣的,其實有很大的差異
重新認識一下js中的構造函數,js中的構造函數 在被new操做符調用時,這個構造函數不屬於每一個類,也不會創造一個類,它就是一個函數,只是被new操做符調用。
使用new操做符調用 構造函數時會執行4步
我們瞭解了js new 操做符調用構造函數時都作了些什麼,哪麼我們就知道構造函數裏面的this是誰了
代碼實現
function Foo(a){
this.a=a
}
var F = new Foo(2)
console.log(F.a)//2
複製代碼
看代碼
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
var red={
name:'red'
}
shiny.foo()//shiny
shiny.foo.call(red)// red
shiny.foo.apply(red)// red
shiny.foo.bind(red)()//red
複製代碼
顯然在這場綁定this比賽中,顯式綁定贏了隱式綁定
function foo(name){
this.name=name
}
var shiny={
foo:foo
}
shiny.foo('shiny')
console.log(shiny.name)//shiny
var red = new shiny.foo('red')
console.log(red.name)//red
複製代碼
顯然在這場綁定this比賽中new 操做符綁定贏了隱式綁定
使用call、apply方法不能結合new操做符會報錯誤
可是我們能夠是bind綁定this來比較 顯式綁定和new操做符的綁定this優先級。 看代碼
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny'
}
var bar = foo.bind(shiny)
var obj = new bar();
console.log(obj.name)// undefind
複製代碼
顯然 new操做符綁定 打敗了 顯式綁定
foo.call(window)
obj.foo();
function foo(){
console.log(name)
}
var name ='shiny'
foo.call(null)//shiny
foo.call(undefined)//shiny
var bar = foo.bind(null)
var baz = foo.bind(undefined)
bar()//siny
baz()//siny
複製代碼
把 null、undefined經過 apply、call、bind 顯式綁定,雖然實現可默認綁定,可是建議這麼作由於在非嚴格的模式下會給全局對象添加屬性,有時候會形成不可必要的bug。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 咱們的空對象
var ø = Object.create( null );
// 把數組展開成參數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
複製代碼
function foo(){
return ()=>{
console.log(this.name)
}
}
var obj ={
name:'obj'
}
var shiny ={
name:'shiny'
}
var bar = foo.call(obj);
bar.call(shiny)// foo
複製代碼
咱們看到箭頭函數的this被綁定到該函數執行的做用域上。
我們在看看 js內部提供內置函數使用箭頭函數
function foo() {
setTimeout(() => {
// 這裏的 this 在此法上繼承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
複製代碼
箭頭函數能夠像 bind(..) 同樣確保函數的 this 被綁定到指定對象,此外,其重要性還體 如今它用更常見的詞法做用域取代了傳統的 this 機制。實際上,在 ES6 以前咱們就已經 在使用一種幾乎和箭頭函數徹底同樣的模式。
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
複製代碼
雖然 self = this 和箭頭函數看起來均可以取代 bind(..),可是從本質上來講,它們想替 代的是 this 機制。 若是你常常編寫 this 風格的代碼,可是絕大部分時候都會使用 self = this 或者箭頭函數。 若是徹底採用 this 風格,在必要時使用 bind(..),儘可能避免使用 self = this 和箭頭函數。
在函數執行過程當中,爲讀取和寫入變量的值,就須要在做用域鏈中查找變量。來看下面的例子。
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
複製代碼
以上代碼先定義了 compare()函數,而後又在全局做用域中調用了它。當調用 compare()時,會 建立一個包含 arguments、value1 和 value2 的活動對象。全局執行環境的變量對象(包含 result 和 compare)在 compare()執行環境的做用域鏈中則處於第二位。圖片 展現了包含上述關係的 compare()函數執行時的做用域鏈。
後臺的每一個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像 compare()函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。在建立 compare()函數 時,會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的[[Scope]]屬性中。 當調用 compare()函數時,會爲函數建立一個執行環境,而後經過複製函數的[[Scope]]屬性中的對 象構建起執行環境的做用域鏈。此後,又有一個活動對象(在此做爲變量對象使用)被建立並被推入執 行環境做用域鏈的前端。對於這個例子中 compare()函數的執行環境而言,其做用域鏈中包含兩個變 量對象:本地活動對象和全局變量對象。顯然,做用域鏈本質上是一個指向變量對象的指針列表,它只 引用但不實際包含變量對象。 不管何時在函數中訪問一個變量時,就會從做用域鏈中搜索具備相應名字的變量。通常來說, 當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域(全局執行環境的變量對象)。 可是,閉包的狀況又有所不一樣。
在看一個案例
function createComparisonFunction(propertyName) {
return function (object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
複製代碼
在這個例子中,object1[propertyName] object2[propertyName] 兩行代碼是內部函數(一個匿名函數)中的代碼,這兩行代碼訪問了外部 函數中的變量 propertyName。即便這個內部函數被返回了,並且是在其餘地方被調用了,但它仍然可 以訪問變量 propertyName。之因此還可以訪問這個變量,是由於內部函數的做用域鏈中包含 createComparisonFunction()的做用域。要完全搞清楚其中的細節,必須從理解函數被調用的時候 都會發生什麼入手。
當某個函數被調用時,會建立一個執行環境(execution context)及相應的做用域鏈。 而後,使用 arguments 和其餘命名參數的值來初始化函數的活動對象(activation object)。但在做用域 鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,……直至做爲做用域鏈終點的全局執行環境
看圖
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i;
};
}
return result;
}
複製代碼
這個函數會返回一個函數數組。表面上看,彷佛每一個函數都應該返本身的索引值,即位置 0 的函數 返回 0,位置 1 的函數返回 1,以此類推。但實際上,每一個函數都返回 10。由於每一個函數的做用域鏈中 都保存着 createFunctions() 函數的活動對象,因此它們引用的都是同一個變量 i 。 當 createFunctions()函數返回後,變量 i 的值是 10,此時每一個函數都引用着保存變量 i 的同一個變量 對象,因此在每一個函數內部 i 的值都是 10。可是,咱們能夠經過建立另外一個匿名函數強制讓閉包的行爲 符合預期,以下所示。
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = (function (num) {
return function () {
return num;
};
})(i);
}
return result;
}
複製代碼
在重寫了前面的 createFunctions()函數後,每一個函數就會返回各自不一樣的索引值了。在這個版 本中,咱們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將當即執行該匿名函數的結果賦 給數組。這裏的匿名函數有一個參數 num,也就是最終的函數要返回的值。在調用每一個匿名函數時,我 們傳入了變量 i。因爲函數參數是按值傳遞的,因此就會將變量 i 的當前值複製給參數 num。而在這個 匿名函數內部,又建立並返回了一個訪問 num 的閉包。這樣一來,result 數組中的每一個函數都有本身 num 變量的一個副本,所以就能夠返回各自不一樣的數值了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name;
};
},
};
alert(object.getNameFunc()()); //"The Window"
複製代碼
每一個函數在被調用時都會自動取得兩個特殊變量:this 和 arguments。內部函 數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變 量。不過,把外部做用域中的 this 對象保存在一個閉包可以訪問 到的變量裏,就可讓閉包訪問該對象了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name;
};
},
};
alert(object.getNameFunc()()); //"My Object"
複製代碼
在幾種特殊狀況下,this 的值可能會意外地改變。好比,下面的代碼是修改前面例子的結果。
var name = "The Window";
var object = {
name: "My Object",
getName: function () {
return this.name;
},
};
複製代碼
第一行代碼跟日常同樣調用了 object.getName(),返回的是"My Object",由於 this.name 就是 object.name。第二行代碼在調用這個方法前先給它加上了括號。雖然加上括號以後,就好像只 是在引用一個函數,但 this 的值獲得了維持,由於 object.getName 和(object.getName)的定義 是相同的。第三行代碼先執行了一條賦值語句,而後再調用賦值後的結果。由於這個賦值表達式的值是 函數自己,因此 this 的值不能獲得維持,結果就返回了"The Window"。 固然,你不大可能會像第二行和第三行代碼同樣調用這個方法。不過,這個例子有助於說明即便是 語法的細微變化,都有可能意外改變 this 的值。
淺拷貝:
建立一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。若是屬性是基本類型,拷貝的就是基本類型的值,若是屬性是引用類型,拷貝的就是內存地址 ,因此若是其中一個對象改變了這個地址,就會影響到另外一個對象。
深拷貝:
將一個對象從內存中完整的拷貝一份出來,從堆內存中開闢一個新的區域存放新對象,且修改新對象不會影響原對象
JSON.parse(JSON.stringify());
複製代碼
這種寫法很是簡單,並且能夠應對大部分的應用場景,可是它仍是有很大缺陷的,好比拷貝其餘引用類型、拷貝函數、循環引用等狀況。
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
複製代碼
這是一個最基礎版本的深拷貝,這段代碼可讓你向面試官展現你能夠用遞歸解決問題,可是顯然,他還有很是多的缺陷,好比,尚未考慮數組。
在上面的版本中,咱們的初始化結果只考慮了普通的object
,下面咱們只須要把初始化代碼稍微一變,就能夠兼容數組了:
module.exports = function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
複製代碼
OK,沒有問題,你的代碼又向合格邁進了一小步。
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
target.target = target;
複製代碼
很明顯,由於遞歸進入死循環致使棧內存溢出了。
緣由就是上面的對象存在循環引用的狀況,即對象的屬性間接或直接的引用了自身的狀況:
解決循環引用問題,咱們能夠額外開闢一個存儲空間,來存儲當前對象和拷貝對象的對應關係,當須要拷貝當前對象時,先去存儲空間中找,有沒有拷貝過這個對象,若是有的話直接返回,若是沒有的話繼續拷貝,這樣就巧妙化解的循環引用的問題。
這個存儲空間,須要能夠存儲key-value
形式的數據,且key
能夠是一個引用類型,咱們能夠選擇Map
這種數據結構:
map
中有無克隆過的對象key
,克隆對象做爲value
進行存儲function clone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
};
複製代碼
接下來,咱們可使用,WeakMap
提代Map
來使代碼達到畫龍點睛的做用。
function clone(target, map = new WeakMap()) {
// ...
};
複製代碼
爲何要這樣作呢?,先來看看WeakMap
的做用:
WeakMap 對象是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是對象,而值能夠是任意的。
什麼是弱引用呢?
在計算機程序設計中,弱引用與強引用相對,是指不能確保其引用的對象不會被垃圾回收器回收的引用。 一個對象若只被弱引用所引用,則被認爲是不可訪問(或弱可訪問)的,並所以可能在任什麼時候刻被回收。
舉個例子:
let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code祕密花園');
obj = null;
複製代碼
雖然咱們手動將obj
,進行釋放,然是target
依然對obj
存在強引用關係,因此這部份內存依然沒法被釋放。
再來看WeakMap
:
let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code祕密花園');
obj = null;
複製代碼
若是是WeakMap
的話,target
和obj
存在的就是弱引用關係,當下一次垃圾回收機制執行時,這塊內存就會被釋放掉。
設想一下,若是咱們要拷貝的對象很是龐大時,使用Map
會對內存形成很是大的額外消耗,並且咱們須要手動清除Map
的屬性才能釋放這塊內存,而WeakMap
會幫咱們巧妙化解這個問題。