春招季如何橫掃 Javascript 面試核心考點? | 技術頭條
作者 | 浪裡行舟
責編| 郭芮
Java是前端面試的重點,本文重點梳理下Java中的常考知識點,然後就一些容易出現的題目進行解析。限於文章的篇幅,無法將知識點講解的面面俱到,因此只羅列了一些重難點。
變數型別
1、JS 的資料型別分類
根據 Java 中的變數型別傳遞方式,分為基本資料型別和引用資料型別。其中基本資料型別包括Undefined、Null、Boolean、Number、String、Symbol (ES6新增,表示獨一無二的值),而引用資料型別統稱為Object物件,主要包括物件、陣列和函式。在引數傳遞方式上,基本型別是按值傳遞,引用型別是按共享傳遞。
PS:注重基本型別和引用型別的區別。
基本型別和引用型別儲存於記憶體的位置不同,基本型別直接儲存在棧中,而引用型別的物件儲存在堆中,與此同時,在棧中儲存了指標,而這個指標指向正是堆中實體的起始位置。
下面通過一個小題目,來看下兩者的主要區別:
// 基本型別
vara = 10
varb = a
b = 20
console.log(a) // 10
console.log(b) // 20
上述程式碼中,a b都是值型別,兩者分別修改賦值,相互之間沒有任何影響。再看引用型別的例子:
// 引用型別
vara = { x: 10, y: 20}
varb = a
b.x = 100
b.y = 200
console.log(a) // {x: 100, y: 200}
console.log(b) // {x: 100, y: 200}
上述程式碼中,a b都是引用型別。在執行了b = a之後,修改b的屬性值,a的也跟著變化。因為a和b都是引用型別,指向了同一個記憶體地址,即兩者引用的是同一個值,因此b修改屬性時,a的值隨之改動。
2、資料型別的判斷
1)typeof
typeof返回一個表示資料型別的字串,返回結果包括:number、boolean、string、symbol、object、undefined、function等7種資料型別,但不能判斷null、array等。
typeofSymbol(); // symbol 有效
typeof''; // string 有效
typeof1; // number 有效
typeoftrue; //boolean 有效
typeofundefined; //undefined 有效
typeofnewFunction(); // function 有效
typeofnull; //object 無效
typeof[] ; //object 無效
typeofnewDate(); //object 無效
typeofnewRegExp(); //object 無效
2)instanceof
instanceof 是用來判斷A是否為B的例項,表示式為:A instanceof B,如果A是B的例項,則返回true,否則返回false。instanceof 運算子用來測試一個物件在其原型鏈中是否存在一個建構函式的 prototype 屬性,但它不能檢測null 和 undefined。
[] instanceofArray; //true
{} instanceofObject; //true
newDate() instanceofDate; //true
newRegExp() instanceofRegExp//true
nullinstanceofNull //報錯
undefinedinstanceofundefined//報錯
3)嚴格運算子===
只能用於判斷null和undefined,因為這兩種型別的值都是唯一的。
4)constructor
constructor作用和instanceof非常相似。但constructor檢測 Object與instanceof不一樣,還可以處理基本資料型別的檢測。
不過函式的 constructor 是不穩定的,這個主要體現在把類的原型進行重寫,在重寫的過程中很有可能出現把之前的constructor給覆蓋了,這樣檢測出來的結果就是不準確的。
5)Object.prototype.toString.call()
Object.prototype.toString.call() 是最準確最常用的方式。
Object.prototype.toString.call( '') ; // [object String]
Object.prototype.toString.call( 1) ; // [object Number]
Object.prototype.toString.call( true) ; // [object Boolean]
Object.prototype.toString.call( undefined) ; // [object Undefined]
Object.prototype.toString.call( null) ; // [object Null]
Object.prototype.toString.call( newFunction()) ; // [object Function]
Object.prototype.toString.call( newDate()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call( newRegExp()) ; // [object RegExp]
Object.prototype.toString.call( newError()) ; // [object Error]
3、淺拷貝與深拷貝
淺拷貝只複製指向某個物件的指標,而不復制物件本身,新舊物件還是共享同一塊記憶體。
淺拷貝的實現方式(詳見淺拷貝與深拷貝):
- Object.assign():需注意的是目標物件只有一層的時候,是深拷貝;
- Array.prototype.concat();
- Array.prototype.slice()。
深拷貝就是在拷貝資料的時候,將資料的所有引用結構都拷貝一份。簡單的說就是,在記憶體中存在兩個資料結構完全相同又相互獨立的資料,將引用型型別進行復制,而不是隻複製其引用關係。
深拷貝的實現方式:
- 熱門的函式庫lodash,也有提供_.cloneDeep用來做深拷貝;
- jquery 提供一個$.extend可以用來做深拷貝;
- JSON.parse(JSON.stringify());
- 手寫遞迴方法。
遞迴實現深拷貝的原理:要拷貝一個數據,我們肯定要去遍歷它的屬性,如果這個物件的屬性仍是物件,繼續使用這個方法,如此往復。
//定義檢測資料型別的功能函式
function checkedType(target) {
returnObject.prototype.toString.call(target).slice( 8, -1)
}
//實現深度克隆---物件/陣列
function clone(target) {
//判斷拷貝的資料型別
//初始化變數result 成為最終克隆的資料
letresult,
targetType = checkedType(target)
if(targetType === 'Object') {
result = {}
} elseif(targetType === 'Array') {
result = []
} else{
returntarget
}
//遍歷目標資料
for( leti intarget) {
//獲取遍歷資料結構的每一項值。
letvalue= target[i]
//判斷目標結構裡的每一值是否存在物件/陣列
if(checkedType( value) === 'Object'|| checkedType( value) === 'Array') {
//物件/數組裡嵌套了物件/陣列
//繼續遍歷獲取到value值
result[i] = clone( value)
} else{
//獲取到value值是基本的資料型別或者是函式。
result[i] = value
}
}
returnresult
}
作用域和閉包
1、執行上下文和執行棧
執行上下文就是當前 Java 程式碼被解析和執行時所在環境的抽象概念, Java 中執行任何的程式碼都是在執行上下文中執行。執行上下文的生命週期包括三個階段:建立階段→執行階段→回收階段,我們重點介紹建立階段。
建立階段(當函式被呼叫,但未執行任何其內部程式碼之前)會做以下三件事:
- 建立變數物件:首先初始化函式的引數arguments,提升函式宣告和變數宣告;
- 建立作用域鏈:下文會介紹;
- 確定this指向:下文會介紹。
functiontest(arg){
// 1.形參 arg是 "hi"
// 2.因為函式宣告比變數宣告優先順序高,所以此時 arg是 function
console.log(arg);
var arg= 'hello'; // 3.var arg變數宣告被忽略, arg= 'hello'被執行
functionarg(){
console. log( 'hello world')
}
console. log( arg);
}
test( 'hi');
/* 輸出:
functionarg(){
console. log( 'hello world');
}
hello
*/
這是因為當函式執行的時候,首先會形成一個新的私有的作用域,然後依次按照如下的步驟執行:
- 如果有形參,先給形參賦值;
- 進行私有作用域中的預解釋,函式宣告優先順序比變數宣告高,最後後者會被前者所覆蓋,但是可以重新賦值;
- 私有作用域中的程式碼從上到下執行。
函式多了,就有多個函式執行上下文,每次呼叫函式建立一個新的執行上下文,那如何管理建立的那麼多執行上下文呢?
Java 引擎建立了執行棧來管理執行上下文。可以把執行棧認為是一個儲存函式呼叫的棧結構,遵循先進後出的原則。
從上面的流程圖,我們需要記住幾個關鍵點:
- Java執行在單執行緒上,所有的程式碼都是排隊執行;
- 一開始瀏覽器執行全域性的程式碼時,首先建立全域性的執行上下文,壓入執行棧的頂部;
- 每當進入一個函式的執行就會建立函式的執行上下文,並且把它壓入執行棧的頂部。當前函式執行完成後,當前函式的執行上下文出棧,並等待垃圾回收;
- 瀏覽器的JS執行引擎總是訪問棧頂的執行上下文;
- 全域性上下文只有唯一的一個,它在瀏覽器關閉時出棧。
2、作用域與作用域鏈
ES6 到來Java 有全域性作用域、函式作用域和塊級作用域(ES6新增)。我們可以這樣理解:作用域就是一個獨立的地盤,讓變數不會外洩、暴露出去。也就是說作用域最大的用處就是隔離變數,不同作用域下同名變數不會有衝突。
在介紹作用域鏈之前,先要了解下自由變數,如下程式碼中,console.log(a)要得到a變數,但是在當前的作用域中沒有定義a(可對比一下b)。當前作用域沒有定義的變數,這成為自由變數。
vara = 100
functionfn() {
varb = 200
console.log(a) // 這裡的a在這裡就是一個自由變數
console.log(b)
}
fn()
自由變數的值如何得到 —— 向父級作用域(建立該函式的那個父級作用域)尋找。如果父級也沒呢?再一層一層向上尋找,直到找到全域性作用域還是沒找到,就宣佈放棄。這種一層一層的關係,就是作用域鏈 。
functionF1() {
vara = 100
returnfunction() {
console.log(a)
}
}
functionF2(f1) {
vara = 200
console.log(f1())
}
varf1 = F1()
F2(f1) // 100
上述程式碼中,自由變數a的值,從函式F1中查詢而不是F2,這是因為當自由變數從作用域鏈中去尋找,依據的是函式定義時的作用域鏈,而不是函式執行時。
3、閉包是什麼?
閉包這個概念也是Java中比較抽象的概念,我個人理解,閉包是就是函式中的函式(其他語言不能這樣),裡面的函式可以訪問外面函式的變數,外面的變數的是這個內部函式的一部分。
閉包的作用:
- 使用閉包可以訪問函式中的變數;
- 可以使變數長期儲存在記憶體中,生命週期比較長。
閉包不能濫用,否則會導致記憶體洩露,影響網頁的效能。閉包使用完了後,要立即釋放資源,將引用變數指向null。
閉包主要有兩個應用場景:
- 函式作為引數傳遞(見作用域部分例子);
- 函式作為返回值(如下例)。
functionouter() {
varnum = 0//內部變數
returnfunctionadd() {
//通過return返回add函式,就可以在outer函式外訪問了。
num++ //內部函式有引用,作為add函式的一部分了
console.log(num)
}
}
varfunc1 = outer() //
func1() //實際上是呼叫add函式, 輸出1
func1() //輸出2
varfunc2 = outer()
func2() // 輸出1
func2() // 輸出2
4、this全面解析
先搞明白一個很重要的概念——this的值是在執行的時候才能確認,定義的時候不能確認! 為什麼呢?因為this是執行上下文環境的一部分,而執行上下文需要在程式碼執行之前確定,而不是定義的時候。
看如下例子:
// 情況1
function foo() {
console.log( this.a) //1
}
vara = 1
foo()
// 情況2
function fn(){
console.log( this);
}
varobj={fn:fn};
obj.fn(); //this->obj
// 情況3
function CreateJsPerson(name,age){
//this是當前類的一個例項p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
varp1= newCreateJsPerson( "尹華芝", 48);
// 情況4
function add(c, d){
returnthis.a + this.b + c + d;
}
varo = {a: 1, b: 3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [ 10, 20]); // 1 + 3 + 10 + 20 = 34
// 情況5
< type= "text/java">
letbtn1 = document.getElementById( 'btn1');
letobj = {
name: 'kobe',
age: 39,
getName: function () {
btn1.onclick = () => {
console.log( this); //obj
};
}
};
obj.getName();
>
接下來我們逐一解釋上面幾種情況。
- 對於直接呼叫 foo 來說,不管 foo 函式被放在了什麼地方,this 一定是 window;
- 對於 obj.foo() 來說,我們只需要記住,誰呼叫了函式,誰就是 this,所以在這個場景下 foo 函式中的 this 就是 obj 物件;
- 在建構函式模式中,類中(函式體中)出現的this.xxx=xxx中的this是當前類的一個例項;
- call、apply 和 bind:this 是第一個引數;
- 箭頭函式this指向:箭頭函式沒有自己的this,看其外層的是否有函式,如果有,外層函式的this就是內部箭頭函式的this,如果沒有,則this是window。
非同步
1、同步 vs 非同步
同步,我的理解是一種線性執行的方式,執行的流程不能跨越。比如說話後在吃飯,吃完飯後在看手機,必須等待上一件事完了,才執行後面的事情。
非同步,是一種並行處理的方式,不必等待一個程式執行完,可以執行其它的任務。比方說一個人邊吃飯,邊看手機,邊說話,就是非同步處理的方式。在程式中非同步處理的結果通常使用回撥函式來處理結果。
// 同步
console. log( 100)
alert( 200);
console. log( 300) //100 200 300
// 非同步
console.log( 100)
setTimeout( function(){
console.log( 200)
})
console.log( 300) //100 300 200
2、非同步和單執行緒
JS 需要非同步的根本原因是 JS 是單執行緒執行的,即在同一時間只能做一件事,不能“一心二用”。為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許Java指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變Java單執行緒的本質。
一個 Ajax 請求由於網路比較慢,請求需要 5 秒鐘。如果是同步,這 5 秒鐘頁面就卡死在這裡啥也幹不了了。非同步的話,就好很多了,5 秒等待就等待了,其他事情不耽誤做,至於那 5 秒鐘等待是網速太慢,不是因為 JS 的原因。
3、前端非同步的場景
前端使用非同步的場景:
- 定時任務:setTimeout,setInterval;
- 網路請求:ajax請求,動態載入;
- 事件繫結。
4、Event Loop
一個完整的 Event Loop 過程,可以概括為以下階段:
- 一開始執行棧空,我們可以把執行棧認為是一個儲存函式呼叫的棧結構,遵循先進後出的原則。micro 佇列空,macro 佇列裡有且只有一個 指令碼(整體程式碼)。
- 全域性上下文( 標籤)被推入執行棧,同步程式碼執行。在執行的過程中,會判斷是同步任務還是非同步任務,通過對一些介面的呼叫,可以產生新的 macro-task 與 micro-task,它們會分別被推入各自的任務佇列裡。同步程式碼執行完了, 指令碼會被移出 macro 佇列,這個過程本質上是佇列的 macro-task 的執行和出隊的過程。
- 上一步我們出隊的是一個 macro-task,這一步我們處理的是 micro-task。但需要注意的是:當 macro-task 出隊時,任務是一個一個執行的;而 micro-task 出隊時,任務是一隊一隊執行的。因此,我們處理 micro 佇列這一步,會逐個執行佇列中的任務並把它出隊,直到佇列被清空。
- 執行渲染操作,更新介面。
- 檢查是否存在 Web worker 任務,如果有,則對其進行處理。
- 上述過程迴圈往復,直到兩個佇列都清空。
接下來我們看道例子來介紹上面流程:
Promise.resolve(). then( ()=>{
console.log( 'Promise1')
setTimeout( ()=>{
console.log( 'setTimeout2')
}, 0)
})
setTimeout( ()=>{
console.log( 'setTimeout1')
Promise.resolve(). then( ()=>{
console.log( 'Promise2')
})
}, 0)
最後輸出結果是Promise1,setTimeout1,Promise2,setTimeout2。
- 一開始執行棧的同步任務(這屬於巨集任務)執行完畢,會去檢視是否有微任務佇列,上題中存在(有且只有一個),然後執行微任務佇列中的所有任務輸出Promise1,同時會生成一個巨集任務 setTimeout2。
- 然後去檢視巨集任務佇列,巨集任務 setTimeout1 在 setTimeout2 之前,先執行巨集任務 setTimeout1,輸出 setTimeout1。
- 在執行巨集任務 setTimeout1 時會生成微任務 Promise2,放入微任務佇列中,接著先去清空微任務佇列中的所有任務,輸出 Promise2。
- 清空完微任務佇列中的所有任務後,就又會去巨集任務佇列取一個,這回執行的是 setTimeout2。
原型鏈與繼承
1、原型和原型鏈
原型:在Java中原型是一個prototype物件,用於表示型別之間的關係。
原型鏈:Java萬物都是物件,物件和物件之間也有關係,並不是孤立存在的。物件之間的繼承關係,在Java中是通過prototype物件指向父類物件,直到指向Object物件為止,這樣就形成了一個原型指向的鏈條,專業術語稱之為原型鏈。
varPerson = function() {
this.age = 18
this.name = '匿名'
}
varStudent = function() {}
//建立繼承關係,父類例項作為子類原型
Student.prototype = newPerson()
vars1 = newStudent()
console.log(s1)
原型關係圖:
當試圖得到一個物件的某個屬性時,如果這個物件本身沒有這個屬性,那麼會去它的__proto__(即它的建構函式的prototype)中尋找。如果一直找到最上層都沒有找到,那麼就宣告失敗,返回undefined。最上層是什麼 —— Object.prototype.__proto__ === null。
2、繼承
下面介紹幾種常見的繼承方式。
- 原型鏈+借用建構函式的組合繼承
functionParent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log( this.val)
}
functionChild(value) {
Parent.call( this, value)
}
Child.prototype = newParent()
constchild = newChild( 1)
child.getValue() // 1
child instanceofParent // true
以上繼承的方式核心是在子類的建構函式中通過 Parent.call(this) 繼承父類的屬性,然後改變子類的原型為 new Parent() 來繼承父類的函式。
這種繼承方式優點在於建構函式可以傳參,不會與父類引用屬性共享,可以複用父類的函式,但是也存在一個缺點就是在繼承父類函式的時候呼叫了父類建構函式,導致子類的原型上多了不需要的父類屬性,存在記憶體上的浪費。
- 寄生組合繼承:這種繼承方式對上一種組合繼承進行了優化
functionParent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log( this.val)
}
functionChild(value) {
Parent.call( this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
constchild = newChild( 1)
child.getValue() // 1
child instanceofParent // true
以上繼承實現的核心就是將父類的原型賦值給了子類,並且將建構函式設定為子類,這樣既解決了無用的父類屬性問題,還能正確的找到子類的建構函式。
- ES6中class的繼承
ES6中引入了class關鍵字,class可以通過extends關鍵字實現繼承,還可以通過static關鍵字定義類的靜態方法,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。需要注意的是,class關鍵字只是原型的語法糖,Java繼承仍然是基於原型實現的。
classParent{
constructor(value) {
this.val = value
}
getValue() {
console.log( this.val)
}
}
classChildextendsParent{
constructor(value) {
super(value)
this.val = value
}
}
letchild = newChild( 1)
child.getValue() // 1
child instanceofParent // true
class 實現繼承的核心在於使用 extends 表明繼承自哪個父類,並且在子類建構函式中必須呼叫 super,因為這段程式碼可以看成 Parent.call(this, value)。
DOM操作與BOM操作
1、DOM操作
當網頁被載入時,瀏覽器會建立頁面的文件物件模型(DOM),我們可以認為 DOM 就是 JS 能識別的 HTML 結構,一個普通的 JS 物件或者陣列。接下來我們介紹常見DOM操作:
- 新增節點和移動節點
vardiv1 = document.getElementById( 'div1')
// 新增新節點
varp1 = document.( 'p')
p1.innerHTML = 'this is p1'
div1.(p1) // 新增新建立的元素
// 移動已有節點。注意,這裡是“移動”,並不是拷貝
varp2 = document.getElementById( 'p2')
div1.(p2)
- 獲取父元素
vardiv1 = document.getElementById( 'div1')
varparent = div1.parentElement
- 獲取子元素
vardiv1 = document.getElementById( 'div1')
varchild = div1.childNodes
- 刪除節點
vardiv1 = document.getElementById( 'div1')
varchild = div1.childNodes
div1.removeChild(child[ 0])
2、DOM事件模型和事件流
DOM事件模型分為捕獲和冒泡,一個事件發生後,會在子元素和父元素之間傳播(propagation)。這種傳播分成三個階段:
- 捕獲階段:事件從window物件自上而下向目標節點傳播的階段;
- 目標階段:真正的目標節點正在處理事件的階段;
- 冒泡階段:事件從目標節點自下而上向window物件傳播的階段。
DOM事件捕獲的具體流程:
捕獲是從上到下,事件先從window物件,然後再到document(物件),然後是html標籤(通過document.documentElement獲取html標籤),然後是body標籤(通過document.body獲取body標籤),然後按照普通的html結構一層一層往下傳,最後到達目標元素。
接下來我們看個事件冒泡的例子:
// 事件冒泡
......
window.onclick = function(){
console. log( 'window');
};
document.onclick = function(){
console. log( 'document');
};
document.documentElement.onclick = function(){
console. log( 'html');
};
document.body.onclick = function(){
console. log( 'body');
}
outer.onclick = function(ev){
console. log( 'outer');
};
inner.onclick = function(ev){
console. log( 'inner');
};
如何阻止冒泡?
通過event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件處理程式被執行。我們可以在上例中inner元素的click事件上,新增event.stopPropagation()這句話後,就阻止了父事件的執行,最後只打印了'inner'。
inner.onclick = function(ev) {
console.log( 'inner')
ev.stopPropagation()
}
3、事件代理(事件委託)
由於事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函式定義在父節點上,由父節點的監聽函式統一處理多個子元素的事件。這種方法叫做事件的代理。
我們設定一種場景,如下程式碼,一個 <divid="div1"> <ahref="#">a1 a> <ahref="#">a2 a> <ahref="#">a3 a> <ahref="#">a4 a> div> <button>點選增加一個 a 標籤 button> vardiv1 = document.getElementById( 'div1') div1.addEventListener( 'click', function(e) { // e.target 可以監聽到觸發點選事件的元素是哪一個 vartarget = e.target if(e.nodeName === 'A') { // 點選的是 元素 alert(target.innerHTML) } }) 最後,使用代理的優點如下: 4、BOM 操作 BOM(瀏覽器物件模型)是瀏覽器本身的一些資訊的設定和獲取,例如獲取瀏覽器的寬度、高度,設定讓瀏覽器跳轉到哪個地址。 獲取螢幕的寬度和高度: console.log( screen.width) console.log( screen.height) 獲取網址、協議、path、引數、hash 等: // 例如當前網址是 https://juejin.im/timeline/frontend?a=10&b=10#some console. log(location.href) // https://juejin.im/timeline/frontend?a=10&b=10#some console. log(location.protocol) // https: console. log(location.pathname) // /timeline/frontend console. log(location.search) // ?a=10&b=10 console. log(location.hash) // #some 另外,還有呼叫瀏覽器的前進、後退功能等: history.back() history.forward() 獲取瀏覽器特性(即俗稱的UA)然後識別客戶端,例如判斷是不是 Chrome 瀏覽器: varua = navigator.userAgent varisChrome = ua.indexOf( 'Chrome') console.log(isChrome) 5、Ajax與跨域 Ajax 是一種非同步請求資料的一種技術,對於改善使用者的體驗和程式的效能很有幫助。 簡單地說,在不需要重新重新整理頁面的情況下,Ajax 通過非同步請求載入後臺資料,並在網頁上呈現出來。常見運用場景有表單驗證是否登入成功、百度搜索下拉框提示和快遞單號查詢等等。Ajax的目的是提高使用者體驗,較少網路資料的傳輸量。 如何手寫 不借助任何庫: varxhr = new() xhr.onreadystatechange = function() { // 這裡的函式非同步執行 if(xhr.readyState == 4) { if(xhr.status == 200) { alert(xhr.responseText) } } } xhr.open( "GET", "/api", false) xhr.send( null) 因為瀏覽器出於安全考慮,有同源策略。也就是說,如果協議、域名或者埠有一個不同就是跨域,Ajax 請求會失敗。 那麼是出於什麼安全考慮才會引入這種機制呢? 其實主要是用來防止 CSRF 攻擊的。簡單點說,CSRF 攻擊是利用使用者的登入態發起惡意請求。 然後我們來考慮一個問題,請求跨域了,那麼請求到底發出去沒有? 請求必然是發出去了,但是瀏覽器攔截了響應。 常見的幾種跨域解決方案有: 6、儲存 sessionStorage 、localStorage 和 cookie 之間的區別: 作用域:localStorage只要在相同的協議、相同的主機名、相同的埠下,就能讀取/修改到同一份localStorage資料。sessionStorage比localStorage更嚴苛一點,除了協議、主機名、埠外,還要求在同一視窗(也就是瀏覽器的標籤頁)下: 生命週期:localStorage 是持久化的本地儲存,儲存在其中的資料是永遠不會過期的,使其消失的唯一辦法是手動刪除;而 sessionStorage 是臨時性的本地儲存,它是會話級別的儲存,當會話結束(頁面被關閉)時,儲存內容也隨之被釋放。 模組化 幾種常見模組化規範的簡介: 作者簡介:浪裡行舟,碩士研究生,專注於前端,運營有個人公眾號:前端工匠,致力於打造適合初中級工程師能夠快速吸收的一系列優質文章。