DOM
DOM:文本對象模型,是HTML和XML文檔的編程接口。提供了對文檔的結構化的表述,并定義可一種方式可以使從程序中對該結構進行訪問,從而改變文檔的結構、樣式和內容。
DOM操作
- 創建節點:
document.createElement()
、document.createTextNode()
、document.createDocumentFragment()
(創建文檔碎片,表示一種輕量級的文檔,主要用來存儲臨時節點,然后把文檔碎片的內容一次性添加到DOM中)、document.createAttribute()
- 查詢節點:
querySelector()
、querySelectorAll()
、 - 更新節點:
innerHTML
、innerText
、textContent
- 添加節點:
innerHTML
、appendChild()
、insertBefore()
- 刪除節點:
removeChild()
如何判斷一個元素是否在可視區域內
- offsetTop、scrollTop
- getBoundingClientRect
- Intersection Observe
- offsetTop、scrollTop實現:
function isInViewPortOne(el) {const viewPortHeight = window.innerHeight || document.documentElement.clienHeight || document.body.clientHeightconst offsetTop = el.offsetTopconst scrollTop = document.documentElement.scrollTopconst top = offsetTop - scrollTopreturn top <= viewPortHeight
}
getBoundingClientRect()
實現
function isInViewPortOne(el) {const viewHeight = window.innerHeight || document.documentElement.clienHeight || document.body.clientHeightconst viewWidth = window.innerWidth || document.documentElement.clienWidth || document.body.clientWidthconst {top,right,bottom,left} = el.getBoundingClientRect()return (top > 0 && left > 0 && bottom > 0 && left > 0)
}
BOM
BOM:瀏覽器對象模型,頂級對象是
window
,表示瀏覽器的一個實例。
window對象
window.open(url, target)
window.close()
:僅用于關閉window.open()
打開的窗口;- 窗口操作方法:
- moveBy(x, y)
- moveTo(x, y):移動窗體左上角到相對于屏幕左上角的(x,y)點;
- resizeTo(x, y)
- resizeBy(x, y)
- scrollTo(x, y)
- scrollBy(x, y)
location對象
- location.hash:#后面的字符
- location.host:服務器名稱+端口號
- location.hostname:域名
- location.href:完整url
- location.port
- location.pathname:服務器下文件路徑
- location.protocol:使用協議
- location.search:url查詢字符串,
?
后的內容
navigator對象
- navigator.appName
- navigator.geolocation
- navigator.getUserMedia():可用媒體設備硬件關聯的流
- navigator.mediaDevices:可用的媒體設備
- …
screen對象
- screen.height
- screen.width
- screen.pixelDepth
- …
history對象
- history.go()
- history.back()
- history.forward()
- history.length
JS的數據類型
類型分類
- 基本類型:包含
Number
、String
、Boolean
、Null
(空對象指針,typeof
判斷時候會返回object
)、Undefined
、Symbol
; - 引用類型:包含
Object
、Array
、Function
(還有Date
、RegExp
、Map
、Set
等)。
存儲方式
基本數據類型存儲在棧中(讀取棧中數據),引用數據類型存儲在堆中(索引棧地址,讀取堆中數據)。
常見問題
- 聲明變量時不同的地址分配
- 基本數據類型的值存放在棧中,在棧中存放的是對應的值;
- 引用數據類型的值存放在堆中,在棧中存放的是指向堆內存的地址;
- 不同的類型數據導致賦值變量時的不同
- 基本數據類型賦值,是生成相同的值,兩個數據對應不同的地址;
- 引用數據類型賦值,是將保存對象的內存地址復制給另一個變量,兩個數據對應指向同一個堆內存中的地址;
類型轉換機制
類型轉換分類
- 顯式轉換:Number()、String()、parseInt()、Boolean()
- 隱式轉換
隱式轉換注意點
- 對象與基本類型數據比較:對象會先調用
valueOf
方法(如果valueOf
方法繼續返回對象,則調用toString
方法),嘗試得到一個原始值來進行比較; null
與undifined
比較:==
或者!=
時是相等的,其它情況下不等;NaN
的比較:與任何值都不等。(要檢查一個值是否是NaN
,應使用Number.isNaN()
函數)- 布爾值與數字或字符串比較:布爾值會先轉換為數字,然后按照數字與數字或者字符串比較規則比較。
- 兩個值都為引用類型,則比較他們是否指向同一個對象;
常見問題
Number(undefined)
// NaNparseInt('234sdf2')
// 234null == undefined
// true[] == ![]
// truetypeof null
//object
undefined
和null
與自身嚴格相等
數據類型檢測:typeof
和instanceof
typeof
操作符返回一個字符串,表示未經計算的操作數的類型。使用方法:typeof val
typeof
判斷引用類型數據,只能識別出function
,其它均為object
。
instanceof
用于檢測構造函數的prototype
屬性是否出現在某個實例對象的原型鏈上。使用方法:object instanceof constructor
。構造函數通過
new
可以實例對象,instanceof
能判斷這個對象是否是之前那個構造函數生成的對象。
typeof
和instanceof
用于判斷數據類型,均存在弊端。因此,通用的數據類型檢測方法為:Object.prototype.toString()
,該方法統一返回[object Xxx]
字符串。
數據類型檢測方法封裝
const getValType = val => {let type = typeof val;if(type !== 'object') return type;return Object.prototype.toString.call(val).replace(/^\[object (\S+)\]$/, '$1');
}
常用的字符串操作方法
- 增:
+
、${}
拼接以及concat()
等; - 刪(創建新的副本):
slice(start, end)
、substr(start, end)
、substring(start, num)
- 改(創建新的副本):
trim()
、toLowerCase()
、repeat()
等; - 查:
chatAt()
、indexOf()
、includes()
、strartWith()
等; - 轉換方法:
split()
- 模板匹配:
match()
、search()
、replace()
數組的常用操作方法
- 增:
push()
、unshift()
、splice()
、concat()
等 - 刪:
pop()
、shift()
、splice()
、slice()
等 - 改:
splice()
- 查:
indexOf()
、includes()
、find()
等 - 排序:
sort()
、reverse()
等 - 轉換:
join()
- 迭代:
some()
、map()
、filter()
、forEach
、every()
等
淺拷貝和深拷貝
淺拷貝和深拷貝的主要區別是在復制對象或數據結構時,拷貝的深度以及對原始數據內部結構的影響。
基本類型傳遞的是值,數據存儲在棧中,引用類型傳遞的是地址!數據存儲在堆中
- 淺拷貝:基本類型拷貝的是基本類型的值;引用類型拷貝:創建一個新對象,只復制原始對象的基本數據類型的字段或引用地址,不復制引用指向的對象,即新對象和原始對象數據均指向同一個引用對象,數據修改會相互影響;
Object.assigin()
slice()
/concat
- 拓展運算符
- 深拷貝:創建一個新對象,遞歸復制原始對象的所有字段和引用對象,即新對象和原始對象之間的數據相互獨立,復制后無直接影響關系。
JSON.stringfly()
:存在弊端,會忽略undefined、symbol和函數- 手寫循環遞歸
常見問題
- 手寫一個淺拷貝
const shallowCopyFn = obj => {let result = null;const type = Object.prototype.toSting.call(obj)// 創建一個新對象if(type === '[Object Object]') {result = {}} else if(Object === '[Object Array]'){result = []} else {result = obj}// 對象數據 基本類型和字段賦值for(let prop in obj) {if(obj.hasOwnProperty(prop)) {result[prop] = obj[prop]}}return result
}
淺拷貝或者可以使用Object.assign()
、ES6的展開運算符、concat()
等實現。
- 手寫一個深拷貝
function deepClone(obj, hash = new WeakMap()) { if (obj === null) return null; // null 的情況 if (obj instanceof Date) return new Date(obj); // 日期對象直接返回一個新的日期對象 if (obj instanceof RegExp) return new RegExp(obj); // 正則對象直接返回一個新的正則對象 if(typeof obj !== "object") return obj;// 如果循環引用了就用 weakMap 解決 if (hash.has(obj)) return hash.get(obj); let cloneObj = new obj.constructor; // 找到所屬類型原型上的constructorhash.set(obj, cloneObj); for (let key of obj) { if(obj.hasOwnProperty(key))cloneObj[key] = deepClone(obj[key], hash)} return cloneObj;
} // 示例
const original = { a: 1, b: { c: 2 }, d: [3, 4], e: new Date(), f: /abc/g, g: function() {} };
const cloned = deepClone(original);
console.log(cloned);
console.log(cloned === original); // false
console.log(cloned.b === original.b); // false
JS的數據結構
數據結構:計算機存儲、組織數據的方式。
分類
- 數組:連續的內存空間保存數據,保存的數據個數在內存分配的時候是確認的;
- 棧(Stack):先進后出(LIFO)的有序集合;
- 隊列(Queue):先進先出(FIFO)的有序集合;
- 堆(Heap)
- 鏈表:以鍵-值對存儲的數據結構;
- 字典
- 樹
- 圖
- 散列表:也稱為哈希表,特點是操作很快;
原型和原型鏈
原型
由于JS中只有對象沒有類(ES6之前),因此為了解決數據共享,引入了原型的概念。
原型其實就是一個普通對象,prototype
, 也稱為顯式原型,主要作用是為其它對象提供共享屬性。
- 只有構造函數才有原型;
- 公有屬性,可操作;
- 幾乎所有對象在創建的時候都會被賦予一個非空的值作為原型對象的引用;
隱式原型:__proto__
- 只有對象(普通對象、函數)具備;
- 私有的對象屬性,不可操作;
顯示原型
prototype
是構造函數才具備的,普通對象要調用構造函數的方法,就只能通過__proto__
。隱式原型全等于顯示原型,即__proto__ === prototype
。
常見問題
- Google中,隱式原型的寫法:
[[prototype]]
- 函數的原型是放在
prototype
上; - 對象、數組的原型是放在
__proto__
上; Object.getPrototypeof(obj)
:獲取val的原型對象
constructor、原型對象和函數實例三者間的關系
默認情況下,所有的函數的原型對象都會自動獲得一個名為
constructor
的屬性,指向與之關聯的構造函數。
function Person() {}let per = new Person()console.log(Person.prototype === per.__proto__) // true
console.log(Person.prototype.constructor === Person) // true
構造函數的原型和函數實例對象的原型是同一個對象。
常見問題
constructor
用于判斷類型:arr.constructor === Array
// true
原型鏈
原型鏈其實就是一條訪問鏈路,通過對象特有的原型構成的一種鏈式結構,用來繼承多個引用類型的屬性和方法。當試圖訪問一個對象的屬性時,就會在原型鏈上進行查找,默認情況下,終點就是最初原型對象的原型:
null
。
字符串。數組、構造函數的原型最終都會指向Object
,而Object
的原型指向是null
。
常見問題
__proto__ === prototype
prototype == {}
{}.__proto__ == Object.prototype[].__proto__ === Array.prototype
{}.__proto__ === [].__proto__.__proto__Person.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
總結
- 一切對象都是繼承自
Object
,Object
對象直接繼承根源對象null
; - 一切的函數對象,都是繼承自
Function
對象; Object
對象直接繼承自Function
對象;Function
對象的__proto__
會指向自己的原型對象,最終還是繼承自Object
。
繼承
在JS中,所有引用類型都繼承了Object
,而繼承也是通過原型鏈實現的,常見繼承方法有8種。
- 原型鏈繼承:把子類的原型指向父類構造函數實例來覆蓋子類原型對象。一般用于一個子類繼承的情況,避免屬性篡改影響。
function Parent(name) {this.name = namethis.hobby= ['吃飯','睡覺']
}
Parent.prototype.getInfo = _ => {console.log(this.name)console.log(this.hobby)
}
function Child() {}
Child.prototype = new Parent()const children1 = new Child()
children1.name = '222'
children1.hobby.push('唱歌')
children1.getInfo() // 輸出 222 ['吃飯','睡覺', '唱歌']const children2 = new Child()
children2.name = '333'
children2.getInfo() // 輸出 333['吃飯','睡覺', '唱歌']
- 構造函數繼承:在子類構造函數中調用父類構造函數,把
this
指向改變為子類對象(借助call
)。
注:父類的引用屬性不會被共享,即構造函數繼承,只能繼承父類的實例屬性和方法,不能繼承原型屬性和方法。
function Parent(name) {this.name = name;this.hobby = ['吃飯', '睡覺']
}
Parent.prototype.getName = function() {return this.name
}
function Child(name) {Parent.call(this, name)
}
const children1 = new Child('小明')
children1.hobby.push('唱歌')
console.log(children1.name + ', ' + children1.hobby) // 輸出:小明 ['吃飯','睡覺', '唱歌']const children2 = new Child()
children2.name = '小紅'
console.log(children2.name + ', ' + children2.hobby) // 輸出:小紅 ['吃飯','睡覺']
children2.getName() // 報錯
- 組合繼承:將原型鏈繼承和構造函數繼承結合。主要是使用原型鏈實現對原型屬性和方法的繼承,又通過借用構造函數實現對實例屬性的繼承。因此,組合式繼承一般情況下回調用兩次父類的構造函數。
function Parent(name) {this.name = name;this.hobby = ['吃飯', '睡覺']
}
Parent.prototype.getInfo = _ =>{console.log(this.name)console.log(this.hobby)
}
function Child(name, age) {Parent.call(this. name) // 第二次調用 this.age = age
}
// 繼承Parent的原型 第一次調用 繼承父類的屬性和方法
Child.prototype = new Parent()
// 修復構造函數的指向
Child.prototype.constructor = Child
// 添加自定義方法
Child.prototype.getAge = _=>{console.log(this.age)
}
注:在上述示例中,Child.prototype = new Parent()
(即第一次調用)實際上是不必要的,因為它會導致父類的構造函數被不必要的調用,從而繼承父類實例的所有屬性的方法,數據共享。更推薦的做法是使用Object.create(Parent.prototype)
來創建子類的原型對象,即寄生組合式繼承。
- 原型式繼承
ES5中新增了Object.create()
方法規范了原型式繼承。即實現一個對象的繼承,不比創建構造函數。
let Parent = {name: '132',hobby: ['吃飯', '睡覺'],getInfo() {console.log(this.name)console.log(this.hobby)}
}
const children1 = Object.create(Parent)
children1.name = '232'
children1.hobby.push('打游戲')
children1.getInfo() // 232 ['吃飯', '睡覺', '打游戲']const children2 = Object.create(Parent)
children2.getInfo() // 132 ['吃飯', '睡覺', '打游戲']
- ES6 extend class 關鍵字繼承
ES6繼承是一種語法糖,先將父類實例對象的屬性和方法,駕到this
上(super
使用),然后再用子類的構造函數修改this
。
class Parent() {constructor(name) {this.name = name}sayHello() {console.log(`hello, ${this.name}`)}
}
class Child extend Parent {constructor(name) {super(name);}
}
作用域和作用域鏈
作用域
作用域:即變量和函數有效的區域集合,變量作用域又稱為上下文。換句話說,就是代碼中國變量和其它資源的可見性。
分類
- 全局作用域
- 局部作用域(函數作用域)
- 塊級作用域:ES6中國引入了
let
和const
關鍵字,局部訪問。
作用域鏈
當使用一個變量時,js引擎會在當前作用域下尋找該變量,如果沒有找到,則向上查找,知道找到全局作用域下結束。
執行上下文與執行棧
this
對象
this
關鍵字是函數運行時自動生成的一個內部對象,只能在函數內部使用,總指向調用它的對象。
this
在函數執行過程中,一旦確定,就不可再更改。
綁定規則
- 默認綁定:嚴格模式下,不能講全局對象用于默認綁定,this會綁定到
undefined
; - 隱式綁定:
this
永遠指向最后調用它的對象; - new綁定:通過構建函數
new
關鍵字生成一個實例對象,此時this
指向這個實例對象(new
過程中如果返回一個對象,this
則指向返回的對象;如果返回一個簡單類型數據時,this
依舊指向實例對象); - 顯式修改:
apply(obj)
、call(obj)
、bind(obj)
是改變函數調用對象的方法。
綁定規則優先級
new
綁定 > 顯式綁定 > 隱式綁定 > 默認綁定
apply
、call
、bind
的區別
apply
、call
、bind
的作用都是改變函數執行時的上下文(即this
指向)。
const name = 'Li'
const obj = {name: 'Bo',sayHello() {console.log(this.name)}
}
obj.sayHello() // 輸出Bo
setTimeout(obj.sayHello, 0) // 輸出Li
上述示例中,由于使用了setTimeout
,在回調中執行obj.sayHello()
,因此在指向環境回到主棧執行時,實在全局執行上下文中的環境執行的,此時this
指向window
,因此輸出 Li,因此,需要調整綁定this
指向:
setTimeout(obj.sayHello.call(obj), 0)
三者區別
apply(obj, arrayArgs)
:參數以數組形式傳入,改變this
指向后原函數會立即執行,且此方法只是臨時改變一次。call(obj, listArgs)
:參數以列表形式傳入,改變this
指向后原函數會立即執行,且此方法只是臨時改變一次。bind(obj, listArgs)
:參數以列表形式傳入(可多次傳入)。改變this
指向后不會立即執行,而是返回一個永久改變this指向的函數。
三者相同點
- 三者第一個參數都是
this
要指向的對象,如果是null
、undefined
時,默認指向window
;
手動實現bind
Function.prototype.myBind = function (context) {// 判斷是否是函數if(typeof this !== "function") {throw new TypeError('error')}// 獲取參數const args = [...arguments].slice(1), fn = this;return function Fn () {// 根據調用方式 傳入不同的綁定值return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments))}
}
箭頭函數
ES6中提供了箭頭函數語法,在書寫時候就能確定this
指向。
箭頭函數不能作為構建函數。
閉包
在JS中,閉包是指一個函數能夠訪問并操作它外部的變量。閉包的創建主要是在嵌套函數中發生的。及時外部函數執行完畢并返回之后,內部函數依舊可訪問和修改這些變量,只是因為內部函數保持了對外部作用域的引用。
一般函數的詞法環境在函數返回后就被銷毀。但由于閉包保留了對所在詞法環境的數據引用,因此創建時所在執行上下文被銷毀,但創建所在詞法環境依然存在,延長了變量的生命周期。
示例:
const sumFn = _ => {let count = 0return function () {return count += 1}
}
// 創建兩個獨立的計數器
const sum1 = sumFun()
const sum2 = sumFn()
console.log(sum1()) // 1
console.log(sum1()) // 2
console.log(sum2()) // 1
-
閉包的特性
- 保持變量私有;
- 模擬素有方法;
- 實現封裝和抽象,使得代碼模塊化;
- 實現回調和異步操作;
-
閉包的用途
- 創建私有變量
- 數據封裝和隱私;
- 模擬類的私有方法和屬性;
- 實現函數工程;
-
閉包的缺點
- 內存消耗:由于閉包保持了對外部變量的引用,因此可能會造成內存消耗增加,甚至內存泄漏;
- 性能考慮:閉包可能會比普通函數的調用要稍慢一些。
-
其它
在創建新的對象或者類時,方法通常應該關聯于對象的原型,而不是定義在對象的構造器中,原因:每個對象的創建,方法都會被重新賦值。
function Obj(name, age) {this.name = namethis.age = age
}
Obj.prototype.getName = function() {return this.name
}
Obj.prototype.getAge = function() {return this.age
}
執行上下文
執行上下文就是代碼的執行環境。分為:
- 全局執行上下文:
window
- 函數執行上下文:只有在函數被調用的時候才會被創建
- Eval函數執行上下文:Eval函數中的代碼
生命周期
函數執行上下文的生命周期包括3個階段:
- 創建節點:確定
this
指向 - 執行階段:執行變量賦值、代碼執行(找不到值,則分配
undefined
) - 回收階段:執行上下文出棧,等待虛擬機回收執行上下文
變量提升
變量提升:由執行上下文和作用域的工作原理決定的。在JS代碼執行前,解析器會首先解析代碼,找出所有的變量聲明(
var
關鍵字聲明),然后在執行之前,講這些變量提升在其所在作用域的最頂端,這個過程就是變量提升。
變量提升的實際原因
- 編譯階段與執行階段分離:編譯階段,代碼解析,變量和函數聲明被提升到作用域頂部;執行階段,代碼按照編寫順序執行;
- 作用域決定:在JS中,作用域由函數決定;
注意
- 只有聲明本身會被提升,賦值或者其它邏輯操作依舊在原處執行;
- 使用
let
和const
關鍵字聲明的變量不會被提升(ES6處理);
執行棧(調用棧 - 先進后出結構)
執行棧:用于存儲代碼執行階段創建的所有執行上下文。先進后出,從上到下,創建對應的函數執行上下文冰牙人員棧,執行完成被推出,直至結束。
new
操作符具體實現
在JS中,new
操作符用于創建一個給定構造函數的實例對象。
流程實現
- 創建一個新的
object
- 將對象與構造函數通過原型鏈連接起來
- 將構造函數中的
this
綁定道新建的對象object
上 - 根據構建函數返回類型做判斷:原始值責備忽略,對象則需要正常處理使用。
function Person(name, age) {this.name = namethis.age = age
}
const per = new Person('haa', 33)
// 構造函數沒有return語句 則將新創建的對象返回
console.log(per) //Person {name: 'haa', age: 33}
手寫new操作符
function NNew(Func, ...args) {const obj = {}// 將新對象原型指向構造函數原型對象obj.__proto__ = Func.prototypelet res = Func.apply(obj, args)returm res instanceof Object ? res : obj
}
JS的事件模型
事件:HTML文檔或瀏覽器中發生的一種交互操作;
事件流:父子節點事件綁定,觸發順序
事件流
事件流的三個階段
- 事件捕獲階段:從上到下依次觸發;
- 處于目標階段:
- 事件冒泡階段:**從下(觸發節點)往上(DOM的最高層父節點)**的傳播方式。
事件模型
分類
- 原始事件模型(DOM0級)
- 綁定速度快
- 只支持冒泡,不支持捕獲
- 同一個事件類型只能綁定一次,后綁定會覆蓋之前的;
- 刪除事件處理,賦值為
null
即可
- 標準事件模型(DOM2級)
標準事件模型中,一次事件共有三個過程:
- 事件捕獲:從
document
一直向下傳播到目標元素- 事件處理:觸發目標元素的監聽元素
- 事件冒泡:從目標元素冒泡到
document
,依次檢查和執行相關的監聽函數
添加事件監聽:
addEventListener(evt, handler, userCapture)
移除事件監聽:removeEventListener(evt, handler, userCapture)
useCapture
默認為false
,表示在冒泡過程中執行,設置為true
,表示在捕獲過程中執行。
- IE事件模型
IE事件模型共有兩個過程:
- 事件處理階段:事件到達目標元素,觸發監聽函數;
- 事件冒泡階段:冒泡到
document
,過程中檢查和執行相關監聽函數;
添加事件監聽:
attachEvent(evt, handler)
移除事件監聽:detachEvent(evt, handler)
事件代理(事件委托)
事件代理,就是把一個或一組元素的響應事件的函數委托給外層的元素完成,其在冒泡階段完成。
應用場景
通常用于動態綁定,減少重復工作。列表項操作(增、刪、改、查)示例:
const ulDom = document.getElementById('ulDom')
ulDom.onclick = function (evt) {evt = evt || window.eventconst target = evt.target || evt.srcElemnentif(target.nodeName.toLowerCase() === 'li') {console.log(target.innerText)}
}
事件委托的局限性
focus
、bour
等沒有冒泡機制的事件,無法使用;mousemove
、mouseout
這些只能不斷通過位置計算定位的,對性能消耗高,不適合事件委托;
事件循環
JS是一門單線程語言,實現單線程非阻塞的方法就是事件循環。主要實現:同步任務進入主線程(主執行棧),異步任務進入任務隊列(先進先出)。主線程內的任務執行完畢為空,則會去任務隊列讀取對應的任務,推入主線程執行,不斷重復。
在JS中,所有的任務都可以分為:
- 同步任務:一般會直接進入主線程執行;
- 異步任務:比如
ajax
請求、定時器函數等。
宏任務與微任務
微任務
微任務:一個需要異步執行的函數,執行時機:主函數執行結束之后、當前宏任務執行結束之前。
常見微任務
Promise.then()
MutationOberserve
Proxy
nextTick()
宏任務
宏任務的時間粒度比較大,執行事件間隔是不能精確控制的。
常見宏任務
- script
setTimeout
、setInterval
- UI事件
postMessage
、MessageChanel
setImmediate
、I/O等
關系圖
按照關系圖,執行機制:
- 執行一個宏任務,如果遇到微任務則先將它推入微任務的事件隊列中;
- 當前宏任務執行完,查看微任務的事件隊列,將里面的任務依次執行,再繼續執行下一個宏任務。
示例:
console.log(1)setTimeout(_=>{console,log(2)
}, 0)new Promise((res, rej) => {console.log('Promise')resolve()
}).then(_=>{console.log('then')
})
console.log(3)
// 執行后 輸出
1
Promise
3
then
2 // setTimeout術語新的宏任務,所以要等上一個微任務列表執行完后再執行
async
和 await
async
:異步的意思,await
可以理解為async await
,即等待異步方法執行,阻塞后面的代碼。
async
async
函數返回一個Promise
對象。以下示例,兩種實現是等效的:
function fnc() {return Promise.resolve('123')
}async function() {return '123'
}
await
正常情況下,await
后是一個Promise
對象,如果不是,則直接返回對應的值。函數運行,遇到await
,則會阻塞下面的代碼(加入微任務列表),跳出去執行同步代碼。
async function func() {return await 123
}
func().then(val => console.log(val)) // 123
函數緩存
函數緩存,就是將函數運算過的結果進行緩存。
實現函數緩存主要依靠閉包、科利華、高階函數等。
柯里化:把接受多個參數的函數轉換為接受一個單一參數的函數
const add = (x) => {return function (y) {return x+ y}
}
// 使用
add(2)(3) // 5
JS本地存儲
JS的本地存儲方式主要有:
- cookie
- sessionStorage
- localStorage
- indexedDB
Cookie
cookie,類型為小型文本文件,指某些網站為了辨別用戶身份而存儲在用戶本地終端上的數據。是為了解決HTTP無狀態導致的問題。
cookie一般大小不超過4KB,由key-value形式存儲,還包含一些有效期、安全性、適用范圍的可選屬性。
Cookie每次請求都會被發送。修改Cookie,必須保證Domain和Path的值相同。刪除Cookie,一般通過設置一個過期時間,使其從瀏覽器上刪除。
Expires
:過期時間Domain
:主機名Path
:要求請求的資源路徑必須帶上這個URL路徑,才可以發送Cookie受不- 標記為
Secure
的cookie只通過HTTPS協議加密的請求發送給服務端。
localStorage
特點
- 持久化存儲,除非主動刪除,否則不會過期;
- 存儲信息在同一域下是共享的;
- 當前頁進行 localStorage的增刪改的時候,本頁不會觸發
storage
事件,只會在其它頁面觸發; - 大小:5M;
- 本質上就是字符串的讀取,內容過多會消耗內存空間,導致頁面卡頓;
- 受同源策略的限制
使用
localStorage.setItem(key, value)
localStorage.getItem(key)
localStorage.key(num)
:獲取第num個鍵名localStorage.removeItem(key)
localStorage.clear()
sessionStorage
特點
- 會話級別存儲,關閉頁面則自動清除數據;
- 存儲信息在同一域下是共享的;
- 當前頁進行 sessionStorage的增刪改的時候,本頁不會觸發
storage
事件,只會在其它頁面觸發; - 大小:5M;
- 本質上就是字符串的讀取,內容過多會消耗內存空間,導致頁面卡頓;
- 受同源策略的限制
使用
sessionStorage.setItem(key, value)
sessionStorage.getItem(key)
sessionStorage.key(num)
:獲取第num個鍵名sessionStorage.removeItem(key)
sessionStorage.clear()
應用場景
- 標記用戶與跟蹤用戶行為,使用
cookie
- 適合長期保存在本地的數據(令牌),使用
localStorage
- 敏感賬號一次性登錄,使用
sessionStorage
- 存儲大量數據、在線文檔保存編輯歷史的情況,使用
indexDB
大文件的斷點續傳
分片上傳
分片上傳,就是將上傳的文件,按照一定的大小等分割規則,將整個文件分割成多個數據塊(chunk),來進行分片上傳,上傳完成后,再由服務端對所有分片進行匯總整合拼接,生成原始的文件。
斷點續傳
斷點續傳,就是在上傳或下載時,將上傳或下載任務人為的劃分為幾個部分。每一個部分采用一個線程來進行。如果遇到網絡故障,可以從已經完成的部分處開始繼續上傳或下載未完成的部分。節省時間,提高速度。
實現的兩種方式:
- 服務端返回,告訴從哪開始;
- 瀏覽器端自行處理。
上傳或下載過程中在服務器謝偉臨時文件,處理完成后,再將此文件重命名為正式文件即可。
實現思路
拿到文件,保存文件唯一標識,切割,分段上傳。每次上傳一段,根據唯一標識判斷此次上傳進度,直到全部文件上傳完畢。對于上傳失敗、上傳過程中刷星頁面等情況,處理的一種常見方法是使用瀏覽器的存儲機制(如localStorage、sessionStorage、IndexedDB或Cookies)來保存上傳進度和已上傳的文件塊信息。
function uploadFile(file, chunkSize = 1024 * 1024) { const totalChunks = Math.ceil(file.size / chunkSize); for (let index = 0; index < totalChunks; index++) { const chunk = file.slice(index * chunkSize, (index + 1) * chunkSize); const formData = new FormData(); formData.append('file', chunk); formData.append('index', index); formData.append('totalChunks', totalChunks); fetch('/upload', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { console.log('Chunk uploaded', data); }) .catch(error => { console.error('Error uploading chunk:', error); // 保存已上傳的塊信息 }); }
}
使用場景
- 大文件加速上傳
- 流式文件上傳
- 網絡環境不太行
使用Web worker處理大文件上傳
- 新建 fileUploader.js文件(web worker)
self.onmessage = function(e) { const { file, chunkSize } = e.data; const totalChunks = Math.ceil(file.size / chunkSize); for (let index = 0; index < totalChunks; index++) { const chunk = file.slice(index * chunkSize, (index + 1) * chunkSize); const formData = new FormData(); formData.append('file', chunk); formData.append('index', index); formData.append('totalChunks', totalChunks); // 假設你有一個上傳函數 uploadChunk(formData, index).then(() => { // 通知主線程該塊已上傳 self.postMessage({ index: index, status: 'success' }); }).catch(error => { // 通知主線程上傳失敗 self.postMessage({ index: index, status: 'error', error: error.message }); }); } // 假設的上傳函數(需要替換為實際的API調用) function uploadChunk(formData, index) { return new Promise((resolve, reject) => { // 使用fetch或其他HTTP客戶端發送formData // 這里只是模擬 setTimeout(() => { if (Math.random() > 0.5) { resolve(); } else { reject(new Error('Upload failed for chunk ' + index)); } }, 1000); }); }
};
主文件(index.js)中,創建Worker,并將文件數據和其它必要數據發送給Worker:
if (window.Worker) { const worker = new Worker('fileUploader.js'); // 假設你有一個文件輸入元素 const fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener('change', function(e) { const file = e.target.files[0]; const chunkSize = 1024 * 1024; // 1MB // 發送文件和塊大小到Worker worker.postMessage({ file: file, chunkSize: chunkSize }); // 監聽來自Worker的消息 worker.onmessage = function(e) { console.log('Chunk ' + e.data.index + ' uploaded with status: ' + e.data.status); if (e.data.status === 'error') { console.error('Error:', e.data.error); } }; // 監聽Worker的錯誤 worker.onerror = function(error) { console.error('Worker error:', error); }; });
} else { console.log('Your browser doesn\'t support web workers.');
}
ajax
Ajax:即異步的JS和XML,可以在不重新加載整個網頁的情況下,與服務器交換數據,并且更新部分網頁。
Ajax的原理:通過
XMLHttpRequest
對象向服務器發送異步請求,從服務器獲取數據,然后用JS來操作DOM而更新頁面。
創建Ajax異步交互需要服務器邏輯進行配合,完成如下步驟:
- 創建 Ajax 的核心對象:
XMLHttpRequest
; - 通過
XMLHttpRequest
對象的open()
方法與服務器建立連接; - 構建請求所需的數據內容,并通過
XMLHttpRequest
對象的send
方法發送給服務器端 - 通過
XMLHttpRequest
對象提供的onreadystatechange
事件監聽服務器端的通信狀態(XMLHttpRequest.readyState
) - 接受并處理服務器向客戶端響應的數據結果
- 將處理結果更新到HTML中
封裝:
function ajaxReq (options) {options = options || {}options.type = (options.type || 'GET').toUpperCase()options.dataType = options.dataType || 'json'const datas = options.dataconst xhr = new XMLHttpRequest()if(options.type === 'GET') {xhr.open('GET', `${options.url}?${params}`, true)xhr.send()} else if(options.type === 'POST'){xhr.open('POST', options.url, true)xhr.send(params)}// 監聽服務器的通信狀態xhr.onreadychange = function(e) {if(xhr.readyState === 4) { // 請求完成if(xhr.status >=200 && xhr.status < 300) {options.success && options.success(xhr.responseText, xhr.responseXML)} else {options.fail && options.fail(xhr.status)}}}
}
// 使用
ajaxReq({type: 'get',datas: {id: 1},url:'https:xxx',success: (text, xml) => {console.log(text)},fail: status => {console.log(status)}
})
防抖和節流
防抖和節流本質上就是優化高頻率執行代碼的一種手段,減少調用頻率,優化體驗。
- 防抖(debounce):n秒后再執行該事件,如在n秒內被重復觸發,則重新計時(示例:電梯關門,計時關門)。確保事件處理函數在最后一次事件觸發后的一段時間內才執行,通常用于用戶輸入、驗證碼輸入驗證等場景;
- 節流(throttle):n秒內只運行一次,如在n秒內重復觸發,只有一次生效(示例:火車發車,要準點)。確保事件處理函數在固定時間間隔內只執行一次,通常應用于滾動、搜索聯想等;
代碼實現
- 防抖:n秒后執行
function debounce(func, wait, immediate) {let timeout;return function (...args) {let context = thisif(timeout) clearTimeout(timeout)if(immediate) {let callNow = !timeout // 第一次會立即執行,后續觸發才執行timeout = setTimeout(function() {timeout = null}, wait)if(callNow) {func.apply(context, args)}} else {timeout = setTimeout(function () {func.apply(context, args)}, wait)}}
}
- 節流:n秒內僅執行一次
function throttled(fn, delay = 500) {let timer = nullreturn function(...args) {if(!timer) {timer = setTimeout(_ => {fn.apply(this, args)timer = null}, delay)}}
}
單點登錄
單點登錄,是目前比較流行的企業業務整合的解決方案之一。
SSO的定義是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統。
SSO一般需要一個獨立的認證中心,子系統的登錄均需要通過認證中心,本身不參與登錄操作。
當一個系統成功登錄以后,認證中心會頒發一個令牌給各個子系統,子系統拿著令牌去獲取各自受保護的資源。為了減少頻繁驗證,一般在授權以后,一定時間內無需再發起認證。
實現方式
- 同域名下的單點登錄:使用Cookie,即將Cookie的
domain
設置為當前域的父域,并且服務的Cookie會被子域共享。path
默認為web應用的上下文路徑。 - 不同域名下的單點登錄(兩種方法均支持跨域):
- 方法一:認證中心進行登錄,登錄成功,將token寫入Cookie。應用系統檢查當前請求是否有token,沒有則跳轉登錄,有則將當前token寫入當前應用系統的Cookie,訪問放行。
- 方法二:將認證中心的token保存在localStorage中,前端每次請求,都主動將localStorage的數據傳給服務端。前端拿到token后,除了寫入自己的localStorage中,還可以通過特殊手段寫入其他域的localStorage中(iframe + postMessage)。
上拉加載、下拉刷新
上拉加載的本質就是頁面觸底時機,自動觸發加載請求。
頁面觸底公式:
const scrollTop = document.documentElement.scrollTop
const scrollHeight = document.body.scrollHeight
const clientHeight = document.documentElement.clientHeight // 瀏覽器高度// 距離底部還有50的時候就可以開始觸發
const isPut = (scrollTop + clientHeight) >= (scrollHeight - 50)
下拉刷新的本質就是頁面本身置于頂部時,用戶下拉觸發操作。
正則表達式
遇到特殊字符,需要使用\
轉義哦~
構建正則表達式的方式:
- 字面量創建,其包含在斜杠之間:
/\d+/g
- 調用
RegExp
對象的構造函數:new RegExp("\\d+", g)
匹配規則
- ^:匹配輸入開始
- $:匹配輸入結束
- *:匹配前一個表達式0次或毒刺
- +:匹配前一個表達式1次或多次,等價于
{1,}
- ?:匹配前一個表達式0次或1次,等價于
${0, 1}
- .:匹配除換行符意外的任何單個字符
- …
標記
- g:全局搜索
- i:不區分大小寫搜索
- m:多行搜索
- …
匹配方法
- 字符串方法:
match()
、matchAll()
、search()
、replace()
、split()
- 正則對象方法:
test()
、exec()
常用正則
- 5-20個字符,以字母開頭,可帶數字,以及_、.的字符串:
/^[a-zA-Z]{1}([a-zA-Z0-9]|[._]){4, 19}$/
函數式編程
純函數
純函數就是對給定的輸入返回相同輸出的函數,并要求所有的數據都是不可變的,即:純函數=無狀態+數據不可變
高階函數
高階函數,就是以函數作為輸入或者輸出的函數。
高階函數存在緩存的特性,主要是利用了閉包作用。
const doOnce = fn => {let done = falsereturn function() {if(!done) fn.apply(this. fn)else console.log('已處理')done = true}
}
柯里化
柯里化就是把一個多參數函數轉換為一個嵌套的一元函數的過程(惰性執行)。
web攻擊方式
常見攻擊方式:
-
SQL注入:在表單的輸入框中輸入惡意SQL代碼,并通過提交這些字段來執行惡意的SQL語句,從而影響網站的數據庫安全。
SQL注入,主要是通過將惡意的Sql查詢或添加語句插入到應用的輸入參數中,再在后臺Sql服務器上解析執行進行的攻擊。
SQL注入預防:
- 嚴格檢查輸入變量的類型和格式;
- 過濾和轉義特殊字符;
- 對訪問數據庫的Web應用采用防火墻等。
-
跨站腳本攻擊(XSS):在Web應用中輸入惡意指令代碼到網頁,使用戶加載并執行攻擊者惡意制造的網頁程序。
分類:
- 存儲型:惡意數據提交,讀取返回,解析執行
- 反射型:包含惡意代碼的URL,讀取、拼接返回給HTML,解析執行
- DOM型:包含惡意代碼的URL,前端讀取打開(前端自身漏洞)
解決辦法
- 在使用innerHTML、outerHTML、document.write()等小心,不要把不可信的HTML插入到頁面上。如果使用Vue/react等技術棧,應在
render
階段避免 innerHTML 的xss攻擊隱患; - DOM中的內聯事件監聽器,如onclick、onload、onmousemove等,a標簽的href屬性,setTimeout()等,都能把字符串作為代碼運行。如果吧不可信的數據拼接到字符串中傳遞給這些API,很容易產生隱患;
-
跨站請求偽造(CSRF):攻擊者優導受害者進入第三方網站,在第三方網站中,向被攻擊網站發起跨站請求。
跨站請求偽造,可以通過
get
請求,即通過訪問img的頁面后,瀏覽器自動訪問目標地址,發送請求。也可以設置一個自動提交的表單發送POST
請求。CSRF的特點:
- 攻擊一般發起在第三方網站;
- 攻擊利用受害者在被攻擊網站的登錄憑證,冒充受害者提交操作;
- 跨站請求可以用各種方式:圖片URL、超鏈接、Form表單提交等;部分請求方式可以直接嵌入第三方論壇、文章中,難以追蹤。
CSRF的預防:
- 阻止不明外域的訪問;
- 提交時要求附加本域才能獲取的信息(token等);
-
文件上傳漏洞:攻擊者可能會利用文件上傳功能將惡意軟件或腳本上傳到目標服務器上,進而執行惡意操作或獲取敏感信息。
-
遠程命令執行漏洞:攻擊者可利用該漏洞在受害機上執行任意命令或程序,進一步控制受害機器。
-
目錄遍歷攻擊:攻擊者試圖訪問服務器根目錄之外的目錄,并利用特定的符號(如“…/”)來嘗試訪問受限制的目錄或文件。
-
信息泄露攻擊:攻擊者可能試圖通過各種方法獲取或猜測敏感信息,如用戶密碼、密鑰等,以便進一步入侵系統。
-
會話劫持:攻擊者利用各種技術手段捕獲會話信息,然后冒充合法用戶進行非法操作。
-
零日攻擊:黑客利用尚未被公眾發現的漏洞信息進行攻擊,使得目標系統無法防范。
JS內存泄漏
JS的內存泄漏,是指計算處理中,由于疏忽或者錯誤造成程序未能釋放已經不在使用的內存,從而造成內存的浪費。
對于持續運行的服務進程,必須及時釋放不再用到的內存,否則,內存占用越來越高,影響系統性能,甚至導致程序崩潰。
垃圾回收機制
JS具有自動垃圾回收機制,執行環境會負責管理代碼執行過程中使用的內存。
原理:垃圾收集器會定期找出不再繼續使用的變量,然后釋放內存。
實現方式:
- 標記清除:變量標記進入和離開執行環境,垃圾回收程序會將離開狀態,且無被引用的變量做清理銷毀‘’
- 引用計數:如果一個值的引用次數為0,就表示這個值不再用了,可以將此銷毀釋放。
常見內存泄漏情況
- 意外的全局變量(使用嚴格模式可解決)
- 定時器
- 閉包
- 監聽器:
addEventListener
JS數字精度丟失
0.1 + 0.3 === 0.4 // false
存儲二進制小數點的偏移量最大為52位,最多可以表達的位數是2^53=90071992547740992,對應科學計數位數是9.0071992547740992,這也是JS最多能表示的精度。他的長度是16,所以可以使用toPrecision(16)
來做運算,超過的精度會自動做湊整處理。
要想解決大數的問題,使用第三方庫:bignumber.js
,原理是把所有數字當做字符串,重新實現了計算邏輯,缺點就是性能差。
因此,0.1 + 0.3 === 0.4
為false
,主要是因為計算機存儲雙精度浮點數需要先把十進制數轉換為二進制的科學計數法的形式,然后計算機以自己的規則存儲二進制的科學計數法。
由于存儲時有位數限制(64位),并且某些十進制浮點數在轉換為二進制數時會出現無線循環,造成二進制的舍入操作,當再轉換為十進制,就造成了計算誤差。
解決方案
使用toPrecision()
湊整,并parseFloat()
轉換為數字后顯示
function strip(num, precision = 12) {return +parseFloat(num.toPrecision(precision))
}
或者直接使用第三方庫:Math.js
、BigDecimal.js
等
尾遞歸
數組求和
const sum = (arr, total) => {if(arr.lengh === 1) return totalreturn sum(arr, total + arr.pop())
}
數組扁平化
const flatArr = (arr=[], result=[]) =>{arr.forEach(val => {if(Arrar.isArray(val)) {result = result.concat(flat(val, []))} else result.push(val)})
}