一文搞懂JavaScript中的內(nèi)存泄露
以前我們說(shuō)的內(nèi)存泄漏,通常發(fā)生在后端,但是不代表前端就不會(huì)有內(nèi)存泄漏。特別是當(dāng)前端項(xiàng)目變得越來(lái)越復(fù)雜后,前端也逐漸稱(chēng)為內(nèi)存泄漏的高發(fā)區(qū)。本文就帶你認(rèn)識(shí)一下Javascript的內(nèi)存泄漏。
什么是內(nèi)存泄漏
什么是內(nèi)存??jī)?nèi)存其實(shí)就是程序在運(yùn)行時(shí),系統(tǒng)為其分配的一塊存儲(chǔ)空間。每一塊內(nèi)存都有對(duì)應(yīng)的生命周期:
- 內(nèi)存分配:在聲明變量、函數(shù)時(shí),系統(tǒng)分配的內(nèi)存空間
- 內(nèi)存使用:對(duì)分配到的內(nèi)存進(jìn)行讀/寫(xiě)操作,即訪問(wèn)并使用變量、函數(shù)等
- 釋放內(nèi)存:內(nèi)存使用完畢后,釋放掉不再被使用的內(nèi)存
不像C語(yǔ)言等底層語(yǔ)言需要程序員在開(kāi)發(fā)的時(shí)候自己通過(guò)malloc
和free
來(lái)申請(qǐng)或者釋放內(nèi)存,JavaScript同大多數(shù)現(xiàn)代編程語(yǔ)言一樣,都實(shí)現(xiàn)了給變量自動(dòng)分配內(nèi)存,并且在不使用變量的時(shí)候“自動(dòng)”釋放內(nèi)存,這個(gè)釋放內(nèi)存的過(guò)程就被稱(chēng)為垃圾回收。
每一個(gè)程序的運(yùn)行都需要一塊內(nèi)存空間,如果某一塊內(nèi)存空間在使用后未被釋放,并且持續(xù)累積,導(dǎo)致未釋放的內(nèi)存空間越積越多,直至用盡全部的內(nèi)存空間。程序?qū)o(wú)法正常運(yùn)行,直觀體現(xiàn)就是程序卡死,系統(tǒng)崩潰,這一現(xiàn)象就被稱(chēng)為內(nèi)存泄漏。
我們來(lái)舉幾個(gè)例子說(shuō)明一下:
內(nèi)存分配
const obj = { name: '張三' } // 給{name: '張三'}分配內(nèi)存 function foo() { console.log('hello world') } // 給函數(shù)分配內(nèi)存 let date = new Date(); // 根據(jù)函數(shù)返回的結(jié)果創(chuàng)建變量,會(huì)分配一個(gè)Date對(duì)象
可能發(fā)生內(nèi)存泄漏
function foo() { const obj = {name: '張三'} window.obj = obj; console.log(obj) } foo(); // foo()執(zhí)行完畢,{name: '張三'}對(duì)應(yīng)的內(nèi)存空間本應(yīng)該被釋放,但是由于又被全局變量所引用,因此其對(duì)應(yīng)的內(nèi)存空間不會(huì)被垃圾回收
閉包的內(nèi)存占用
function bar() { const data = {} return { get(key) { return data[key] }, set(key, value) { data[key] = value } }; // 閉包對(duì)象 } const {get, set} = bar; // 結(jié)構(gòu) set('name', '張三') get('name'); // 張三 // 函數(shù)執(zhí)行完畢,data對(duì)象并不會(huì)被垃圾回收,這是閉包的機(jī)制
內(nèi)存泄漏聽(tīng)起來(lái)可能會(huì)有點(diǎn)抽象,怎么能比較直觀的看到內(nèi)存泄漏的過(guò)程呢?
怎么檢測(cè)內(nèi)存泄漏
內(nèi)存泄漏主要是指的是內(nèi)存持續(xù)升高,但是如果是正常的內(nèi)存增長(zhǎng)的話,不應(yīng)該被當(dāng)作內(nèi)存泄漏來(lái)排查。排查內(nèi)存泄漏,我們可以借助Chrome DevTools的Performance和Memory選項(xiàng)。舉個(gè)栗子:
我們新建一個(gè)memory.html
的文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> body { text-align: center; } </style> </head> <body> <p>檢測(cè)內(nèi)存變化</p> <button id="btn">開(kāi)始</button> <script> const arr = []; // 數(shù)組中添加100萬(wàn)個(gè)數(shù)據(jù) for (let i = 0; i < 100 * 10000; i++) { arr.push(i) } function bind() { const obj = { str: JSON.stringify(arr) // 淺拷貝的方式創(chuàng)建一個(gè)比較大的字符串 } // 每次調(diào)用bind函數(shù),都在全局綁定一個(gè)onclick監(jiān)聽(tīng)事件,不一定非要執(zhí)行 // 使用綁定事件,主要是為了保持obj被全局標(biāo)記 window.addEventListener('click', () => { // 引用對(duì)象obj console.log(obj); }) } let n = 0; function start() { setTimeout(() => { bind(); // 調(diào)用bind函數(shù) n++; // 循環(huán)次數(shù)增加 if (n < 50) { start(); // 循環(huán)執(zhí)行50次,注意這里并沒(méi)有使用setInterval定時(shí)器 } else { alert('done'); } }, 200); } document.getElementById('btn').addEventListener('click', () => { start(); }) </script> </body> </html>
在無(wú)法確定是否發(fā)生內(nèi)存泄漏時(shí),我們可以先使用Performance來(lái)錄制一段頁(yè)面加載的性能變化,先判斷是否有內(nèi)存泄漏發(fā)生。
Performance
本次案例僅以Chrome瀏覽器展開(kāi)描述,其他瀏覽器可能會(huì)有些許差異。首先我們鼠標(biāo)右鍵選擇檢查或者直接F12進(jìn)入DevTools頁(yè)面,面板上選擇Performance
,選擇后應(yīng)該是如下頁(yè)面:
在開(kāi)始之前,我們先點(diǎn)擊一下Collect garbage
和clear
來(lái)保證內(nèi)存干凈,沒(méi)有其他遺留內(nèi)存的干擾。然后我們點(diǎn)擊Record
來(lái)開(kāi)始錄制,并且同時(shí)我們也要點(diǎn)擊頁(yè)面上的開(kāi)始
按鈕,讓我們的代碼跑起來(lái)。等到代碼結(jié)束后,我們?cè)冱c(diǎn)擊Record
按鈕以停止錄制,錄制的時(shí)間跟代碼執(zhí)行的時(shí)間相比會(huì)有出入,只要保證代碼是完全執(zhí)行完畢的即可。停止錄制后,我們會(huì)得到如下的結(jié)果:
Performance
的內(nèi)容很多,我們只需要關(guān)注內(nèi)存的變化,由此圖可見(jiàn),內(nèi)存這塊區(qū)域的曲線是在一直升高的并且到達(dá)頂點(diǎn)后并沒(méi)有回落,這就有可能發(fā)生了內(nèi)存泄漏。因?yàn)檎5膬?nèi)存變化曲線應(yīng)該是類(lèi)似于“鋸齒”,也就是有上有下,正常增長(zhǎng)后會(huì)有一定的回落,但不一定回落到和初始值一樣。而且我們還可以隱約看到程序運(yùn)行結(jié)束后,內(nèi)存從初始的6.2MB增加到了差不多351MB,這個(gè)數(shù)量級(jí)的增加還是挺明顯的。我們只是執(zhí)行了50次循環(huán),如果執(zhí)行的次數(shù)更多,將會(huì)耗盡瀏覽器的內(nèi)存空間,導(dǎo)致頁(yè)面卡死。
雖然是有內(nèi)存泄漏,但是如果我們想進(jìn)一步看內(nèi)存泄漏發(fā)生的地方,那么Performance
就不夠用了,這個(gè)時(shí)候我們就需要使用Memory
面板。
Memory
DevTools的Memory選項(xiàng)主要是用來(lái)錄制堆內(nèi)存的快照,為的是進(jìn)一步分析內(nèi)存泄漏的詳細(xì)信息。有人可能會(huì)說(shuō),為啥不一開(kāi)始就直接使用Memory
呢,反而是先使用Performance
。因?yàn)槲覀儎傞_(kāi)始就說(shuō)了,內(nèi)存增長(zhǎng)不表示就一定出現(xiàn)了內(nèi)存泄漏,有可能是正常的增長(zhǎng),直接使用Memory來(lái)分析可能得不到正確的結(jié)果。
我們先來(lái)看一下怎么使用Memory
:
首先選擇Memory
選項(xiàng),然后清除緩存,在配置選項(xiàng)中選擇堆內(nèi)存快照。內(nèi)存快照每次點(diǎn)擊錄制按鈕都會(huì)記錄當(dāng)前的內(nèi)存使用情況,我們可以在程序開(kāi)始前點(diǎn)擊一下記錄初始的內(nèi)存使用,代碼結(jié)束后再點(diǎn)一下記錄最終的內(nèi)存使用,中間可以點(diǎn)擊也可以不點(diǎn)擊。最后在快照列表中至少可以得到兩個(gè)內(nèi)存記錄:
初始內(nèi)存我們暫時(shí)不深究,我們選擇列表的最后一條記錄,然后在篩選下拉框選擇最后一個(gè),即第一個(gè)快照和第二個(gè)快照的差異。
這里我們重點(diǎn)說(shuō)一下Shallow Size
和Retained Size
的區(qū)別:
- Shallow Size:對(duì)象自身占用的內(nèi)存大小,一般來(lái)說(shuō)字符串、數(shù)組的Shallow Size都會(huì)比較大
- Retained Size:這個(gè)是對(duì)象自身占用的內(nèi)存加上無(wú)法被GC釋放的內(nèi)存的大小,如果Retained Size和Shallow Size相差不大,基本上可以判定沒(méi)有發(fā)生內(nèi)存泄漏,但是如果相差很大,例如上圖的
Object
,這就表明發(fā)生了內(nèi)存泄漏。
我們?cè)賮?lái)細(xì)看一下Object
,任意展開(kāi)一個(gè)對(duì)象,可以在樹(shù)結(jié)構(gòu)中發(fā)現(xiàn)每一個(gè)對(duì)象都有一個(gè)全局事件綁定,并且占用了較大的內(nèi)存空間。解決本案例涉及的內(nèi)存泄漏也比較簡(jiǎn)單,就是及時(shí)釋放綁定的全局事件。
關(guān)于Performance
和Memory
的詳細(xì)使用可以參考:手把手教你排查Javascript內(nèi)存泄漏
內(nèi)存泄漏的場(chǎng)景
大多數(shù)情況下,垃圾回收器會(huì)幫我們及時(shí)釋放內(nèi)存,一般不會(huì)發(fā)生內(nèi)存泄漏。但是有些場(chǎng)景是內(nèi)存泄漏的高發(fā)區(qū),我們?cè)谑褂玫臅r(shí)候一定要注意:
我們?cè)陂_(kāi)發(fā)的時(shí)候經(jīng)常會(huì)使用console
在控制臺(tái)打印信息,但這也會(huì)帶來(lái)一個(gè)問(wèn)題:被console
使用的對(duì)象是不能被垃圾回收的,這就可能會(huì)導(dǎo)致內(nèi)存泄漏。因此在生產(chǎn)環(huán)境中不建議使用console.log()
的理由就又可以加上一條了
被全局變量、全局函數(shù)引用的對(duì)象,在Vue組件銷(xiāo)毀時(shí)未清除,可能會(huì)導(dǎo)致內(nèi)存泄漏
// Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); onMounted(() => { window.arr = arr; // 被全局變量引用 window.arrFunc = () => { console.log(arr); // 被全局函數(shù)引用 } }) // 正確的方式 onBeforeUnmount(() => { window.arr = null; window.arrFunc = null; }) </script>
定時(shí)器未及時(shí)在Vue組件銷(xiāo)毀時(shí)清除,可能會(huì)導(dǎo)致內(nèi)存泄漏
// Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); const timer = reactive(null); onMounted(() => { setInterval(() => { console.log(arr); // arr被定時(shí)器占用,無(wú)法被垃圾回收 }, 200); // 正確的方式 timer = setInterval(() => { console.log(arr); }, 200); }) // 正確的方式 onBeforeUnmount(() => { if (timer) { clearInterval(timer); timer = null; } }) </script>
setTimeout
和setInterval
兩個(gè)定時(shí)器在使用時(shí)都應(yīng)該注意是否需要清理定時(shí)器,特別是setInterval
,一定要注意清除。
綁定的事件未及時(shí)在Vue組件銷(xiāo)毀時(shí)清除,可能會(huì)導(dǎo)致內(nèi)存泄漏
綁定事件在實(shí)際開(kāi)發(fā)中經(jīng)常遇到,我們一般使用addEventListener
來(lái)創(chuàng)建。
// Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); const printArr = () => { console.log(arr) } onMounted(() => { // 監(jiān)聽(tīng)事件綁定的函數(shù)為匿名函數(shù),將無(wú)法被清除 window.addEventListener('click', () => { console.log(arr); // 全局綁定的click事件,arr被引用,將無(wú)法被垃圾回收 }) // 正確的方式 window.addEventListener('click', printArr); }) // 正確的方式 onBeforeUnmount(() => { // 注意清除綁定事件需要前后是同一個(gè)函數(shù),如果函數(shù)不同將不會(huì)清除 window.removeEventListener('click', printArr); }) </script>
被自定義事件引用,在Vue組件銷(xiāo)毀時(shí)未清除,可能會(huì)導(dǎo)致內(nèi)存泄漏
自定義事件通過(guò)emit/on
來(lái)發(fā)起和監(jiān)聽(tīng),清除自定義事件和綁定事件差不多,不同的是需要調(diào)用off
方法
// Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' import event from './event.js'; // 自定義事件 const arr = reactive([1,2,3]); const printArr = () => { console.log(arr) } onMounted(() => { // 使用匿名函數(shù),會(huì)導(dǎo)致自定義事件無(wú)法被清除 event.on('printArr', () => { console.log(arr) }) // 正確的方式 event.on('printArr', printArr) }) // 正確的方式 onBeforeUnmount(() => { // 注意清除自定義事件需要前后是同一個(gè)函數(shù),如果函數(shù)不同將不會(huì)清除 event.off('printArr', printArr) }) </script>
除了及時(shí)清除監(jiān)聽(tīng)器、事件等,對(duì)于全局變量的引用,我們可以選擇WeakMap
、WeakSet
等弱引用數(shù)據(jù)類(lèi)型。這樣的話,即使我們引用的對(duì)象數(shù)據(jù)要被垃圾回收,弱引用的全局變量并不會(huì)阻止GC。
垃圾回收算法
我們知道了內(nèi)存泄漏的含義,也知道了怎么來(lái)檢測(cè)內(nèi)存泄漏,甚至可以一定程度上規(guī)避內(nèi)存泄漏了。除了那些容易產(chǎn)生內(nèi)存泄漏的場(chǎng)景,js是使用什么樣的機(jī)制來(lái)保證垃圾會(huì)被盡可能的回收呢?垃圾回收算法早期使用的是引用計(jì)數(shù),現(xiàn)在主流都采用標(biāo)記清除的方式了。
引用計(jì)數(shù)
我們創(chuàng)建一個(gè)對(duì)象,js會(huì)在堆內(nèi)存中分配一塊區(qū)域用于存儲(chǔ)對(duì)象信息,并且在棧中存在對(duì)象數(shù)據(jù)的引用地址。舉個(gè)栗子:
let obj = {name: '張三'}; let obj2 = obj; // 對(duì)象數(shù)據(jù){name: '張三'}被obj和obj2引用,引用計(jì)數(shù)為2,此時(shí){name: '張三'}不能被垃圾回收 obj = 0; // obj雖然不引用{name: '張三'},但是obj2還在引用,此時(shí){name: '張三'}也不能被垃圾回收 obj2 = 0; // 此時(shí)的{name: '張三'}已經(jīng)是零引用了,可以被垃圾回收
下圖為創(chuàng)建變量時(shí)的內(nèi)存管理:
對(duì)于函數(shù)來(lái)說(shuō),正常情況下函數(shù)執(zhí)行完畢,其占用的內(nèi)存就會(huì)被垃圾回收:
function foo() { const obj = {name: '張三'} console.log(obj) } foo(); // 函數(shù)執(zhí)行完畢,obj作為局部變量,會(huì)隨著函數(shù)的結(jié)束而結(jié)束,{name: '張三'}由于零引用,其占用的內(nèi)存空間會(huì)被釋放
但是如果函數(shù)中存在全局引用,那么函數(shù)結(jié)束后,全局引用占用的內(nèi)存將無(wú)法被釋放
function foo() { const obj = {name: '張三'} window.obj = obj; console.log(obj) } foo(); // 函數(shù)執(zhí)行完畢,{name: '張三'}被全局引用
引用計(jì)數(shù)有一個(gè)缺陷,那就是無(wú)法處理循環(huán)引用。
循環(huán)引用
循環(huán)引用指的是兩個(gè)或者多個(gè)對(duì)象之間,存在相互引用,并形成了一個(gè)循環(huán)。如果是在函數(shù)中,函數(shù)運(yùn)行結(jié)束后應(yīng)該釋放掉所有局部變量引用的對(duì)象,但是按照引用計(jì)數(shù)算法,循環(huán)引用間并不是零引用,因此它們就不會(huì)被釋放。舉個(gè)例子:
function foo() { const obj = {name: '張三'}; const obj2 = {age: 0}; obj.a = obj2; // obj對(duì)象引用了obj2 obj2.a = obj; // obj2引用了obj } foo(); // 由于存在循環(huán)引用,{name: '張三'}和{age: 0}所占用的堆內(nèi)存都不會(huì)被釋放
上面的例子可能比較抽象,我們?cè)賮?lái)舉一個(gè)實(shí)際的例子。在IE6、7版本中(IE已于2022年6月15日正式退出了歷史舞臺(tái)),在對(duì)DOM對(duì)象進(jìn)行垃圾回收時(shí),就有可能因?yàn)檠h(huán)引用導(dǎo)致內(nèi)存泄漏。
let div = document.getElementById('div1'); div.a = div; // 循環(huán)引用自己 div.someBigData = new Array(10000).fill('*');
在上述例子中,div
對(duì)象的a
屬性引用了div
對(duì)象自身,造成了最簡(jiǎn)單的循環(huán)引用,并且該屬性并沒(méi)有被移除或者顯示設(shè)置為null,這對(duì)于引用計(jì)數(shù)器來(lái)說(shuō)就是一個(gè)有意義的引用。因此,div
對(duì)象會(huì)一直保持在內(nèi)存中,即使在DOM樹(shù)中將div1
刪除,而且div
對(duì)象的someBigData
所引用的數(shù)據(jù)也會(huì)一直保持在內(nèi)存中,不會(huì)被釋放。如果div
對(duì)象本身很大,或者其屬性引用的數(shù)據(jù)很大,那么持續(xù)累積就可能造成內(nèi)存泄漏 。
標(biāo)記清除
標(biāo)記清除算法是對(duì)引用計(jì)數(shù)算法的改進(jìn),如果說(shuō)引用計(jì)數(shù)算法是判斷對(duì)象是否不再需要,那么標(biāo)記清楚算法就是判斷對(duì)象是否可以獲得??梢垣@得的對(duì)象就保留在內(nèi)存中,不可獲得的對(duì)象就會(huì)被垃圾回收。
垃圾回收并不是實(shí)時(shí)的,使用標(biāo)記清除算法的垃圾回收器,會(huì)定期從根對(duì)象開(kāi)始,在js中就是從window對(duì)象開(kāi)始,找出所有從根開(kāi)始引用的對(duì)象,以及找到這些對(duì)象引用的對(duì)象,直到全部遍歷。垃圾回收器就可以收集到所有可獲得的對(duì)象以及所有不可獲得的對(duì)象,然后將不可獲得的對(duì)象回收。
使用標(biāo)記清除算法可以有效解決引用計(jì)數(shù)算法的循環(huán)引用問(wèn)題,還是剛剛那個(gè)例子:
function foo() { const obj = {name: '張三'}; const obj2 = {age: 0}; obj.a = obj2; // obj對(duì)象引用了obj2 obj2.a = obj; // obj2引用了obj } foo();
foo()函數(shù)執(zhí)行完畢后,垃圾回收器從window對(duì)象開(kāi)始找,發(fā)現(xiàn)obj和obj2是不可獲得的對(duì)象,那么其引用的數(shù)據(jù)就會(huì)被回收。我們簡(jiǎn)單修改一下代碼:
function foo() { const obj = {name: '張三'}; const obj2 = {age: 0}; obj.a = obj2; // obj對(duì)象引用了obj2 obj2.a = obj; // obj2引用了obj window.obj = obj; } foo(); console.log(window.obj)
foo()函數(shù)執(zhí)行完畢,垃圾回收期從window對(duì)象開(kāi)始找,發(fā)現(xiàn)可以在window對(duì)象上找到obj屬性,obj和obj2存在循環(huán)引用,最終obj和obj2引用的數(shù)據(jù)都不會(huì)被垃圾回收。
這個(gè)例子也告訴我們,使用全局變量時(shí)一定要注意是否可能造成內(nèi)存泄漏,詳細(xì)可查看內(nèi)存泄漏的場(chǎng)景。使用標(biāo)記清楚算法就基本上能滿(mǎn)足垃圾回收的需求了,而且從2012年開(kāi)始,現(xiàn)代主流瀏覽器的垃圾回收器都實(shí)現(xiàn)了標(biāo)記清除算法,后續(xù)的改進(jìn)也是基于該算法來(lái)實(shí)現(xiàn)的。
閉包是內(nèi)存泄漏嗎
使用過(guò)閉包的都知道,其數(shù)據(jù)是保持在內(nèi)存中的,不會(huì)被回收,那么閉包是內(nèi)存泄漏嗎?答案:閉包不是內(nèi)存泄漏。因?yàn)槲覀冋f(shuō)的內(nèi)存泄漏是不符合預(yù)期的內(nèi)存持續(xù)增長(zhǎng),閉包雖然也會(huì)占著內(nèi)存不釋放,但是這個(gè)是符合我們預(yù)期的效果。
閉包不是內(nèi)存泄漏,難道就可以隨便使用了嗎。那肯定不是的,閉包中的數(shù)據(jù)如果很大,也會(huì)消耗大量的內(nèi)存,造成網(wǎng)頁(yè)卡頓,甚至崩潰。因此我們不能濫用閉包,在閉包函數(shù)退出前,一些不必要的局部變量該清除的還是要清除。
總結(jié)
本文詳細(xì)的解釋了什么是內(nèi)存泄漏,泄漏了該怎么檢測(cè),瀏覽器是怎么來(lái)進(jìn)行垃圾回收的,以及一些常見(jiàn)的內(nèi)存泄漏場(chǎng)景??偟膩?lái)說(shuō),內(nèi)存泄漏就是指發(fā)生非預(yù)期的內(nèi)存占用持續(xù)增長(zhǎng),以至于耗盡內(nèi)存,導(dǎo)致系統(tǒng)崩潰。內(nèi)存泄漏在實(shí)際開(kāi)發(fā)過(guò)程中還是比較容易犯的,在寫(xiě)代碼的時(shí)候一定要留意高發(fā)場(chǎng)景,盡可能在寫(xiě)代碼的時(shí)候就避免潛在的內(nèi)存泄漏,而不是等到系統(tǒng)崩潰了才來(lái)一句:重啟(刷新)大法好。
以上就是一文搞懂JavaScript中的內(nèi)存泄露的詳細(xì)內(nèi)容,更多關(guān)于JavaScript內(nèi)存泄露的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js實(shí)現(xiàn)文本框輸入文字個(gè)數(shù)限制代碼
這篇文章主要介紹了js實(shí)現(xiàn)文本框輸入文字個(gè)數(shù)限制代碼,文本框輸入的文字個(gè)數(shù)并不是無(wú)限制的,一般都會(huì)限定一個(gè)輸入最高上限,如何限制,請(qǐng)看本文2015-12-12JavaScript標(biāo)準(zhǔn)對(duì)象_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了JavaScript標(biāo)準(zhǔn)對(duì)象的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06一文教你如何實(shí)現(xiàn)localStorage的過(guò)期機(jī)制
要知道localStorage本身并沒(méi)有提供過(guò)期機(jī)制,既然如此那就只能我們自己來(lái)實(shí)現(xiàn)了,這篇文章主要給大家介紹了關(guān)于如何實(shí)現(xiàn)localStorage過(guò)期機(jī)制的相關(guān)資料,需要的朋友可以參考下2022-02-02JS計(jì)算兩個(gè)時(shí)間相差分鐘數(shù)的方法示例
這篇文章主要介紹了JS計(jì)算兩個(gè)時(shí)間相差分鐘數(shù)的方法,結(jié)合完整實(shí)例形式分析了javascript針對(duì)日期時(shí)間的轉(zhuǎn)換與計(jì)算相關(guān)操作技巧,需要的朋友可以參考下2018-01-01uniapp小程序點(diǎn)擊輸入框時(shí)阻止彈出軟鍵盤(pán)的幾種解決方案
在寫(xiě)項(xiàng)目時(shí)候需要在表單里面加一個(gè)picker選擇器,但選擇input的時(shí)候軟鍵盤(pán)與選擇器會(huì)同時(shí)彈出,下面這篇文章主要給大家介紹了關(guān)于uniapp小程序點(diǎn)擊輸入框時(shí)阻止彈出軟鍵盤(pán)的幾種解決方案,需要的朋友可以參考下2024-02-02詳解微信小程序調(diào)起鍵盤(pán)性能優(yōu)化
這篇文章主要介紹了詳解微信小程序調(diào)起鍵盤(pán)性能優(yōu)化,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07用js實(shí)現(xiàn)計(jì)算代碼行數(shù)的簡(jiǎn)單方法附代碼
用js實(shí)現(xiàn)計(jì)算代碼行數(shù)的簡(jiǎn)單方法附代碼...2007-08-08Bootstrap輪播插件中圖片變形的終極解決方案 使用jqthumb.js
這篇文章主要介紹了Bootstrap輪播插件中圖片變形的終極解決方案,使用jqthumb.js,感興趣的小伙伴們可以參考一下2016-07-07