daily-question(前端每日一題)
在這裡記錄著每天自己遇到的一道印象深刻的前端問題,以及一道生活中隨處可見的小問題。
強迫自己形成積累的習慣,鞭撻自己不斷前行,共同學習。
2019/04/08 - 2019/04/14
-
函式的防抖與節流 ?
防抖
所謂防抖,就是指觸發事件後在 n 秒內函式只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函式執行時間。(防誤觸)
// 延緩執行 function debounce(func, wait) { var timeout; return function() { var context = this; var args = arguments; console.log(args); console.log(func); if (timeout) clearTimeout(timeout); timeout = setTimeout(function() { func.apply(context, args); }, wait); }; } // 立即執行 function debounce(func, wait) { var timeout; return function() { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); var callNow = !timeout; timeout = setTimeout(function() { timeout = null; }, wait); if (callNow) func.apply(context, args); }; }
節流
所謂節流,就是指連續觸發事件但是在 n 秒中只執行一次函式。(限制流量)
// 時間戳 function throttle(func, wait) { var previous = 0; return function() { var now = Date.now(); var context = this; var args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } }; } // 定時器 function throttle(func, wait) { var timeout; return function() { var context = this; var args = arguments; if (!timeout) { timeout = setTimeout(function() { timeout = null; func.apply(context, args); }, wait); } }; }
2019/04/01 - 2019/04/07
-
為何
[] == ![]
結果為true
,而{} == !{}
卻為false
首先了解一下型別轉化的規則:
1、如果有一個運算元是布林值,則在比較相等性之前先將其轉換為數值——false 轉換為 0,而 true 轉換為 1;
2、如果一個運算元是字串,另一個運算元是數值,在比較相等性之前先將字串轉換為數值
3、如果一個運算元是物件,另一個運算元不是,則呼叫物件的 valueOf()(boolean 物件方法)方法或者 toString()方法,用得到的基本型別值按照前面的規則進行比較
null 和 undefined 是相等的
4、要比較相等性之前,不能將 null 和 undefined 轉換成其他任何值
5、如果有一個運算元是 NaN,則相等操作符返回 false ,而不相等操作符返回 true。重要提示:即使兩個運算元都是 NaN,相等操作符也返回 false 了;因為按照規則, NaN 不等於 NaN (NaN 不等於任何值,包括他本身)
6、如果兩個運算元都是物件,則比較它們是不是同一個物件,如果兩個運算元都指向同一個物件,則相等操作符返回 true;否則,返回 false
7、!
可將變數轉換成 boolean 型別,null、undefined、NaN 以及空字串('')取反都為 true,其餘都為 false。
現在開始分析題目
[] == ![]; // 先轉化右邊 ![], // `!`可將變數轉換成 boolean 型別,null、undefined、NaN 以及空字串('')取反都為 true,其餘都為 false。 // 所以 ![] => false => 0 // 左邊 [], 因為[].toString() 為空字串,所以 [] => '' // 綜上, '' == 0, 為 true
{} == !{} // 先轉化右邊 !{}, // `!`可將變數轉換成 boolean 型別,null、undefined、NaN 以及空字串('')取反都為 true,其餘都為 false。 // 所以 !{} => false => 0 // 左邊 ({}).toString() => "[object Object]" // 綜上, "[object Object]" == 0, 為 false
- RESTful 介面的優缺點
什麼是 restful 介面 ?
REST -- REpresentational State Transfer,英語的直譯就是“表現層狀態轉移”,它包含以下三個方面:
URL 設計: RESTful 的核心思想就是,客戶端發出的資料操作指令都是"動詞 + 賓語"的結構。比如,GET /articles 這個命令,GET 是動詞,/articles 是賓語。
動詞通常就是五種 HTTP 方法,對應 CRUD 操作。
- GET:讀取(Read)
- POST:新建(Create)
- PUT:更新(Update)
- PATCH:更新(Update),通常是部分更新
- DELETE:刪除(Delete)
狀態碼: 客戶端的每一次請求,伺服器都必須給出迴應。迴應包括 HTTP 狀態碼和資料兩部分。
伺服器迴應: API 返回的資料格式,不應該是純文字,而應該是一個 JSON 物件,因為這樣才能返回標準的結構化資料。所以,伺服器迴應的 HTTP 頭的Content-Type
屬性要設為application/json。
優點
簡潔明瞭,一目瞭然;輕量,直接通過 http,不需要額外的協議,post/get/put/delete 操作
缺點
當一次更新的內容多的時候需要呼叫更多的介面。刪除也是,如果我想批量刪除呢?
- 對後端開發人員要求高,業務邏輯有時難以被抽象為資源的增刪改查。
- 對前端開發人員不友好,API 粒度較粗,難以查詢符合特殊要求的資料,同樣的業務要比普通的 API 需要更多次 HTTP 請求。
- 對視口 viewport 的理解 ?
視口分為:layout viewport -- 佈局視口,visual viewport -- 視覺視口,ideal viewport -- 理想視口
如果把移動裝置上瀏覽器的可視區域設為 viewport 的話,某些網站就會因為 viewport 太窄而顯示錯亂,所以這些瀏覽器就決定預設情況下把 viewport 設為一個較寬的值,比如 980px,這樣的話即使是那些為桌面設計的網站也能在移動瀏覽器上正常顯示了。這個瀏覽器預設的 viewport 叫做 layout viewport。這個 layout viewport 的寬度可以通過 document.documentElement.clientWidth 來獲取。
layout viewport 的寬度是大於瀏覽器可視區域的寬度的,所以我們還需要一個 visual viewport 來代表瀏覽器可視區域的大小。visual viewport 的寬度可以通過 window.innerWidth 來獲取
ideal viewport 即每個裝置完美適配的視口。所謂的完美適配指的是,第一不需要使用者縮放和橫向滾動條就能正常的檢視網站的所有內容;第二是無論文字,圖片等在不同的裝置都能顯示出差不多的效果。ideal viewport 並沒有一個固定的尺寸,不同的裝置擁有有不同的 ideal viewport。
mata 標籤與 viewport 的關係
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
移動裝置預設的是 layout viewport , 但是我們需要的是 ideal viewport, 那麼通過 meta 標籤的作用就是:讓當前 viewport 的寬度等於裝置的寬度,同時不允許使用者手動縮放。
meta 標籤中 content 的屬性和值如下:
- width 設定 layout viewport 的寬度,為一個正整數,或字串"width-device"
- initial-scale 設定頁面的初始縮放值,為一個數字,可以帶小數
- minimum-scale 允許使用者的最小縮放值,為一個數字,可以帶小數
- maximum-scale 允許使用者的最大縮放值,為一個數字,可以帶小數
- height 設定 layout viewport 的高度,這個屬性對我們並不重要,很少使用
- user-scalable 是否允許使用者進行縮放,值為"no"或"yes", no 代表不允許,yes 代表允許
- 移動端的畫素?
1、物理畫素(裝置畫素)
紅藍綠可以調配出任何顏色,通常說的手機畫素就是由許多紅藍綠組成的一個小塊,1 個小塊表示 1 個畫素。一個物理畫素是顯示器(手機螢幕)上最小的物理顯示單元,通過控制每個畫素點的顏色,使螢幕顯示出不同的影象。螢幕從工廠出來那天起,它上面的物理畫素點就固定不變了,單位 pt - 固定單位。
比如:iPhone6、7、8 的解析度為 1334*750 畫素表示,橫向 750 個畫素,縱向 1334 個畫素
2、CSS 畫素
CSS 和 JS 使用的抽象單位,瀏覽器內的一切長度都是以 CSS 畫素為單位的,CSS 畫素的單位是 px。
一倍屏:當裝置畫素比為 1:1 時,使用 1(1×1)個裝置畫素顯示 1 個 CSS 畫素;
二倍屏:當裝置畫素比為 2:1 時,使用 4(2×2)個裝置畫素顯示 1 個 CSS 畫素;
三倍屏:當裝置畫素比為 3:1 時,使用 9(3×3)個裝置畫素顯示 1 個 CSS 畫素。
3、畫素密度(PPI)
每英寸畫素取值,也就是衡量單位物理面積內擁有畫素值的情況。
ppi 越高,每英寸畫素點越多,影象越清晰;我們可以類比物體的密度,密度越大,單位體積的質量就越大,ppi 越高,單位面積的畫素越多。
ppi 在 120-160 之間的手機被歸為低密度手機,160-240 被歸為中密度,240-320 被歸為高密度,320 以上被歸為超高密度(例如:蘋果的 Retina 螢幕)
-
__proto__
和prototype
的區別 ?-
在 JS 裡,萬物皆物件。方法(Function)是物件,方法的原型(Function.prototype)是物件。因此,它們都會具有物件共有的特點。即:
物件具有屬性
__proto__
,可稱為隱式原型,一個物件的隱式原型指向構造該物件的建構函式的原型,這也保證了例項能夠訪問在建構函式原型中定義的屬性和方法。 - 方法(Function)方法這個特殊的物件,除了和其他物件一樣有上述 proto 屬性之外,還有自己特有的屬性——原型屬性(prototype),這個屬性是一個指標,指向一個物件,這個物件的用途就是包含所有例項共享的屬性和方法(我們把這個物件叫做原型物件)。原型物件也有一個屬性,叫做 constructor,這個屬性包含了一個指標,指回原建構函式。
-
在 JS 裡,萬物皆物件。方法(Function)是物件,方法的原型(Function.prototype)是物件。因此,它們都會具有物件共有的特點。即:
物件具有屬性
-
雙精度浮點數是如何儲存的 ?
在計算機中,浮點表示法,分為三大部分:
第一部分用來儲存符號位(sign),用來區分正負數,0 表示正數
第二部分用來儲存指數(exponent)
第三部分用來儲存小數(fraction), 多出的末尾如果是 1 需要進位;
雙精度浮點數一共佔據 64 位:
符號位(sign)佔用 1 位
指數位(exponent)佔用 11 位
小數位(fraction)佔用 52 位
舉個例子:0.1 的二進位制為
0.00011001100110011001100110011001100110011001100110011001 10011...
轉化為 2 進制科學計數法
1.1001100110011001100110011001100110011001100110011001 * (2 ^ -4);
也就是說 0.1 的:
- 符號位為:0
- 小數位為:1001100110011001100110011001100110011001100110011001
- 指數位為:-4
指數位為負數的怎麼儲存?為了減少不必要的麻煩,IEEE 規定了一個偏移量,對於指數部分,每次都加這個偏移量進行儲存,這樣即使指數是負數,那麼加上這個偏移量也變為正數啦。為了使所有的負指數加上這個偏移量都能夠變為正數,IEEE 規定 1023 為雙精度的偏移量。
因此指數部分為 -4 + 1023 = 1019, 轉化成 11 位二進位制為:01111111011
因此 0.1 在記憶體中的儲存為:
-
如何找出字串中出現最多的字母 (ababccdeajxac)?
最先想到的解法是用 map 紀錄每個字元的次數,然後找出最多的即可:
function getMaxNumberOfChar(str) { return (str + "").split("").reduce( function(pre, cur, index, arr) { cur in pre ? pre[cur]++ : (pre[cur] = 1); pre[cur] > pre.value && ((pre.char = cur), (pre.value = pre[cur])); return pre; }, { value: 0 } ); } getMaxNumberOfChar("ababccdeajxac"); // Object {value: 4, a: 4, char: "a", b: 2, c: 3…}
此外,可以考慮用正則來輔助處理:
function getMaxNumberOfChar(str) { return (str + "") .split("") .sort() .join("") .match(/(\w)\1*/g) .reduce( function(pre, cur) { return cur.length > pre.value ? { value: cur.length, char: cur[0] } : pre; }, { value: 0 } ); } getMaxNumberOfChar("ababccdeajxac"); // Object {value: 4, char: "a"}
這裡拓展一下 reduce 函式的用法
// reduce 函式 // array.reduce(function(accumulator, currentValue, currentIndex, arr), initialValue) // reducer回撥函式本身接受幾個引數,第一個引數是 accumulator 累加器,第二個是陣列中的 item,第三個引數是該項的索引,最後一個引數是原始陣列的引用。 // initialValue 為reduce初始值,否則視陣列第一個值為初始值,選填 const array1 = [1, 2, 3, 4]; // 1 + 2 + 3 + 4 console.log( array1.reduce((accumulator, currentValue) => { console.log(accumulator, currentValue); return accumulator + currentValue; }) );
2019/03/27 - 2019/03/31
-
請問這句語句
var args=[].slice.call(arguments,1)
是什麼意思?先看原函式:
function a() { var args = [].slice.call(arguments, 1); console.log(args); } a("haha", 1, 2, 3, 4, 5); // log出[1, 2, 3, 4, 5] a("run", "-g", "-b"); // log出['-g', '-b']
- 首先,函式 call() 方法,第一個引數改變函式的 this 指向,後面剩餘引數傳入原函式 slice 中
-
arguments 是什麼?
arguments 是函式中的一個類陣列的引數集合物件
-
slice 為陣列可從已有的陣列中返回選定的元素。
此題為從 index = 1 往後
-
綜上,這句語句的作用是——將函式中的實參值轉化成陣列
</details>
-
連等賦值問題
var a = { n: 1 }; var b = a; a.x = a = { n: 2 }; // a ? b ? a.x ? 結果是什麼?
<details>
<summary>點選</summary> 我們可以先嚐試交換下連等賦值順序(a = a.x = {n: 2};),可以發現輸出不變,即順序不影響結果。 那麼現在來解釋物件連等賦值的問題:按照 es5 規範,題中連等賦值等價於 a.x = (a = { n: 2 });,按優先獲取左引用(lref),然後獲取右引用(rref)的順序,a.x 和 a 中的 a 都指向了{ n: 1 }。至此,至關重要或者說最迷惑的一步明確。(a = {n: 2})執行完成後,變數 a 指向{n: 2},**並返回{n: 2}**;接著執行 a.x = { n: 2 },這裡的 a 就是 b(指向{ n: 1 }),所以 b.x 就指向了{ n: 2 }。 賦值時有返回該值, 如 `a = 4 // return 4` , 賦值變數 `let n = 5 //return undefinded` ```js a = { n: 2 }; b = { n: 1, x: { n: 2 } }; a.x = undefinded; ```
-
如何手動實現一個 Promise ?
promise 的三種狀態 pending, resolve, reject
function MyPromise(callback) { let that = this; //定義初始狀態 //Promise狀態 that.status = "pending"; //value that.value = "undefined"; //reason 是一個用於描述Promise被拒絕原因的值。 that.reason = "undefined"; //用來解決非同步問題的陣列 that.onFullfilledArray = []; that.onRejectedArray = []; //定義resolve function resolve(value) { //當status為pending時,定義Javascript值,定義其狀態為fulfilled if (that.status === "pending") { that.value = value; that.status = "resolved"; that.onFullfilledArray.forEach(func => { func(that.value); }); } } //定義reject function reject(reason) { //當status為pending時,定義reason值,定義其狀態為rejected if (that.status === "pending") { that.reason = reason; that.status = "rejected"; that.onRejectedArray.forEach(func => { func(that.reason); }); } } //捕獲callback是否報錯 try { callback(resolve, reject); } catch (error) { reject(error); } } MyPromise.prototype.then = function(onFulfilled, onRejected) { let that = this; //需要修改下,解決非同步問題,即當Promise呼叫resolve之後再呼叫then執行onFulfilled(that.value)。 //用兩個陣列儲存下onFulfilledArray if (that.status === "pending") { that.onFullfilledArray.push(value => { onFulfilled(value); }); that.onRejectedArray.push(reason => { onRejected(reason); }); } if (that.status === "resolved") { onFulfilled(that.value); } if (that.status === "rejected") { onRejected(that.reason); } };
- AST(抽象語法樹)?
什麼是 AST(抽象語法樹)?
它是一種分層程式表示,它根據程式語言的語法呈現原始碼結構,每個 AST 節點對應一個原始碼項。
Babel,Webpack,vue-cli 和 esLint 等很多的工具和庫的核心都是通過 Abstract Syntax Tree (抽象語法樹)這個概念來實現對程式碼的檢查、分析等操作的。
解析(parsing),轉譯(transforming),生成(generation)。
將原始碼解析成 AST 抽象語法樹,再對此語法樹進行相應的轉譯,最後生成我們所需要的程式碼。
第三方的生成 AST 庫有很多,這裡推薦幾個——esprima, babylon(babel 使用)
其轉化的內容大致是這樣的:
{ "type": "Program", "start": 0, "end": 16, "body": [ { "type": "FunctionDeclaration", "start": 0, "end": 16, "id": { "type": "Identifier", "start": 9, "end": 12, "name": "ast" }, "expression": false, "generator": false, "params": [], "body": { "type": "BlockStatement", "start": 14, "end": 16, "body": [] } } ], "sourceType": "module" }
AST 的使用場景
- 程式碼語法的檢查、程式碼風格的檢查、程式碼的格式化、程式碼的高亮、程式碼錯誤提示、程式碼自動補全等等.
- 程式碼混淆壓縮
- 優化變更程式碼,改變程式碼結構使達到想要的結構
-
為什麼
0.1 + 0.2 !== 0.3
?IEEE-754 精度問題
所有使用 IEEE-754 數字實現的程式語言都有這個問題。
0.1 和 0.2 的二進位制浮點數表示並不是精確的,所以相加後不等於 0.3。這個相加的結果接近 0.30000000000000004。
首先將 0.1 轉化為 2 進位制
// 0.1十進位制 -> 二進位制 0.1 * 2 = 0.2取0 0.2 * 2 = 0.4取0 0.4 * 2 = 0.8取0 0.8 * 2 = 1.6取1 0.6 * 2 = 1.2取1 0.2 * 2 = 0.4取0 0.4 * 2 = 0.8取0 0.8 * 2 = 1.6取1 0.6 * 2 = 1.2取1 //0.000110011(0011)`0.1二進位制(0011)裡面的數字表示迴圈
你會發現 0.1 轉二級制會一直無線迴圈下去,根本算不出一個正確的二進位制數。
所以我們得出 0.1 = 0.000110011(0011),那麼 0.2 的演算也基本如上所示,所以得出 0.2 = 0.00110011(0011)
六十四位中符號位佔一位,整數位佔十一位,其餘五十二位都為小數位。因為 0.1 和 0.2 都是無限迴圈的二進位制了,所以在小數位末尾處需要判斷是否進位(就和十進位制的四捨五入一樣)
那麼把這兩個二進位制加起來會得出 0.010011....0100 , 這個值算成十進位制就是 0.30000000000000004