本文將從以下十一個維度為讀者總結(jié)前端基礎(chǔ)知識
JS基礎(chǔ)
-
如何在ES5環(huán)境下實現(xiàn)let
對于這個問題腰素,我們可以直接查看 babel 轉(zhuǎn)換前后的結(jié)果皮钠,看一下在循環(huán)中通過 let 定義的變量是如何解決變量提升的問題
babel在let定義的變量前加了道下劃線秩彤,避免在塊級作用域外訪問到該變量咧纠,除了對變量名的轉(zhuǎn)換钝吮,我們也可以通過自執(zhí)行函數(shù)來模擬塊級作用域
(function(){
for(var i = 0; i < 5; i ++){
console.log(i) // 0 1 2 3 4
}
})();
console.log(i) // Uncaught ReferenceError: i is not defined
- 如何在ES5環(huán)境下實現(xiàn)const
實現(xiàn)const的關(guān)鍵在于 Object.defineProperty() 這個API迷雪,這個API用于在一個對象上增加或修改屬性。通過配置屬性描述符垄潮,可以精確地控制屬性行為烹卒。 Object.defineProperty() 接收三個參數(shù):
Object.defineProperty(obj, prop, desc)
對于const不可修改的特性,我們通過設(shè)置writable屬性來實現(xiàn)
function _const(key, value) {
const desc = {
value,
writable: false
}
Object.defineProperty(window, key, desc)
}
_const('obj', {a: 1}) //定義obj
obj.b = 2 //可以正常給obj的屬性賦值
obj = {} //拋出錯誤弯洗,提示對象read-only
參考資料: 如何在 ES5 環(huán)境下實現(xiàn)一個const 旅急? [3]
- 手寫call()
call() 方法 使用一個指定的 this 值和單獨給出的一個或多個參數(shù)來調(diào)用一個函數(shù)
語法: function.call(thisArg, arg1, arg2, ...)
call() 的原理比較簡單,由于函數(shù)的this指向它的直接調(diào)用者牡整,我們變更調(diào)用者即完成this指向的變更:
/變更函數(shù)調(diào)用者示例
function foo() {
console.log(this.name)
}
// 測試
const obj = {
name: '寫代碼像蔡徐抻'
}
obj.foo = foo // 變更foo的調(diào)用者
obj.foo() // '寫代碼像蔡徐抻'
基于以上原理, 我們兩句代碼就能實現(xiàn)call()
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this // this指向調(diào)用call的對象,即我們要改變this指向的函數(shù)
return thisArg.fn(...args) // 執(zhí)行函數(shù)并return其執(zhí)行結(jié)果
}
但是我們有一些細節(jié)需要處理:
Function.prototype.myCall = function(thisArg, ...args) {
if(typeof this !== 'function') {
throw new TypeError('error')
}
const fn = Symbol('fn') // 聲明一個獨有的Symbol屬性, 防止fn覆蓋已有屬性
thisArg = thisArg || window // 若沒有傳入this, 默認綁定window對象
thisArg[fn] = this // this指向調(diào)用call的對象,即我們要改變this指向的函數(shù)
const result = thisArg[fn](...args) // 執(zhí)行當前函數(shù)
delete thisArg[fn] // 刪除我們聲明的fn屬性
return result // 返回函數(shù)執(zhí)行結(jié)果
}
//測試
foo.myCall(obj) // 輸出'寫代碼像蔡徐抻'
- 手寫apply()
apply() 方法調(diào)用一個具有給定this值的函數(shù)藐吮,以及作為一個數(shù)組(或類似數(shù)組對象)提供的參數(shù)。
語法:func.apply(thisArg, [argsArray])
apply() 和 call() 類似逃贝,區(qū)別在于call()接收參數(shù)列表谣辞,而apply()接收一個參數(shù)數(shù)組,所以我們在call()的實現(xiàn)上簡單改一下入?yún)⑿问郊纯?/p>
Function.prototype.myApply = function(thisArg, args) {
if(typeof this !== 'function') {
throw new TypeError('error')
}
const fn = Symbol('fn') // 聲明一個獨有的Symbol屬性, 防止fn覆蓋已有屬性
thisArg = thisArg || window // 若沒有傳入this, 默認綁定window對象
thisArg[fn] = this // this指向調(diào)用call的對象,即我們要改變this指向的函數(shù)
const result = thisArg[fn](...args) // 執(zhí)行當前函數(shù)
delete thisArg[fn] // 刪除我們聲明的fn屬性
return result // 返回函數(shù)執(zhí)行結(jié)果
}
//測試
foo.myApply(obj, []) // 輸出'寫代碼像蔡徐抻'
- 手寫bind()
bind() 方法創(chuàng)建一個新的函數(shù)沐扳,在 bind() 被調(diào)用時潦闲,這個新函數(shù)的 this 被指定為 bind() 的第一個參數(shù),而其余參數(shù)將作為新函數(shù)的參數(shù)迫皱,供調(diào)用時使用。
語法: function.bind(thisArg, arg1, arg2, ...)
從用法上看,似乎給call/apply包一層function就實現(xiàn)了bind():
Function.prototype.myBind = function(thisArg, ...args) {
return () => {
this.apply(thisArg, args)
}
}
但我們忽略了三點:
- bind()除了this還接收其他參數(shù)卓起,bind()返回的函數(shù)也接收參數(shù)和敬,這兩部分的參數(shù)都要傳給返回的函數(shù) 2. new的優(yōu)先級:如果bind綁定后的函數(shù)被new了,那么此時this指向就發(fā)生改變戏阅。此時的this就是當前函數(shù)的實例 3. 沒有保留原函數(shù)在原型鏈上的屬性和方法
Function.prototype.myBind = function (thisArg, ...args) {
if (typeof this !== "function") {
throw TypeError("Bind must be called on a function")
}
var self = this
// new優(yōu)先級
var fbound = function () {
self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
}
// 繼承原型上的屬性和方法
fbound.prototype = Object.create(self.prototype);
return fbound;
}
//測試
const obj = { name: '寫代碼像蔡徐抻' }
function foo() {
console.log(this.name)
console.log(arguments)
}
foo.myBind(obj, 'a', 'b', 'c')() //輸出寫代碼像蔡徐抻 ['a', 'b', 'c']
- 手寫一個防抖函數(shù)
防抖和節(jié)流的概念都比較簡單昼弟,所以我們就不在“防抖節(jié)流是什么”這個問題上浪費過多篇幅了,簡單點一下:
防抖奕筐,即 短時間內(nèi)大量觸發(fā)同一事件舱痘,只會執(zhí)行一次函數(shù) ,實現(xiàn)原理為 設(shè)置一個定時器离赫,約定在xx毫秒后再觸發(fā)事件處理芭逝,每次觸發(fā)事件都會重新設(shè)置計時器,直到xx毫秒內(nèi)無第二次操作 渊胸,防抖常用于搜索框/滾動條的監(jiān)聽事件處理旬盯,如果不做防抖,每輸入一個字/滾動屏幕翎猛,都會觸發(fā)事件處理胖翰,造成性能浪費。
function debounce(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
- 手寫一個節(jié)流函數(shù)
防抖是 延遲執(zhí)行 切厘,而節(jié)流是 間隔執(zhí)行 萨咳,函數(shù)節(jié)流即 每隔一段時間就執(zhí)行一次 ,實現(xiàn)原理為 設(shè)置一個定時器疫稿,約定xx毫秒后執(zhí)行事件培他,如果時間到了,那么執(zhí)行函數(shù)并重置定時器 而克,和防抖的區(qū)別在于靶壮,防抖每次觸發(fā)事件都重置定時器,而節(jié)流在定時器到時間后再清空定時器
function throttle(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
實現(xiàn)方式2:使用兩個時間戳
prev舊時間戳
和now新時間戳
员萍,每次觸發(fā)事件都判斷二者的時間差腾降,如果到達規(guī)定時間,執(zhí)行函數(shù)并重置舊時間戳
function throttle(func, wait) {
var prev = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - prev > wait) {
func.apply(context, args);
prev = now;
}
}
}
- 數(shù)組扁平化
對于 [1, [1,2], [1,2,3]] 這樣多層嵌套的數(shù)組碎绎,我們?nèi)绾螌⑵浔馄交癁? [1, 1, 2, 1, 2, 3] 這樣的一維數(shù)組呢:
1.ES6的flat()
const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity) // [1, 1, 2, 1, 2, 3]
2.序列化后正則
const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str) // [1, 1, 2, 1, 2, 3]
3.遞歸
對于樹狀結(jié)構(gòu)的數(shù)據(jù)螃壤,最直接的處理方式就是遞歸
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
let result = []
for (const item of arr) {
item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
}
return result
}
flat(arr) // [1, 1, 2, 1, 2, 3]
4.reduce()遞歸
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
return arr.reduce((prev, cur) => {
return prev.concat(cur instanceof Array ? flat(cur) : cur)
}, [])
}
flat(arr) // [1, 1, 2, 1, 2, 3]
5.迭代+展開運算符
let arr = [1, [1,2], [1,2,3]]
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
console.log(arr) // [1, 1, 2, 1, 2, 3]
- 手寫一個Promise
實現(xiàn)一個符合規(guī)范的Promise篇幅比較長,建議閱讀筆者上一篇文章: 異步編程二三事 | Promise/async/Generator實現(xiàn)原理解析 | 9k字 [4]
JS面向?qū)ο?br> 在JS中一切皆對象筋帖,但JS并不是一種真正的面向?qū)ο?OOP)的語言奸晴,因為它缺少 類(class) 的概念。雖然ES6引入了 class 和 extends 日麸,使我們能夠輕易地實現(xiàn)類和繼承寄啼。但JS并不存在真實的類逮光,JS的類是通過函數(shù)以及原型鏈機制模擬的,本小節(jié)的就來探究如何在ES5環(huán)境下利用函數(shù)和原型鏈實現(xiàn)JS面向?qū)ο蟮奶匦?/p>
在開始之前墩划,我們先回顧一下原型鏈的知識涕刚,后續(xù) new 和 繼承 等實現(xiàn)都是基于原型鏈機制。很多介紹原型鏈的資料都能寫上洋洋灑灑幾千字乙帮,但我覺得讀者們不需要把原型鏈想太復(fù)雜杜漠,容易把自己繞進去,其實在我看來察净,原型鏈的核心只需要記住三點:
-
每個對象都有 proto屬性 驾茴,該屬性指向其原型對象,在調(diào)用實例的方法和屬性時氢卡,如果在實例對象上找不到锈至,就會往原型對象上找 2. 構(gòu)造函數(shù)的 prototype屬性 也指向?qū)嵗脑蛯ο?3. 原型對象的 constructor屬性 指向構(gòu)造函數(shù)
模擬實現(xiàn)new
首先我們要知道 new 做了什么創(chuàng)建一個新對象,并繼承其構(gòu)造函數(shù)的 prototype 异吻,這一步是為了繼承構(gòu)造函數(shù)原型上的屬性和方法 2. 執(zhí)行構(gòu)造函數(shù)裹赴,方法內(nèi)的 this 被指定為該新實例 ,這一步是為了執(zhí)行構(gòu)造函數(shù)內(nèi)的賦值操作 3. 返回新實例 (規(guī)范規(guī)定诀浪,如果構(gòu)造方法返回了一個對象棋返,那么返回該對象,否則返回第一步創(chuàng)建的新對象)
// new是關(guān)鍵字,這里我們用函數(shù)來模擬,new Foo(args) <=> myNew(Foo, args)
function myNew(foo, ...args) {
// 創(chuàng)建新對象,并繼承構(gòu)造方法的prototype屬性, 這一步是為了把obj掛原型鏈上, 相當于obj.__proto__ = Foo.prototype
let obj = Object.create(foo.prototype)
// 執(zhí)行構(gòu)造方法, 并為其綁定新this, 這一步是為了讓構(gòu)造方法能進行this.name = name之類的操作, args是構(gòu)造方法的入?yún)? 因為這里用myNew模擬, 所以入?yún)膍yNew傳入
let result = foo.apply(obj, args)
// 如果構(gòu)造方法已經(jīng)return了一個對象, 那么就返回該對象, 一般情況下雷猪,構(gòu)造方法不會返回新實例睛竣,但使用者可以選擇返回新實例來覆蓋new創(chuàng)建的對象 否則返回myNew創(chuàng)建的新對象
return typeof result === 'object' && result !== null ? result : obj
}
function Foo(name) {
this.name = name
}
const newObj = myNew(Foo, 'zhangsan')
console.log(newObj) // Foo {name: "zhangsan"}
console.log(newObj instanceof Foo) // true
- ES5如何實現(xiàn)繼承
說到繼承,最容易想到的是ES6的 extends 求摇,當然如果只回答這個肯定不合格射沟,我們要從函數(shù)和原型鏈的角度上實現(xiàn)繼承,下面我們一步步地与境、遞進地實現(xiàn)一個合格的繼承
一. 原型鏈繼承
原型鏈繼承的原理很簡單验夯,直接讓子類的原型對象指向父類實例,當子類實例找不到對應(yīng)的屬性和方法時摔刁,就會往它的原型對象挥转,也就是父類實例上找,從而實現(xiàn)對父類的屬性和方法的繼承
// 父類
function Parent() {
this.name = '寫代碼像蔡徐抻'
}
// 父類的原型方法
Parent.prototype.getName = function() {
return this.name
}
// 子類
function Child() {}
// 讓子類的原型對象指向父類實例, 這樣一來在Child實例中找不到的屬性和方法就會到原型對象(父類實例)上尋找
Child.prototype = new Parent()
Child.prototype.constructor = Child // 根據(jù)原型鏈的規(guī)則,順便綁定一下constructor, 這一步不影響繼承, 只是在用到constructor時會需要
// 然后Child實例就能訪問到父類及其原型上的name屬性和getName()方法
const child = new Child()
child.name // '寫代碼像蔡徐抻'
child.getName() // '寫代碼像蔡徐抻'
原型繼承的缺點:
- 由于所有Child實例原型都指向同一個Parent實例, 因此對某個Child實例的父類引用類型變量修改會影響所有的Child實例 2. 在創(chuàng)建子類實例時無法向父類構(gòu)造傳參, 即沒有實現(xiàn) super() 的功能
// 示例:
function Parent() {
this.name = ['寫代碼像蔡徐抻']
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {}
Child.prototype = new Parent() Child.prototype.constructor = Child
// 測試 const child1 = new Child() const child2 = new Child() child1.name[0] = 'foo' console.log(child1.name) // ['foo'] console.log(child2.name) // ['foo'] (預(yù)期是['寫代碼像蔡徐抻'], 對child1.name的修改引起了所有child實例的變化)
### 二. 構(gòu)造函數(shù)繼承
構(gòu)造函數(shù)繼承共屈,即在子類的構(gòu)造函數(shù)中執(zhí)行父類的構(gòu)造函數(shù)绑谣,并為其綁定子類的`this`,讓父類的構(gòu)造函數(shù)把成員屬性和方法都掛到`子類的this`上去拗引,這樣既能避免實例之間共享一個原型實例借宵,又能向父類構(gòu)造方法傳參
```js
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this, 'zhangsan') // 執(zhí)行父類構(gòu)造方法并綁定子類的this, 使得父類中的屬性能夠賦到子類的this上
}
//測試
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // 報錯,找不到getName(), 構(gòu)造函數(shù)繼承的方式繼承不到父類原型上的屬性和方法
構(gòu)造函數(shù)繼承的缺點:
- 繼承不到父類原型上的屬性和方法
三. 組合式繼承
既然原型鏈繼承和構(gòu)造函數(shù)繼承各有互補的優(yōu)缺點, 那么我們?yōu)槭裁床唤M合起來使用呢, 所以就有了綜合二者的組合式繼承
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 構(gòu)造函數(shù)繼承
Parent.call(this, 'zhangsan')
}
//原型鏈繼承
Child.prototype = new Parent()
Child.prototype.constructor = Child
//測試
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
組合式繼承的缺點:
- 每次創(chuàng)建子類實例都執(zhí)行了兩次構(gòu)造函數(shù)( Parent.call() 和 new Parent() ),雖然這并不影響對父類的繼承矾削,但子類創(chuàng)建實例時壤玫,原型中會存在兩份相同的屬性和方法豁护,這并不優(yōu)雅
四. 寄生式組合繼承
為了解決構(gòu)造函數(shù)被執(zhí)行兩次的問題, 我們將 指向父類實例 改為 指向父類原型 , 減去一次構(gòu)造函數(shù)的執(zhí)行
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 構(gòu)造函數(shù)繼承
Parent.call(this, 'zhangsan')
}
//原型鏈繼承
// Child.prototype = new Parent()
Child.prototype = Parent.prototype //將`指向父類實例`改為`指向父類原型`
Child.prototype.constructor = Child
//測試
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
但這種方式存在一個問題,由于子類原型和父類原型指向同一個對象垦细,我們對子類原型的操作會影響到父類原型择镇,例如給 Child.prototype 增加一個getName()方法,那么會導(dǎo)致 Parent.prototype 也增加或被覆蓋一個getName()方法括改,為了解決這個問題,我們給 Parent.prototype 做一個淺拷貝
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 構(gòu)造函數(shù)繼承
Parent.call(this, 'zhangsan')
}
//原型鏈繼承
// Child.prototype = new Parent()
Child.prototype = Object.create(Parent.prototype) //將`指向父類實例`改為`指向父類原型`
Child.prototype.constructor = Child
//測試
const child = new Child()
const parent = new Parent()
child.getName() // ['zhangsan']
parent.getName() // 報錯, 找不到getName()
到這里我們就完成了ES5環(huán)境下的繼承的實現(xiàn)家坎,這種繼承方式稱為 寄生組合式繼承 嘱能,是目前最成熟的繼承方式,babel對ES6繼承的轉(zhuǎn)化也是使用了寄生組合式繼承
我們回顧一下實現(xiàn)過程:
一開始最容易想到的是 原型鏈繼承 虱疏,通過把子類實例的原型指向父類實例來繼承父類的屬性和方法惹骂,但原型鏈繼承的缺陷在于 對子類實例繼承的引用類型的修改會影響到所有的實例對象 以及 無法向父類的構(gòu)造方法傳參 。
因此我們引入了 構(gòu)造函數(shù)繼承 , 通過在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)并傳入子類this來獲取父類的屬性和方法做瞪,但構(gòu)造函數(shù)繼承也存在缺陷对粪,構(gòu)造函數(shù)繼承 不能繼承到父類原型鏈上的屬性和方法 。
所以我們綜合了兩種繼承的優(yōu)點装蓬,提出了 組合式繼承 著拭,但組合式繼承也引入了新的問題,它 每次創(chuàng)建子類實例都執(zhí)行了兩次父類構(gòu)造方法 牍帚,我們通過將 子類原型指向父類實例 改為 子類原型指向父類原型的淺拷貝 來解決這一問題儡遮,也就是最終實現(xiàn) —— 寄生組合式繼承
V8引擎機制
-
V8如何執(zhí)行一段JS代碼
- 預(yù)解析 :檢查語法錯誤但不生成AST 2. 生成AST :經(jīng)過詞法/語法分析,生成抽象語法樹 3. 生成字節(jié)碼 :基線編譯器(Ignition)將AST轉(zhuǎn)換成字節(jié)碼 4. 生成機器碼 :優(yōu)化編譯器(Turbofan)將字節(jié)碼轉(zhuǎn)換成優(yōu)化過的機器碼暗赶,此外在逐行執(zhí)行字節(jié)碼的過程中鄙币,如果一段代碼經(jīng)常被執(zhí)行,那么V8會將這段代碼直接轉(zhuǎn)換成機器碼保存起來蹂随,下一次執(zhí)行就不必經(jīng)過字節(jié)碼十嘿,優(yōu)化了執(zhí)行速度
上面幾點只是V8執(zhí)行機制的極簡總結(jié),建議閱讀參考資料:
- V8 是怎么跑起來的 —— V8 的 JavaScript 執(zhí)行管道 [5]
JavaScript 引擎 V8 執(zhí)行流程概述 [6]
介紹一下引用計數(shù)和標記清除
? 引用計數(shù) :給一個變量賦值引用類型岳锁,則該對象的引用次數(shù)+1绩衷,如果這個變量變成了其他值,那么該對象的引用次數(shù)-1浸锨,垃圾回收器會回收引用次數(shù)為0的對象唇聘。但是當對象循環(huán)引用時,會導(dǎo)致引用次數(shù)永遠無法歸零柱搜,造成內(nèi)存無法釋放迟郎。 ? 標記清除 :垃圾收集器先給內(nèi)存中所有對象加上標記,然后從根節(jié)點開始遍歷聪蘸,去掉被引用的對象和運行環(huán)境中對象的標記宪肖,剩下的被標記的對象就是無法訪問的等待回收的對象表制。-
V8如何進行垃圾回收
JS引擎中對變量的存儲主要有兩種位置,棧內(nèi)存和堆內(nèi)存控乾,棧內(nèi)存存儲基本類型數(shù)據(jù)以及引用類型數(shù)據(jù)的內(nèi)存地址么介,堆內(nèi)存儲存引用類型的數(shù)據(jù)
棧內(nèi)存的回收:
棧內(nèi)存調(diào)用棧上下文切換后就被回收,比較簡單
堆內(nèi)存的回收:
V8的堆內(nèi)存分為新生代內(nèi)存和老生代內(nèi)存蜕衡,新生代內(nèi)存是臨時分配的內(nèi)存壤短,存在時間短,老生代內(nèi)存存在時間長
? 新生代內(nèi)存回收機制:
? 新生代內(nèi)存容量小慨仿,64位系統(tǒng)下僅有32M久脯。新生代內(nèi)存分為 From、To 兩部分镰吆,進行垃圾回收時帘撰,先掃描From,將非存活對象回收万皿,將存活對象順序復(fù)制到To中摧找,之后調(diào)換From/To鞭执,等待下一次回收
? 老生代內(nèi)存回收機制
? 晉升 :如果新生代的變量經(jīng)過多次回收依然存在福贞,那么就會被放入老生代內(nèi)存中 ? 標記清除 :老生代內(nèi)存會先遍歷所有對象并打上標記瘟芝,然后對正在使用或被強引用的對象取消標記巍杈,回收被標記的對象 ? 整理內(nèi)存碎片 :把對象挪到內(nèi)存的一端
參考資料: 聊聊V8引擎的垃圾回收 [7]
- JS相較于C++等語言為什么慢付材,V8做了哪些優(yōu)化
- JS的問題:
? 動態(tài)類型 :導(dǎo)致每次存取屬性/尋求方法時候刁笙,都需要先檢查類型泻肯;此外動態(tài)類型也很難在編譯階段進行優(yōu)化 ? 屬性存取 :C++/Java等語言中方法海洼、屬性是存儲在數(shù)組中的佳励,僅需數(shù)組位移就可以獲取休里,而JS存儲在對象中,每次獲取都要進行哈希查詢
- V8的優(yōu)化:
? 優(yōu)化JIT(即時編譯) :相較于C++/Java這類編譯型語言赃承,JS一邊解釋一邊執(zhí)行妙黍,效率低。V8對這個過程進行了優(yōu)化:如果一段代碼被執(zhí)行多次瞧剖,那么V8會把這段代碼轉(zhuǎn)化為機器碼緩存下來拭嫁,下次運行時直接使用機器碼。 ? 隱藏類 :對于C++這類語言來說抓于,僅需幾個指令就能通過偏移量獲取變量信息做粤,而JS需要進行字符串匹配,效率低捉撮,V8借用了類和偏移位置的思想怕品,將對象劃分成不同的組,即隱藏類 ? 內(nèi)嵌緩存 :即緩存對象查詢的結(jié)果巾遭。常規(guī)查詢過程是:獲取隱藏類地址 -> 根據(jù)屬性名查找偏移值 -> 計算該屬性地址肉康,內(nèi)嵌緩存就是對這一過程結(jié)果的緩存 ? 垃圾回收管理 :上文已介紹
參考資料: 為什么V8引擎這么快闯估? [8]
瀏覽器渲染機制
-
瀏覽器的渲染過程是怎樣的
大體流程如下:
HTML和CSS經(jīng)過各自解析,生成DOM樹和CSSOM樹 2. 合并成為渲染樹 3. 根據(jù)渲染樹進行布局 4. 最后調(diào)用GPU進行繪制吼和,顯示在屏幕上
如何根據(jù)瀏覽器渲染機制加快首屏速度
優(yōu)化文件大小 :HTML和CSS的加載和解析都會阻塞渲染樹的生成涨薪,從而影響首屏展示速度,因此我們可以通過優(yōu)化文件大小炫乓、減少CSS文件層級的方法來加快首屏速度 2. 避免資源下載阻塞文檔解析 :瀏覽器解析到<script>標簽時刚夺,會阻塞文檔解析,直到腳本執(zhí)行完成末捣,因此我們通常把<script>標簽放在底部光督,或者加上 defer、async 來進行異步下載
什么是回流(重排),什么情況下會觸發(fā)回流
? 當元素的尺寸或者位置發(fā)生了變化筐摘,就需要重新計算渲染樹卒茬,這就是回流 ? DOM元素的幾何屬性( width/height/padding/margin/border )發(fā)生變化時會觸發(fā)回流 ? DOM元素移動或增加會觸發(fā)回流 ? 讀寫 offset/scroll/client 等屬性時會觸發(fā)回流 ? 調(diào)用 window.getComputedStyle 會觸發(fā)回流什么是重繪,什么情況下會觸發(fā)重繪
? DOM樣式發(fā)生了變化咖熟,但沒有影響DOM的幾何屬性時圃酵,會觸發(fā)重繪,而不會觸發(fā)回流馍管。重繪由于DOM位置信息不需要更新郭赐,省去了布局過程,因而性能上優(yōu)于回流什么是GPU加速确沸,如何使用GPU加速捌锭,GPU加速的缺點
? 優(yōu)點 :使用transform、opacity罗捎、filters等屬性時观谦,會直接在GPU中完成處理,這些屬性的變化不會引起回流重繪 ? 缺點 :GPU渲染字體會導(dǎo)致字體模糊桨菜,過多的GPU處理會導(dǎo)致內(nèi)存問題-
如何減少回流
? 使用 class 替代 style 豁状,減少style的使用 ? 使用 resize、scroll 時進行防抖和節(jié)流處理倒得,這兩者會直接導(dǎo)致回流 ? 使用 visibility 替換 display: none 泻红,因為前者只會引起重繪,后者會引發(fā)回流 ? 批量修改元素時霞掺,可以先讓元素脫離文檔流谊路,等修改完畢后,再放入文檔流 ? 避免觸發(fā)同步布局事件根悼,我們在獲取 offsetWidth 這類屬性的值時凶异,可以使用變量將查詢結(jié)果存起來蜀撑,避免多次查詢,每次對 offset/scroll/client 等屬性進行查詢時都會觸發(fā)回流 ? 對于復(fù)雜動畫效果,使用絕對定位讓其脫離文檔流剩彬,復(fù)雜的動畫效果會頻繁地觸發(fā)回流重繪酷麦,我們可以將動畫元素設(shè)置絕對定位從而脫離文檔流避免反復(fù)回流重繪。
考資料: 必須明白的瀏覽器渲染機制 [9]
瀏覽器緩存策略
介紹一下瀏覽器緩存位置和優(yōu)先級
Service Worker 2. Memory Cache(內(nèi)存緩存) 3. Disk Cache(硬盤緩存) 4. Push Cache(推送緩存) 5. 以上緩存都沒命中就會進行網(wǎng)絡(luò)請求
說說不同緩存間的差別
Service Worker
和Web Worker類似喉恋,是獨立的線程沃饶,我們可以在這個線程中緩存文件,在主線程需要的時候讀取這里的文件轻黑,Service Worker使我們可以自由選擇緩存哪些文件以及文件的匹配糊肤、讀取規(guī)則,并且緩存是持續(xù)性的
- Memory Cache
即內(nèi)存緩存氓鄙,內(nèi)存緩存不是持續(xù)性的馆揉,緩存會隨著進程釋放而釋放
- Disk Cache
即硬盤緩存,相較于內(nèi)存緩存抖拦,硬盤緩存的持續(xù)性和容量更優(yōu)升酣,它會根據(jù)HTTP header的字段判斷哪些資源需要緩存
- Push Cache
即推送緩存,是HTTP/2的內(nèi)容态罪,目前應(yīng)用較少
- 介紹一下瀏覽器緩存策略
強緩存(不要向服務(wù)器詢問的緩存)
設(shè)置Expires
? 即過期時間噩茄,例如 「Expires: Thu, 26 Dec 2019 10:30:42 GMT」 表示緩存會在這個時間后失效,這個過期日期是絕對日期复颈,如果修改了本地日期绩聘,或者本地日期與服務(wù)器日期不一致,那么將導(dǎo)致緩存過期時間錯誤耗啦。
設(shè)置Cache-Control
? HTTP/1.1新增字段凿菩,Cache-Control可以通過 max-age 字段來設(shè)置過期時間,例如 「Cache-Control:max-age=3600」 除此之外Cache-Control還能設(shè)置 private/no-cache 等多種字段
協(xié)商緩存(需要向服務(wù)器詢問緩存是否已經(jīng)過期)
Last-Modified
? 即最后修改時間芹彬,瀏覽器第一次請求資源時蓄髓,服務(wù)器會在響應(yīng)頭上加上 Last-Modified ,當瀏覽器再次請求該資源時舒帮,瀏覽器會在請求頭中帶上 If-Modified-Since 字段会喝,字段的值就是之前服務(wù)器返回的最后修改時間,服務(wù)器對比這兩個時間玩郊,若相同則返回304肢执,否則返回新資源,并更新Last-Modified
ETag
? HTTP/1.1新增字段译红,表示文件唯一標識预茄,只要文件內(nèi)容改動,ETag就會重新計算。緩存流程和 Last-Modified 一樣:服務(wù)器發(fā)送 ETag 字段 -> 瀏覽器再次請求時發(fā)送 If-None-Match -> 如果ETag值不匹配耻陕,說明文件已經(jīng)改變拙徽,返回新資源并更新ETag,若匹配則返回304
兩者對比
? ETag 比 Last-Modified 更準確:如果我們打開文件但并沒有修改诗宣,Last-Modified 也會改變膘怕,并且 Last-Modified 的單位時間為一秒,如果一秒內(nèi)修改完了文件召庞,那么還是會命中緩存 ? 如果什么緩存策略都沒有設(shè)置岛心,那么瀏覽器會取響應(yīng)頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間
參考資料: 瀏覽器緩存機制剖析 [10]
網(wǎng)絡(luò)相關(guān)
-
講講網(wǎng)絡(luò)OSI七層模型,TCP/IP和HTTP分別位于哪一層
常見HTTP狀態(tài)碼有哪些
2xx 開頭(請求成功)
200 OK :客戶端發(fā)送給服務(wù)器的請求被正常處理并返回
3xx 開頭(重定向)
301 Moved Permanently :永久重定向篮灼,請求的網(wǎng)頁已永久移動到新位置忘古。服務(wù)器返回此響應(yīng)時,會自動將請求者轉(zhuǎn)到新位置
302 Moved Permanently :臨時重定向诅诱,請求的網(wǎng)頁已臨時移動到新位置髓堪。服務(wù)器目前從不同位置的網(wǎng)頁響應(yīng)請求,但請求者應(yīng)繼續(xù)使用原有位置來進行以后的請求
304 Not Modified :未修改娘荡,自從上次請求后旦袋,請求的網(wǎng)頁未修改過。服務(wù)器返回此響應(yīng)時它改,不會返回網(wǎng)頁內(nèi)容
4xx 開頭(客戶端錯誤)
400 Bad Request :錯誤請求,服務(wù)器不理解請求的語法商乎,常見于客戶端傳參錯誤
401 Unauthorized :未授權(quán)央拖,表示發(fā)送的請求需要有通過 HTTP 認證的認證信息,常見于客戶端未登錄
403 Forbidden :禁止鹉戚,服務(wù)器拒絕請求鲜戒,常見于客戶端權(quán)限不足
404 Not Found :未找到,服務(wù)器找不到對應(yīng)資源
5xx 開頭(服務(wù)端錯誤)
500 Inter Server Error :服務(wù)器內(nèi)部錯誤抹凳,服務(wù)器遇到錯誤遏餐,無法完成請求
501 Not Implemented :尚未實施,服務(wù)器不具備完成請求的功能
502 Bad Gateway :作為網(wǎng)關(guān)或者代理工作的服務(wù)器嘗試執(zhí)行請求時赢底,從上游服務(wù)器接收到無效的響應(yīng)失都。
503 service unavailable :服務(wù)不可用,服務(wù)器目前無法使用(處于超載或停機維護狀態(tài))幸冻。通常是暫時狀態(tài)粹庞。
- GET請求和POST請求有何區(qū)別
標準答案:
? GET請求參數(shù)放在URL上,POST請求參數(shù)放在請求體里 ? GET請求參數(shù)長度有限制洽损,POST請求參數(shù)長度可以非常大 ? POST請求相較于GET請求安全一點點庞溜,因為GET請求的參數(shù)在URL上,且有歷史記錄 ? GET請求能緩存碑定,POST不能
更進一步:
其實HTTP協(xié)議并沒有要求GET/POST請求參數(shù)必須放在URL上或請求體里流码,也沒有規(guī)定GET請求的長度又官,目前對URL的長度限制,是各家瀏覽器設(shè)置的限制漫试。GET和POST的根本區(qū)別在于: GET請求是冪等性的六敬,而POST請求不是
冪等性,指的是對某一資源進行一次或多次請求都具有相同的副作用商虐。例如搜索就是一個冪等的操作觉阅,而刪除、新增則不是一個冪等操作秘车。
由于GET請求是冪等的典勇,在網(wǎng)絡(luò)不好的環(huán)境中,GET請求可能會重復(fù)嘗試叮趴,造成重復(fù)操作數(shù)據(jù)的風(fēng)險割笙,因此,GET請求用于無副作用的操作(如搜索)眯亦,新增/刪除等操作適合用POST
參考資料: HTTP|GET 和 POST 區(qū)別伤溉?網(wǎng)上多數(shù)答案都是錯的 [11]
-
HTTP的請求報文由哪幾部分組成
一個HTTP請求報文由請求行(request line)、請求頭(header)妻率、空行和請求數(shù)據(jù)4個部分組成
響應(yīng)報文和請求報文結(jié)構(gòu)類似乱顾,不再贅述
-
HTTP常見請求/響應(yīng)頭及其含義
通用頭(請求頭和響應(yīng)頭都有的首部)
請求頭
響應(yīng)頭
響應(yīng)頭
實體頭(針對請求報文和響應(yīng)報文的實體部分使用首部)
HTTP首部當然不止這么幾個,但為了避免寫太多大家記不住( 主要是別的我也沒去看 )宫静,這里只介紹了一些常用的走净,詳細的可以看 MDN的文檔 [13] HTTP/1.0和HTTP/1.1有什么區(qū)別
? 長連接: HTTP/1.1支持長連接和請求的流水線,在一個TCP連接上可以傳送多個HTTP請求孤里,避免了因為多次建立TCP連接的時間消耗和延時 ? 緩存處理: HTTP/1.1引入 Entity tag伏伯,If-Unmodified-Since, If-Match, If-None-Match 等新的請求頭來控制緩存,詳見瀏覽器緩存小節(jié) ? 帶寬優(yōu)化及網(wǎng)絡(luò)連接的使用: HTTP1.1則在請求頭引入了range頭域捌袜,支持斷點續(xù)傳功能 ? Host頭處理: 在HTTP/1.0中認為每臺服務(wù)器都有唯一的IP地址说搅,但隨著虛擬主機技術(shù)的發(fā)展,多個主機共享一個IP地址愈發(fā)普遍虏等,HTTP1.1的請求消息和響應(yīng)消息都應(yīng)支持Host頭域弄唧,且請求消息中如果沒有Host頭域會400錯誤介紹一下HTTP/2.0新特性
? 多路復(fù)用: 即多個請求都通過一個TCP連接并發(fā)地完成 ? 服務(wù)端推送: 服務(wù)端能夠主動把資源推送給客戶端 ? 新的二進制格式: HTTP/2采用二進制格式傳輸數(shù)據(jù),相比于HTTP/1.1的文本格式霍衫,二進制格式具有更好的解析性和拓展性 ? header壓縮: HTTP/2壓縮消息頭套才,減少了傳輸數(shù)據(jù)的大小說說HTTP/2.0多路復(fù)用基本原理以及解決的問題
HTTP/2解決的問題,就是HTTP/1.1存在的問題:
? TCP慢啟動: TCP連接建立后慕淡,會經(jīng)歷一個先慢后快的發(fā)送過程背伴,就像汽車啟動一般,如果我們的網(wǎng)頁文件(HTML/JS/CSS/icon)都經(jīng)過一次慢啟動,對性能是不小的損耗傻寂。另外慢啟動是TCP為了減少網(wǎng)絡(luò)擁塞的一種策略息尺,我們是沒有辦法改變的。 ? 多條TCP連接競爭帶寬: 如果同時建立多條TCP連接疾掰,當帶寬不足時就會競爭帶寬搂誉,影響關(guān)鍵資源的下載。 ? HTTP/1.1隊頭阻塞: 盡管HTTP/1.1長鏈接可以通過一個TCP連接傳輸多個請求静檬,但同一時刻只能處理一個請求炭懊,當前請求未結(jié)束前,其他請求只能處于阻塞狀態(tài)拂檩。
為了解決以上幾個問題侮腹, HTTP/2一個域名只使用一個TCP?連接來傳輸數(shù)據(jù),而且請求直接是并行的稻励、非阻塞的父阻,這就是多路復(fù)用
實現(xiàn)原理:HTTP/2引入了一個二進制分幀層,客戶端和服務(wù)端進行傳輸時望抽,數(shù)據(jù)會先經(jīng)過二進制分幀層處理加矛,轉(zhuǎn)化為一個個帶有請求ID的幀,這些幀在傳輸完成后根據(jù)ID組合成對應(yīng)的數(shù)據(jù)煤篙。
- 說說HTTP/3.0
盡管HTTP/2解決了很多1.1的問題斟览,但HTTP/2仍然存在一些缺陷,這些缺陷并不是來自于HTTP/2協(xié)議本身辑奈,而是來源于底層的TCP協(xié)議趣惠,我們知道TCP鏈接是可靠的連接,如果出現(xiàn)了丟包身害,那么整個連接都要等待重傳,HTTP/1.1可以同時使用6個TCP連接草戈,一個阻塞另外五個還能工作塌鸯,但HTTP/2只有一個TCP連接,阻塞的問題便被放大了唐片。
由于TCP協(xié)議已經(jīng)被廣泛使用丙猬,我們很難直接修改TCP協(xié)議,基于此费韭,HTTP/3選擇了一個折衷的方法——UDP協(xié)議茧球,HTTP/2在UDP的基礎(chǔ)上實現(xiàn)多路復(fù)用、0-RTT星持、TLS加密抢埋、流量控制、丟包重傳等功能。
參考資料: http發(fā)展史(http0.9揪垄、http1.0穷吮、http1.1、http2饥努、http3)梳理筆記 [14] (推薦閱讀)
HTTP和HTTPS有何區(qū)別
? HTTPS使用443端口捡鱼,而HTTP使用80 ? HTTPS需要申請證書 ? HTTP是超文本傳輸協(xié)議,是明文傳輸酷愧;HTTPS是經(jīng)過SSL加密的協(xié)議驾诈,傳輸更安全 ? HTTPS比HTTP慢,因為HTTPS除了TCP握手的三個包溶浴,還要加上SSL握手的九個包HTTPS是如何進行加密的
我們通過分析幾種加密方式乍迄,層層遞進,理解HTTPS的加密方式以及為什么使用這種加密方式:
對稱加密
客戶端和服務(wù)器公用一個密匙用來對消息加解密戳葵,這種方式稱為對稱加密就乓。客戶端和服務(wù)器約定好一個加密的密匙拱烁∩希客戶端在發(fā)消息前用該密匙對消息加密,發(fā)送給服務(wù)器后戏自,服務(wù)器再用該密匙進行解密拿到消息邦投。這種方式一定程度上保證了數(shù)據(jù)的安全性,但密鑰一旦泄露(密鑰在傳輸過程中被截獲)擅笔,傳輸內(nèi)容就會暴露志衣,因此我們要尋找一種安全傳遞密鑰的方法。
非對稱加密
采用非對稱加密時猛们,客戶端和服務(wù)端均擁有一個公鑰和私鑰念脯,公鑰加密的內(nèi)容只有對應(yīng)的私鑰能解密。私鑰自己留著弯淘,公鑰發(fā)給對方绿店。這樣在發(fā)送消息前,先用對方的公鑰對消息進行加密庐橙,收到后再用自己的私鑰進行解密假勿。這樣攻擊者只拿到傳輸過程中的公鑰也無法破解傳輸?shù)膬?nèi)容盡管非對稱加密解決了由于密鑰被獲取而導(dǎo)致傳輸內(nèi)容泄露的問題,但中間人仍然可以用 篡改公鑰
的方式來獲取或篡改傳輸內(nèi)容态鳖,而且非對稱加密的性能比對稱加密的性能差了不少
第三方認證
上面這種方法的弱點在于转培,客戶端不知道公鑰是由服務(wù)端返回,還是中間人返回的浆竭,因此我們再引入一個第三方認證的環(huán)節(jié):即第三方使用私鑰加密我們 自己的公鑰
浸须,瀏覽器已經(jīng)內(nèi)置一些權(quán)威第三方認證機構(gòu)的公鑰惨寿,瀏覽器會使用 第三方的公鑰
來解開 第三方私鑰加密過的我們自己的公鑰
,從而獲取公鑰羽戒,如果能成功解密缤沦,就說明獲取到的 自己的公鑰
是正確的
但第三方認證也未能完全解決問題,第三方認證是面向所有人的易稠,中間人也能申請證書缸废,如果中間人使用自己的證書掉包原證書,客戶端還是無法確認公鑰的真?zhèn)?/p>
數(shù)字簽名
為了讓客戶端能夠驗證公鑰的來源驶社,我們給公鑰加上一個數(shù)字簽名企量,這個數(shù)字簽名是由企業(yè)、網(wǎng)站等各種信息和公鑰經(jīng)過單向hash而來亡电,一旦構(gòu)成數(shù)字簽名的信息發(fā)生變化届巩,hash值就會改變,這就構(gòu)成了公鑰來源的唯一標識份乒。
具體來說恕汇,服務(wù)端本地生成一對密鑰,然后拿著公鑰以及企業(yè)或辖、網(wǎng)站等各種信息到CA(第三方認證中心)去申請數(shù)字證書瘾英,CA會通過一種單向hash算法(比如MD5),生成一串摘要颂暇,這串摘要就是這堆信息的唯一標識缺谴,然后CA還會使用自己的私鑰對摘要進行加密,連同我們自己服務(wù)器的公鑰一同發(fā)送給我我們耳鸯。
瀏覽器拿到數(shù)字簽名后,會使用 瀏覽器本地內(nèi)置 的CA公鑰解開數(shù)字證書并驗證察迟,從而拿到正確的公鑰乍赫。由于非對稱加密性能低下林束,拿到公鑰以后锨阿,客戶端會隨機生成一個對稱密鑰口渔,使用這個公鑰加密并發(fā)送給服務(wù)端知举,服務(wù)端用自己的私鑰解開對稱密鑰逛钻,此后的加密連接就通過這個對稱密鑰進行對稱加密。
綜上所述锰提,HTTPS在驗證階段使用非對稱加密+第三方認證+數(shù)字簽名獲取正確的公鑰曙痘,獲取到正確的公鑰后以對稱加密的方式通信
參考資料: 看圖學(xué)HTTPS [15]
前端安全
什么是CSRF攻擊
CSRF即Cross-site request forgery(跨站請求偽造),是一種挾制用戶在當前已登錄的Web應(yīng)用程序上執(zhí)行非本意的操作的攻擊方法欲账。
假如黑客在自己的站點上放置了其他網(wǎng)站的外鏈屡江,例如 "www.weibo.com/api
,默認情況下赛不,瀏覽器會帶著 weibo.com
的cookie訪問這個網(wǎng)址惩嘉,如果用戶已登錄過該網(wǎng)站且網(wǎng)站沒有對CSRF攻擊進行防御,那么服務(wù)器就會認為是用戶本人在調(diào)用此接口并執(zhí)行相關(guān)操作踢故,致使賬號被劫持文黎。
如何防御CSRF攻擊
? 驗證 Token
:瀏覽器請求服務(wù)器時,服務(wù)器返回一個token殿较,每個請求都需要同時帶上token和cookie才會被認為是合法請求 ? 驗證 Referer
:通過驗證請求頭的Referer來驗證來源站點耸峭,但請求頭很容易偽造 ? 設(shè)置 SameSite
:設(shè)置cookie的SameSite,可以讓cookie不隨跨域請求發(fā)出淋纲,但瀏覽器兼容不一
什么是XSS攻擊
XSS即Cross Site Scripting(跨站腳本)劳闹,指的是通過利用網(wǎng)頁開發(fā)時留下的漏洞,注入惡意指令代碼到網(wǎng)頁洽瞬,使用戶加載并執(zhí)行攻擊者惡意制造的網(wǎng)頁程序本涕。常見的例如在評論區(qū)植入JS代碼疫衩,用戶進入評論頁時代碼被執(zhí)行鸠姨,造成頁面被植入廣告、賬號信息被竊取
XSS攻擊有哪些類型
? 存儲型 :即攻擊被存儲在服務(wù)端撞秋,常見的是在評論區(qū)插入攻擊腳本为障,如果腳本被儲存到服務(wù)端晦闰,那么所有看見對應(yīng)評論的用戶都會受到攻擊。 ? 反射型 :攻擊者將腳本混在URL里鳍怨,服務(wù)端接收到URL將惡意代碼當做參數(shù)取出并拼接在HTML里返回呻右,瀏覽器解析此HTML后即執(zhí)行惡意代碼 ? DOM型 :將攻擊腳本寫在URL中,誘導(dǎo)用戶點擊該URL鞋喇,如果URL被解析声滥,那么攻擊腳本就會被運行。和前兩者的差別主要在于DOM型攻擊不經(jīng)過服務(wù)端
如何防御XSS攻擊
? 輸入檢查 :對輸入內(nèi)容中的 <script><iframe>
等標簽進行轉(zhuǎn)義或者過濾 ? 設(shè)置httpOnly :很多XSS攻擊目標都是竊取用戶cookie偽造身份認證确徙,設(shè)置此屬性可防止JS獲取cookie ? 開啟CSP 醒串,即開啟白名單,可阻止白名單以外的資源加載和運行
排序算法
1. 手寫冒泡排序
冒泡排序應(yīng)該是很多人第一個接觸的排序鄙皇,比較簡單芜赌,不展開講解了
function bubbleSort(arr){
for(let i = 0; i < arr.length; i++) {
for(let j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j+1]) {
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
return arr
}
- 如何優(yōu)化一個冒泡排序
冒泡排序總會執(zhí)行(N-1)+(N-2)+(N-3)+..+2+1趟,但如果運行到當中某一趟時排序已經(jīng)完成伴逸,或者輸入的是一個有序數(shù)組缠沈,那么后邊的比較就都是多余的,為了避免這種情況错蝴,我們增加一個flag洲愤,判斷排序是否在中途就已經(jīng)完成(也就是判斷有無發(fā)生元素交換)
function bubbleSort(arr){
let flag = true
for(let i = 0; i < arr.length; i++) {
for(let j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j+1]) {
flag = false
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
if(flag)break;
}
return arr
}
手寫快速排序
快排基本步驟:選取基準元素 2. 比基準元素小的元素放到左邊,大的放右邊 3. 在左右子數(shù)組中重復(fù)步驟一二顷锰,直到數(shù)組只剩下一個元素 4. 向上逐級合并數(shù)組
function quickSort(arr) {
if(arr.length <= 1) return arr //遞歸終止條件
const pivot = arr.length / 2 | 0 //基準點
const pivotValue = arr.splice(pivot, 1)
const leftArr = []
const rightArr = []
arr.forEach(val => {
val > pivotValue ? rightArr.push(val) : leftArr.push(val)
})
return [ ...quickSort(leftArr), pivotValue, ...quickSort(rightArr)]
}
4. 如何優(yōu)化一個快速排序
原地排序
上邊這個快排只是讓讀者找找感覺柬赐,我們不能這樣寫快排,如果每次都開兩個數(shù)組官紫,會消耗很多內(nèi)存空間肛宋,數(shù)據(jù)量大時可能造成內(nèi)存溢出,我們要避免開新的內(nèi)存空間束世,即原地完成排序
我們可以用元素交換來取代開新數(shù)組酝陈,在每一次分區(qū)的時候直接在原數(shù)組上交換元素, 將小于基準數(shù)的元素挪到數(shù)組開頭 毁涉,以 [5,1,4,2,3]
為例:
我們定義一個pos指針, 標識等待置換的元素的位置, 然后逐一遍歷數(shù)組元素, 遇到比基準數(shù)小的就和arr[pos]交換位置, 然后pos++
代碼實現(xiàn):
function quickSort(arr, left, right) { //這個left和right代表分區(qū)后“新數(shù)組”的區(qū)間下標沉帮,因為這里沒有新開數(shù)組,所以需要left/right來確認新數(shù)組的位置
if (left < right) {
let pos = left - 1 //pos即“被置換的位置”贫堰,第一趟為-1
for(let i = left; i <= right; i++) { //循環(huán)遍歷數(shù)組穆壕,置換元素
let pivot = arr[right] //選取數(shù)組最后一位作為基準數(shù),
if(arr[i] <= pivot) { //若小于等于基準數(shù)严嗜,pos++粱檀,并置換元素, 這里使用小于等于而不是小于, 其實是為了避免因為重復(fù)數(shù)據(jù)而進入死循環(huán)
pos++
let temp = arr[pos]
arr[pos] = arr[i]
arr[i] = temp
}
}
//一趟排序完成后,pos位置即基準數(shù)的位置漫玄,以pos的位置分割數(shù)組
quickSort(arr, left, pos - 1)
quickSort(arr, pos + 1, right)
}
return arr //數(shù)組只包含1或0個元素時(即left>=right)茄蚯,遞歸終止
}
//使用
var arr = [5,1,4,2,3]
var start = 0;
var end = arr.length - 1;
quickSort(arr, start, end)
這個交換的過程還是需要一些時間理解消化的,詳細分析可以看這篇: js算法-快速排序(Quicksort) [16]
三路快排
上邊這個快排還談不上優(yōu)化睦优,應(yīng)當說是快排的糾正寫法渗常,其實有兩個問題我們還能優(yōu)化一下:
-
有序數(shù)組的情況 :如果輸入的數(shù)組是有序的,而取基準點時也順序取汗盘,就可能導(dǎo)致基準點一側(cè)的子數(shù)組一直為空, 使時間復(fù)雜度退化到O(n2) 2. 大量重復(fù)數(shù)據(jù)的情況 :例如輸入的數(shù)據(jù)是
[1,2,2,2,2,3]
, 無論基準點取1皱碘、2還是3, 都會導(dǎo)致基準點兩側(cè)數(shù)組大小不平衡, 影響快排效率
對于第一個問題, 我們可以通過在取基準點的時候隨機化來解決,對于第二個問題隐孽,我們可以使用 三路快排
的方式來優(yōu)化癌椿,比方說對于上面的 [1,2,2,2,2,3]
健蕊,我們基準點取2,在分區(qū)的時候踢俄,將數(shù)組元素分為 小于2|等于2|大于2
三個區(qū)域缩功,其中等于基準點的部分不再進入下一次排序, 這樣就大大提高了快排效率
5. 手寫歸并排序
歸并排序和快排的思路類似,都是遞歸分治都办,區(qū)別在于快排邊分區(qū)邊排序嫡锌,而歸并在分區(qū)完成后才會排序
function mergeSort(arr) {
if(arr.length <= 1) return arr //數(shù)組元素被劃分到剩1個時,遞歸終止
const midIndex = arr.length/2 | 0
const leftArr = arr.slice(0, midIndex)
const rightArr = arr.slice(midIndex, arr.length)
return merge(mergeSort(leftArr), mergeSort(rightArr)) //先劃分琳钉,后合并
}
//合并
function merge(leftArr, rightArr) {
const result = []
while(leftArr.length && rightArr.length) {
leftArr[0] <= rightArr[0] ? result.push(leftArr.shift()) : result.push(rightArr.shift())
}
while(leftArr.length) result.push(leftArr.shift())
while(rightArr.length) result.push(rightArr.shift())
return result
}
6. 手寫堆排序
堆是一棵特殊的樹, 只要滿足 這棵樹是完全二叉樹
和 堆中每一個節(jié)點的值都大于或小于其左右孩子節(jié)點
這兩個條件, 那么就是一個堆, 根據(jù) 堆中每一個節(jié)點的值都大于或小于其左右孩子節(jié)點
, 又分為大根堆和小根堆
堆排序的流程:
- 初始化大(小)根堆势木,此時根節(jié)點為最大(小)值,將根節(jié)點與最后一個節(jié)點(數(shù)組最后一個元素)交換 2. 除開最后一個節(jié)點歌懒,重新調(diào)整大(小)根堆啦桌,使根節(jié)點為最大(小)值 3. 重復(fù)步驟二,直到堆中元素剩一個及皂,排序完成
以 [1,5,4,2,3]
為例構(gòu)筑大根堆:
代碼實現(xiàn):
// 堆排序
const heapSort = array => {
// 我們用數(shù)組來儲存這個大根堆,數(shù)組就是堆本身
// 初始化大頂堆震蒋,從第一個非葉子結(jié)點開始
for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
heapify(array, i, array.length);
}
// 排序,每一次 for 循環(huán)找出一個當前最大值躲庄,數(shù)組長度減一
for (let i = Math.floor(array.length - 1); i > 0; i--) {
// 根節(jié)點與最后一個節(jié)點交換
swap(array, 0, i);
// 從根節(jié)點開始調(diào)整查剖,并且最后一個結(jié)點已經(jīng)為當前最大值,不需要再參與比較噪窘,所以第三個參數(shù)為 i笋庄,即比較到最后一個結(jié)點前一個即可
heapify(array, 0, i);
}
return array;
};
// 交換兩個節(jié)點
const swap = (array, i, j) => {
let temp = array[i];
array[i] = array[j];
array[j] = temp;
};
// 將 i 結(jié)點以下的堆整理為大頂堆,注意這一步實現(xiàn)的基礎(chǔ)實際上是:
// 假設(shè)結(jié)點 i 以下的子堆已經(jīng)是一個大頂堆倔监,heapify 函數(shù)實現(xiàn)的
// 功能是實際上是:找到 結(jié)點 i 在包括結(jié)點 i 的堆中的正確位置直砂。
// 后面將寫一個 for 循環(huán),從第一個非葉子結(jié)點開始浩习,對每一個非葉子結(jié)點
// 都執(zhí)行 heapify 操作静暂,所以就滿足了結(jié)點 i 以下的子堆已經(jīng)是一大頂堆
const heapify = (array, i, length) => {
let temp = array[i]; // 當前父節(jié)點
// j < length 的目的是對結(jié)點 i 以下的結(jié)點全部做順序調(diào)整
for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
temp = array[i]; // 將 array[i] 取出,整個過程相當于找到 array[i] 應(yīng)處于的位置
if (j + 1 < length && array[j] < array[j + 1]) {
j++; // 找到兩個孩子中較大的一個谱秽,再與父節(jié)點比較
}
if (temp < array[j]) {
swap(array, i, j); // 如果父節(jié)點小于子節(jié)點:交換洽蛀;否則跳出
i = j; // 交換后,temp 的下標變?yōu)?j
} else {
break;
}
}
}
參考資料: JS實現(xiàn)堆排序 [17]
- 歸并疟赊、快排郊供、堆排有何區(qū)別
其實從表格中我們可以看到,就時間復(fù)雜度而言近哟,快排并沒有很大優(yōu)勢驮审,然而為什么快排會成為最常用的排序手段,這是因為時間復(fù)雜度只能說明隨著數(shù)據(jù)量的增加,算法時間代價增長的趨勢
疯淫,并不直接代表實際執(zhí)行時間地来,實際運行時間還包括了很多常數(shù)參數(shù)的差別,此外在面對不同類型數(shù)據(jù)(比如有序數(shù)據(jù)熙掺、大量重復(fù)數(shù)據(jù))時靠抑,表現(xiàn)也不同,綜合來說适掰,快排的時間效率是最高的
在實際運用中, 并不只使用一種排序手段, 例如V8的 Array.sort()
就采取了 當 n<=10 時, 采用插入排序, 當 n>10 時,采用三路快排 的排序策略
設(shè)計模式
設(shè)計模式有許多種荠列,這里挑出幾個常用的:| 設(shè)計模式 | 描述 | 例子 | | :-: | :-: | :-: | | 單例模式 | 一個類只能構(gòu)造出唯一實例 | Redux/Vuex的store | | 工廠模式 | 對創(chuàng)建對象邏輯的封裝 | jQuery的$(selector) | 觀察者模式 | 當一個對象被修改時类浪,會自動通知它的依賴對象 | Redux的subscribe、Vue的雙向綁定 | | 裝飾器模式 | 對類的包裝肌似,動態(tài)地拓展類的功能 | React高階組件费就、ES7 裝飾器 | 適配器模式 | 兼容新舊接口,對類的包裝 | 封裝舊API | 代理模式 | 控制對象的訪問 | 事件代理川队、ES6的Proxy
1. 介紹一下單一職責(zé)原則和開放封閉原則
?
單一職責(zé)原則:一個類只負責(zé)一個功能領(lǐng)域中的相應(yīng)職責(zé)力细,或者可以定義為:就一個類而言,應(yīng)該只有一個引起它變化的原因固额。
?
開放封閉原則:核心的思想是軟件實體(類眠蚂、模塊、函數(shù)等)是可擴展的斗躏、但不可修改的逝慧。也就是說,對擴展是開放的,而對修改是封閉的。
2. 單例模式
單例模式即一個類只能構(gòu)造出唯一實例啄糙,單例模式的意義在于 共享笛臣、唯一 , Redux/Vuex
中的store隧饼、 JQ
的$或者業(yè)務(wù)場景中的購物車沈堡、登錄框都是單例模式的應(yīng)用
class SingletonLogin {
constructor(name,password){
this.name = name
this.password = password
}
static getInstance(name,password){
//判斷對象是否已經(jīng)被創(chuàng)建,若創(chuàng)建則返回舊對象
if(!this.instance)this.instance = new SingletonLogin(name,password)
return this.instance
}
}
let obj1 = SingletonLogin.getInstance('CXK','123')
let obj2 = SingletonLogin.getInstance('CXK','321')
console.log(obj1===obj2) // true
console.log(obj1) // {name:CXK,password:123}
console.log(obj2) // 輸出的依然是{name:CXK,password:123}
- 工廠模式
工廠模式即對創(chuàng)建對象邏輯的封裝,或者可以簡單理解為對 new 的封裝燕雁,這種封裝就像創(chuàng)建對象的工廠诞丽,故名工廠模式。工廠模式常見于大型項目拐格,比如JQ的()已經(jīng)是一個工廠方法,其他例子例如 React.createElement() 禁荒、 Vue.component() 都是工廠模式的實現(xiàn)猬膨。工廠模式有多種: 簡單工廠模式 、 工廠方法模式 、 抽象工廠模式 勃痴,這里只以簡單工廠模式為例:
class User {
constructor(name, auth) {
this.name = name
this.auth = auth
}
}
class UserFactory {
static createUser(name, auth) {
//工廠內(nèi)部封裝了創(chuàng)建對象的邏輯:權(quán)限為admin時,auth傳1,而使用者在外部創(chuàng)建對象時,不需要知道admin對應(yīng)哪個字段
if(auth === 'admin') new User(name, 1)
if(auth === user) new User(name, 2)
}
}
const admin = UserFactory.createUser('admin');
const user = UserFactory.createUser('user');
- 觀察者模式
觀察者模式算是前端最常用的設(shè)計模式了谒所,觀察者模式概念很簡單:觀察者監(jiān)聽被觀察者的變化,被觀察者發(fā)生改變時沛申,通知所有的觀察者劣领。觀察者模式被廣泛用于監(jiān)聽事件的實現(xiàn),有關(guān)觀察者模式的詳細應(yīng)用铁材,可以看我另一篇講解 Redux實現(xiàn)的文章 [18]
//觀察者
class Observer {
constructor (fn) {
this.update = fn
}
}
//被觀察者
class Subject {
constructor() {
this.observers = [] //觀察者隊列
}
addObserver(observer) {
this.observers.push(observer)//往觀察者隊列添加觀察者
}
notify() { //通知所有觀察者,實際上是把觀察者的update()都執(zhí)行了一遍
this.observers.forEach(observer => {
observer.update() //依次取出觀察者,并執(zhí)行觀察者的update方法
})
}
}
var subject = new Subject() //被觀察者
const update = () => {console.log('被觀察者發(fā)出通知')} //收到廣播時要執(zhí)行的方法
var ob1 = new Observer(update) //觀察者1
var ob2 = new Observer(update) //觀察者2
subject.addObserver(ob1) //觀察者1訂閱subject的通知
subject.addObserver(ob2) //觀察者2訂閱subject的通知
subject.notify() //發(fā)出廣播,執(zhí)行所有觀察者的update方法
有些文章也把觀察者模式稱為發(fā)布訂閱模式尖淘,其實二者是有所區(qū)別的,發(fā)布訂閱相較于觀察者模式多一個調(diào)度中心著觉。
- 裝飾器模式
裝飾器模式村生,可以理解為對類的一個包裝,動態(tài)地拓展類的功能饼丘,ES7的 裝飾器 語法以及React中的 高階組件 (HoC)都是這一模式的實現(xiàn)趁桃。react-redux的connect()也運用了裝飾器模式,這里以ES7的裝飾器為例:
function info(target) {
target.prototype.name = '張三'
target.prototype.age = 10
}
@info
class Man {}
let man = new Man()
man.name // 張三
- 適配器模式
適配器模式肄鸽,將一個接口轉(zhuǎn)換成客戶希望的另一個接口卫病,使接口不兼容的那些類可以一起工作。我們在生活中就常常有使用適配器的場景典徘,例如出境旅游插頭插座不匹配蟀苛,這時我們就需要使用轉(zhuǎn)換插頭,也就是適配器來幫我們解決問題逮诲。
class Adaptee {
test() {
return '舊接口'
}
}
class Target {
constructor() {
this.adaptee = new Adaptee()
}
test() {
let info = this.adaptee.test()
return `適配${info}`
}
}
let target = new Target()
console.log(target.test())
- 代理模式
代理模式屹逛,為一個對象找一個替代對象,以便對原對象進行訪問汛骂。即在訪問者與目標對象之間加一層代理罕模,通過代理做授權(quán)和控制。最常見的例子是經(jīng)紀人代理明星業(yè)務(wù)帘瞭,假設(shè)你作為一個投資者淑掌,想聯(lián)系明星打廣告,那么你就需要先經(jīng)過代理經(jīng)紀人蝶念,經(jīng)紀人對你的資質(zhì)進行考察抛腕,并通知你明星排期,替明星本人過濾不必要的信息媒殉。事件代理担敌、 JQuery的$.proxy 、ES6的 proxy 都是這一模式的實現(xiàn)廷蓉,下面以ES6的proxy為例:
const idol = {
name: '蔡x抻',
phone: 10086,
price: 1000000 //報價
}
const agent = new Proxy(idol, {
get: function(target) {
//攔截明星電話的請求,只提供經(jīng)紀人電話
return '經(jīng)紀人電話:10010'
},
set: function(target, key, value) {
if(key === 'price' ) {
//經(jīng)紀人過濾資質(zhì)
if(value < target.price) throw new Error('報價過低')
target.price = value
}
}
})
agent.phone //經(jīng)紀人電話:10010
agent.price = 100 //Uncaught Error: 報價過低
HTML相關(guān)
1. 說說HTML5在標簽全封、屬性、存儲、API上的新特性
? 標簽:新增語義化標簽( aside / figure / section / header / footer / nav
等)刹悴,增加多媒體標簽 video
和 audio
行楞,使得樣式和結(jié)構(gòu)更加分離 ? 屬性:增強表單,主要是增強了 input
的type屬性土匀; meta
增加charset以設(shè)置字符集子房; script
增加async以異步加載腳本 ? 存儲:增加 localStorage
、 sessionStorage
和 indexedDB
就轧,引入了 application cache
對web和應(yīng)用進行緩存 ? API:增加 拖放API
证杭、 地理定位
、 SVG繪圖
妒御、 canvas繪圖
解愤、 Web Worker
、 WebSocket
2. doctype的作用是什么携丁?
聲明文檔類型,告知瀏覽器用什么文檔標準解析這個文檔:
? 怪異模式:瀏覽器使用自己的模式解析文檔兰怠,不加doctype時默認為怪異模式 ? 標準模式:瀏覽器以W3C的標準解析文檔
3. 幾種前端儲存以及它們之間的區(qū)別
? cookies :HTML5之前本地儲存的主要方式梦鉴,大小只有4k,HTTP請求頭會自動帶上cookie揭保,兼容性好 ? localStorage :HTML5新特性肥橙,持久性存儲,即使頁面關(guān)閉也不會被清除秸侣,以鍵值對的方式存儲存筏,大小為5M ? sessionStorage :HTML5新特性,操作及大小同localStorage味榛,和localStorage的區(qū)別在于sessionStorage在選項卡(頁面)被關(guān)閉時即清除椭坚,且不同選項卡之間的sessionStorage不互通 ? IndexedDB :NoSQL型數(shù)據(jù)庫,類比MongoDB搏色,使用鍵值對進行儲存善茎,異步操作數(shù)據(jù)庫,支持事務(wù)频轿,儲存空間可以在250MB以上垂涯,但是IndexedDB受同源策略限制 ? Web SQL:是在瀏覽器上模擬的關(guān)系型數(shù)據(jù)庫,開發(fā)者可以通過SQL語句來操作Web SQL航邢,是HTML5以外一套獨立的規(guī)范耕赘,兼容性差
4. href和src有什么區(qū)別
href(hyperReference)
即超文本引用:當瀏覽器遇到href時,會并行的地下載資源膳殷,不會阻塞頁面解析操骡,例如我們使用 <link>
引入CSS,瀏覽器會并行地下載CSS而不阻塞頁面解析. 因此我們在引入CSS時建議使用 <link>
而不是 @import
<link href="style.css" rel="stylesheet" />
src(resource)
即資源,當瀏覽器遇到src時当娱,會暫停頁面解析吃既,直到該資源下載或執(zhí)行完畢,這也是script標簽之所以放底部的原因
<script src="script.js"></script>
- meta有哪些屬性跨细,作用是什么
meta標簽用于描述網(wǎng)頁的 元信息 鹦倚,如網(wǎng)站作者、描述冀惭、關(guān)鍵詞震叙,meta通過name=xxx
和content=xxx
的形式來定義信息,常用設(shè)置如下:
? charset:定義HTML文檔的字符集
<meta charset="UTF-8" >
http-equiv:可用于模擬http請求頭散休,可設(shè)置過期時間媒楼、緩存、刷新
<meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT">
? viewport:視口戚丸,用于控制頁面寬高及縮放比例
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
>
. viewport有哪些參數(shù)划址,作用是什么
? width/height,寬高限府,默認寬度980px ? initial-scale夺颤,初始縮放比例,1~10 ? maximum-scale/minimum-scale胁勺,允許用戶縮放的最大/小比例 ? user-scalable世澜,用戶是否可以縮放 (yes/no)
7. http-equive屬性的作用和參數(shù)
? expires,指定過期時間 ? progma署穗,設(shè)置no-cache可以禁止緩存 ? refresh寥裂,定時刷新 ? set-cookie,可以設(shè)置cookie ? X-UA-Compatible案疲,使用瀏覽器版本 ? apple-mobile-web-app-status-bar-style封恰,針對WebApp全屏模式,隱藏狀態(tài)欄/設(shè)置狀態(tài)欄顏色
CSS相關(guān)
清除浮動的方法
為什么要清除浮動:清除浮動是為了解決子元素浮動而導(dǎo)致父元素高度塌陷的問題
1.添加新元素
<div class="parent">
<div class="child"></div>
<!-- 添加一個空元素褐啡,利用css提供的clear:both清除浮動 -->
<div style="clear: both"></div>
</div>
2.使用偽元素
/* 對父元素添加偽元素 */
.parent::after{
content: "";
display: block;
height: 0;
clear:both;
}
3.觸發(fā)父元素BFC
/* 觸發(fā)父元素BFC */
.parent {
overflow: hidden;
/* float: left; */
/* position: absolute; */
/* display: inline-block */
/* 以上屬性均可觸發(fā)BFC */
}
常見布局
編輯中俭驮,請稍等-_-||
什么是BFC
BFC全稱 Block Formatting Context 即 塊級格式上下文
,簡單的說春贸,BFC是頁面上的一個隔離的獨立容器混萝,不受外界干擾或干擾外界
如何觸發(fā)BFC
? float
不為 none ? overflow
的值不為 visible ? position
為 absolute 或 fixed ? display
的值為 inline-block 或 table-cell 或 table-caption 或 grid
BFC的渲染規(guī)則是什么
? BFC是頁面上的一個隔離的獨立容器,不受外界干擾或干擾外界 ? 計算BFC的高度時萍恕,浮動子元素也參與計算(即內(nèi)部有浮動元素時也不會發(fā)生高度塌陷) ? BFC的區(qū)域不會與float的元素區(qū)域重疊 ? BFC內(nèi)部的元素會在垂直方向上放置 ? BFC內(nèi)部兩個相鄰元素的margin會發(fā)生重疊
BFC的應(yīng)用場景
? 清除浮動 :BFC內(nèi)部的浮動元素會參與高度計算逸嘀,因此可用于清除浮動,防止高度塌陷 ? 避免某元素被浮動元素覆蓋 :BFC的區(qū)域不會與浮動元素的區(qū)域重疊 ? 阻止外邊距重疊 :屬于同一個BFC的兩個相鄰Box的margin會發(fā)生折疊允粤,不同BFC不會發(fā)生折疊
總結(jié)
對于前端基礎(chǔ)知識的講解崭倘,到這里就告一小段落翼岁。前端的世界紛繁復(fù)雜,遠非筆者寥寥幾筆所能勾畫司光,筆者就像在沙灘上拾取貝殼的孩童琅坡,有時僥幸拾取收集一二,就為之歡欣鼓舞残家,迫不及待與伙伴們分享榆俺。
最后還想可恥地抒(自)發(fā)(夸)一下(? ̄?? ̄??)??°:
不知不覺,在掘金已經(jīng)水了半年有余坞淮,這半年來我寫下了近6萬字茴晋,不過其實一共只有5篇文章,這是因為我并不想寫水文回窘,不想把基礎(chǔ)的東西水上幾千字幾十篇來混贊升級诺擅。寫下的文章,首先要能說服自己啡直。要對自己寫下的東西負責(zé)任烁涌,即使是一張圖、一個標點酒觅。例如第一張圖撮执,我調(diào)整了不下十次,第一次我直接截取babel的轉(zhuǎn)化結(jié)果阐滩,覺得不好看二打,換成了代碼塊县忌,還是不好看掂榔,又換成了carbon的代碼圖,第一次下載症杏,發(fā)現(xiàn)兩張圖寬度不一樣装获,填充寬度重新下載,又發(fā)現(xiàn)自己的代碼少了一個空格厉颤,重新下載穴豫,為了實現(xiàn)兩張圖并排效果,寫了一個HTML來調(diào)整兩張圖的樣式逼友,為了保證每張圖的內(nèi)容和邊距一致精肃,我一邊截圖,一邊記錄下每次截圖的尺寸和邊距帜乞,每次截圖都根據(jù)上一次的數(shù)據(jù)調(diào)整邊距司抱。
其實我并非提倡把時間花在這些細枝末節(jié)上,只是單純覺得文章沒寫好黎烈,就不能發(fā)出來习柠,就像小野二郎先生說的那樣:“菜做的不好匀谣,就不能拿給客人吃”,世間的大道理资溃,往往都這樣通俗且簡潔武翎。
References
[1] 小冊: https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5c024ecbf265da616a476638
[2] 前端每日一問: https://github.com/sanyuan0704/sanyuan0704.github.io
[3] 如何在 ES5 環(huán)境下實現(xiàn)一個const ?: https://juejin.im/post/5ce3b2d451882533287a767f
[4] 異步編程二三事 | Promise/async/Generator實現(xiàn)原理解析 | 9k字: https://juejin.im/post/5e3b9ae26fb9a07ca714a5cc
[5] V8 是怎么跑起來的 —— V8 的 JavaScript 執(zhí)行管道: https://juejin.im/post/5dc4d823f265da4d4c202d3b
[6] JavaScript 引擎 V8 執(zhí)行流程概述: https://juejin.im/post/5df1ed1f6fb9a015fd69b78d
[7] 聊聊V8引擎的垃圾回收: https://juejin.im/post/5ad3f1156fb9a028b86e78be#heading-10
[8] 為什么V8引擎這么快溶锭?: https://zhuanlan.zhihu.com/p/27628685
[9] 必須明白的瀏覽器渲染機制: https://juejin.im/post/5ce120fbe51d4510a50334fa
[10] 瀏覽器緩存機制剖析: https://juejin.im/post/58eacff90ce4630058668257
[11] HTTP|GET 和 POST 區(qū)別宝恶?網(wǎng)上多數(shù)答案都是錯的: https://juejin.im/entry/597ca6caf265da3e301e64db
[12] www.baidu.com: http://www.baidu.com
[13] MDN的文檔: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers
[14] http發(fā)展史(http0.9、http1.0暖途、http1.1卑惜、http2、http3)梳理筆記: https://juejin.im/post/5dbe8eba5188254fe019dabb#heading-9
[15] 看圖學(xué)HTTPS: https://juejin.im/post/5b0274ac6fb9a07aaa118f49#heading-5
[16] js算法-快速排序(Quicksort): https://segmentfault.com/a/1190000017814119
[17] JS實現(xiàn)堆排序: http://www.reibang.com/p/90bf2dcd6a7b
[18] Redux實現(xiàn)的文章: https://juejin.im/post/5def4831e51d45584b585000#heading-3
了方便進行探討和交流驻售,我為大家建立了一個Q群:1093606290露久,以方便大家學(xué)習(xí)交流。