擊上方藍字關注“小鄭搞碼事”,每天都能學到知識,搞懂一個問題!
這里,我們要解決的一個問題是,如何用最簡單的方法處理JavaScript堆棧溢出的問題,所以,我們得搞一段代碼,能讓它產生堆棧溢出。如下
這是一個數組,長度為100000。其中倒數第二位的值設成了'小鄭搞碼事'(為了下面代碼演示直觀一點),其它位都是按數字順序輸出。
這段代碼很容易看懂,主要利用了一個遞歸的方法,里面用了一個數組的pop方法,輸出數據中所有的值。我們運行一下之后,就產生了堆棧溢出,如下結果:
原因就是每次執行代碼時,都會分配一定尺寸的棧空間,每次方法調用時都會在棧里 儲存一定信息(如參數,局部變量,返回值等)。這些信息再少也會占用一定空間,累積下來,自然就超過線程的棧空間了。
保留這個遞歸,消除堆棧溢出,最簡單的方法就是改成事件循環來處理,代碼很簡單:
將nextItem函數直接運行改成加一個定時器。當item不為null,則將函數nextItem推送到事件隊列,并且函數退出,從而使調用堆棧清零,看一下輸出結果:
沒有問題。
總結一下:
JavaScript中解決堆棧溢出,最簡單的方法是將堆棧調用改成事件循環來處理。當然還有一些其它的方法,如閉包,或者優化一下現有的代碼等。
品|開源中國
近日,Mozilla 檢測到 Firefox 出現了大量崩潰事件,這個問題主要發生在使用 Linux 系統的用戶身上,尤其是使用基于舊版本 Debian 的 Linux 系統上。
經過調查,Mozilla 最后發現這個問題并非由 Firefox 本身引起,而是涉及到 Linux 內核和 Google 的 JavaScript 代碼。
這些崩潰事件發生的十分突然,一開始 Mozilla 檢測到數以千計使用一個名為 Huayra 的 Debian 發行版的用戶受到影響,特別是 Huayra 5(基于 Debian 10)。后續的持續檢測發現,這個問題影響了幾乎所有基于舊版本 Debian 的發行版。
崩潰事件也并不是隨機發生,而是能夠 100% 復現。用戶只要在 Google 上搜索圖片 Firefox 就會出現崩潰,這個問題影響了所有版本的 Firefox 瀏覽器 —— 無論是最新版本還是非常老的版本,都會崩潰。
結合上面兩個條件,Mozilla 的研究人員認定這個問題不是由 Firefox 這邊引發的,問題應該是出在 Google 和舊版 Linux 那邊,并開始分析這個問題發生的原因。
Mozilla 隨后開始分析 Firefox 在崩潰時的行為,發現崩潰發生在堆棧探測期間。JIT 觸及了為下一個 JavaScript 調用保存變量的區域,并不知為何導致了溢出。
第一個奇怪的地方在于,Mozilla 發現 Google 最近對其圖像搜索頁面進行了更改,該頁面現在有一個 JS 函數,Google 在這個單獨的函數中分配了 20000 個變量。進一步分析發現,這個函數可能是由 AI 生成的代碼。
雖然發現了有這樣的問題,不過理論上 Firefox 應該依然不會出現崩潰才對,因為 Linux 會自動擴展堆棧,Mozilla 團隊也已經預留了足夠的空間,隨后他們通過查看受影響進程的內存確認了這一點。
在執行此操作之前,我們進行了堆棧檢查并驗證了我們分配的額外堆棧內存量不會溢出我們為自己設置的本機堆棧限制。因此,似乎存在我們自我施加的限制與操作系統限制之間的分歧。這在某種程度上取決于發行版,但很混亂:例如,它影響 Debian 10 但不影響 Debian 11。
隨后 Mozilla 團隊將檢查重點放在了 Linux 內核上,結果發現 Linux 內核曾經有一個檢查,可以防止對堆棧的訪問離堆棧指針太遠。特別是在 64KiB+256 字節以外的訪問會產生崩潰,而不是擴展堆棧。這個問題在 Linux 4.20 中被修復了,所以使用較新的發行版的用戶不受影響。
根據測試,Google 似乎已經在圖片搜索中修復了這個問題,不過 Mozilla 仍然在著手研究解決方案,看看是否能為還在使用老系統的用戶一勞永逸解決這個問題,以免未來發生同樣的情況。
(1)基本類型:
5種基本數據類型Undefined、Null、Boolean、Number 和 String,變量是直接按值存放的,存放在棧內存中的簡單數據段,可以直接訪問。
(2)引用類型:
存放在堆內存中的對象,變量保存的是一個指針,這個指針指向另一個位置。當需要訪問引用類型(如對象,數組等)的值時,首先從棧中獲得該對象的地址指針,然后再從堆內存中取得所需的數據。
JavaScript存儲對象都是存地址的,所以淺拷貝會導致 obj1 和obj2 指向同一塊內存地址。改變了其中一方的內容,都是在原來的內存上做修改會導致拷貝對象和源對象都發生改變,而深拷貝是開辟一塊新的內存地址,將原對象的各個屬性逐個復制進去。對拷貝對象和源對象各自的操作互不影響。
例如:數組拷貝
//淺拷貝,雙向改變,指向同一片內存空間 var arr1 = [1, 2, 3]; var arr2 = arr1; arr1[0] = 'change'; console.log('shallow copy: ' + arr1 + " ); //shallow copy: change,2,3 console.log('shallow copy: ' + arr2 + " ); //shallow copy: change,2,3
2.1、簡單的引用復制###
function shallowClone(copyObj) { var obj = {}; for ( var i in copyObj) { obj[i] = copyObj[i]; } return obj; } var x = { a: 1, b: { f: { g: 1 } }, c: [ 1, 2, 3 ] }; var y = shallowClone(x); console.log(y.b.f === x.b.f); // true
2.2、Object.assign()
Object.assign() 方法可以把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,然后返回目標對象。
var x = { a: 1, b: { f: { g: 1 } }, c: [ 1, 2, 3 ] }; var y = Object.assign({}, x); console.log(y.b.f === x.b.f); // true
3.1、Array的slice和concat方法
Array的slice和concat方法不修改原數組,只會返回一個淺復制了原數組中的元素的一個新數組。之所以把它放在深拷貝里,是因為它看起來像是深拷貝。而實際上它是淺拷貝。原數組的元素會按照下述規則拷貝:
如果向兩個數組任一中添加了新元素,則另一個不會受到影響。例子如下:
var array = [1,2,3]; var array_shallow = array; var array_concat = array.concat(); var array_slice = array.slice(0); console.log(array === array_shallow); //true console.log(array === array_slice); //false,“看起來”像深拷貝 console.log(array === array_concat); //false,“看起來”像深拷貝
可以看出,concat和slice返回的不同的數組實例,這與直接的引用復制是不同的。而從另一個例子可以看出Array的concat和slice并不是真正的深復制,數組中的對象元素(Object,Array等)只是復制了引用。如下:
var array = [1, [1,2,3], {name:"array"}]; var array_concat = array.concat(); var array_slice = array.slice(0); array_concat[1][0] = 5; //改變array_concat中數組元素的值 console.log(array[1]); //[5,2,3] console.log(array_slice[1]); //[5,2,3] array_slice[2].name = "array_slice"; //改變array_slice中對象元素的值 console.log(array[2].name); //array_slice console.log(array_concat[2].name); //array_slice
3.2、JSON對象的parse和stringify
JSON對象是ES5中引入的新的類型(支持的瀏覽器為IE8+),JSON對象parse方法可以將JSON字符串反序列化成JS對象,stringify方法可以將JS對象序列化成JSON字符串,借助這兩個方法,也可以實現對象的深拷貝。
//例1 var source = { name:"source", child:{ name:"child" } } var target = JSON.parse(JSON.stringify(source)); target.name = "target"; //改變target的name屬性 console.log(source.name); //source console.log(target.name); //target target.child.name = "target child"; //改變target的child console.log(source.child.name); //child console.log(target.child.name); //target child //例2 var source = { name:function(){console.log(1);}, child:{ name:"child" } } var target = JSON.parse(JSON.stringify(source)); console.log(target.name); //undefined //例3 var source = { name:function(){console.log(1);}, child:new RegExp("e") } var target = JSON.parse(JSON.stringify(source)); console.log(target.name); //undefined console.log(target.child); //Object {}
這種方法使用較為簡單,可以滿足基本的深拷貝需求,而且能夠處理JSON格式能表示的所有數據類型,但是對于正則表達式類型、函數類型等無法進行深拷貝(而且會直接丟失相應的值)。還有一點不好的地方是它會拋棄對象的constructor。也就是深拷貝之后,不管這個對象原來的構造函數是什么,在深拷貝之后都會變成Object。同時如果對象中存在循環引用的情況也無法正確處理。
4、jQuery.extend()方法源碼實現
jQuery的源碼 - src/core.js #L121源碼及分析如下:
jQuery.extend = jQuery.fn.extend = function() { //給jQuery對象和jQuery原型對象都添加了extend擴展方法 var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; //以上其中的變量:options是一個緩存變量,用來緩存arguments[i],name是用來接收將要被擴展對象的key,src改變之前target對象上每個key對應的value。 //copy傳入對象上每個key對應的value,copyIsArray判定copy是否為一個數組,clone深拷貝中用來臨時存對象或數組的src。 // 處理深拷貝的情況 if (typeof target === "boolean") { deep = target; target = arguments[1] || {}; //跳過布爾值和目標 i++; } // 控制當target不是object或者function的情況 if (typeof target !== "object" && !jQuery.isFunction(target)) { target = {}; } // 當參數列表長度等于i的時候,擴展jQuery對象自身。 if (length === i) { target = this; --i; } for (; i < length; i++) { if ((options = arguments[i]) != null) { // 擴展基礎對象 for (name in options) { src = target[name]; copy = options[name]; // 防止永無止境的循環,這里舉個例子, // 如 var a = {name : b}; // var b = {name : a} // var c = $.extend(a, b); // console.log(c); // 如果沒有這個判斷變成可以無限展開的對象 // 加上這句判斷結果是 {name: undefined} if (target === copy) { continue; } if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) { if (copyIsArray) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是數組的話就讓clone副本等于src否則等于空數組。 } else { clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是對象的話就讓clone副本等于src否則等于空數組。 } // 遞歸拷貝 target[name] = jQuery.extend(deep, clone, copy); } else if (copy !== undefined) { target[name] = copy; // 若原對象存在name屬性,則直接覆蓋掉;若不存在,則創建新的屬性。 } } } } // 返回修改的對象 return target; };
jQuery的extend方法使用基本的遞歸思路實現了淺拷貝和深拷貝,但是這個方法也無法處理源對象內部循環引用,例如:
var a = {"name":"aaa"}; var b = {"name":"bbb"}; a.child = b; b.parent = a; $.extend(true,{},a);//直接報了棧溢出。Uncaught RangeError: Maximum call stack size exceeded
5、自己動手實現一個拷貝方法
*請認真填寫需求信息,我們會在24小時內與您取得聯系。