【前端試題】前端之九陽真經 | 每日更新一題(附解析)| 今日試題: 輸出下列代碼執行的結果

新的一天,加油!

每日一道筆試題,遇見不一樣的自己!

第102題:請輸出下列代碼執行的結果

//參考:忍者祕籍第二版
console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')
})
console.log('script end')

解釋:

輸出結果:script start->promise1->promise1 end->script end->promise2->settimeout

當JS主線程執行到Promise對象時,

  • promise1 是 resolved或rejected: 那這個 task 就會放入當前事件循環隊列microtask queue
  • promise1 是 pending: 這個 task 就會放入事件循環隊列未來的某個(可能下一個)回合的 microtask queue
  • setTimeout 的回調也是個 task ,它會被放入macrotask queue,即使是 0ms 的情況

第101題:請輸出下列代碼執行的結果

//參考:忍者祕籍第二版
async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start');
async1();
console.log('script end')

解釋:

輸出結果:script start->async1 start->async2->script end->async1 end
  • async 函數返回一個 Promise 對象(await通過返回一個Promise對象來實現同步的效果),當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的異步操作完成,再執行函數體內後面的語句。可以理解爲,是讓出了線程,跳出了 async 函數體。
  • await的含義爲等待,也就是 async 函數需要等待await後的函數執行完成並且有了返回結果(Promise對象)之後,才能繼續執行下面的代碼。

第100題:請解釋下列三個方法在判斷 是否是數組類型 時的區別:

- Object.prototype.toString.call()
- instanceof
- Array.isArray()
參考:js高級程序設計第三版

解釋

  1. Object.prototype.toString.call()

    每一個繼承 Object 的對象都有 toString 方法,如果 toString 方法沒有重寫的話,會返回 [Object type],其中 type 爲對象的類型。但是,當除了 Object 類型的對象外,其他類型直接使用 toString 方法時,會直接返回內容的字符串,所以我們需要使用call或者apply方法來改變toString方法的執行上下文

例如:

const arr = ['abc','bca'];
arr.toString(); // "abc,bca"
Object.prototype.toString.call(arr); // "[object Array]"
  • 結論:這種方法對於所有基本的數據類型都能進行判斷,即使是 null 和 undefined 。通常,該方法常用於判斷瀏覽器內置對象。
  1. instanceof

    instanceof 的內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype。

    使用 instanceof判斷一個對象是否爲數組,instanceof 會判斷這個對象的原型鏈上是否會找到對應的 Array 的原型,找到返回 true,否則返回 false

    []  instanceof Array; // true
    

    instanceof 只能用來判斷對象類型,原始類型不可以。並且所有對象類型 instanceof Object 都是 true。

    例如:

    []  instanceof Object; // true
    
  2. Array.isArray()

    Array.isArray()是ES5新增的方法,當不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 實現。

    if (!Array.isArray) {
      Array.isArray = function(arg) {
        return Object.prototype.toString.call(arg) === '[object Array]';
      };
    }
    

    同時,Array.isArray()優於instanceof,特別是在檢測Array實例時,Array.isArray可以檢測出iframes下的Array實例。

例如:

let iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
let arr = new xArray(1,2,3); // [1,2,3]
Array.isArray(arr);  // true
arr instanceof Array; // false

第99題:實現下列功能

輸入: [2,3,4,6,7,9]
輸出: '2~4,6~7,9'
const nums = [2,3,4,6,7,9];
function example(num) {
  let result = [];
  let temp = num[0]
  num.forEach((value, index) => {
    if (value + 1 !== num[index + 1]) {
      if (temp !== value) {
        result.push(`${temp}~${value}`)
      } else {
        result.push(`${value}`)
      }
      temp = num[index + 1]
    }
  })
  return result;
}
console.log(example(nums).join(','))

第98題:如何優化瀏覽器的Repaint和Reflow

  • 參考:https://juejin.im/post/5a9923e9518825558251c96a

第97題:解釋下Vue是如何進行雙向數據綁定的?View->Model和Model-View,原理是什麼

  • 參考:https://www.zhihu.com/question/36929146/answer/97611755

第96題:輸出下列代碼執行結果

String('123') == new String('123'); //true,==時做了隱式轉換,調用了toString

String('123') === new String('123');//false,兩者的類型不一樣,前者是string,後者是object
var name = 'abc';
(function() {
    if (typeof name == 'undefined') {
        name = 'cba';
        console.log(name);
    } else {
        console.log(name);
    }
})();
1、首先進入立即執行函數作用域當中,獲取name屬性
2、在當前作用域沒有找到name
3、通過作用域鏈找到最外層,得到name屬性
4、執行else的內容,輸出 abc

第95題:輸出下列代碼執行結果

2 + "3";

3 * "5";

[6, 3] + [3, 6];

"b" + + "c";  

//解釋:

2 + "3";
加性操作符:如果只有一個操作數是字符串,則將另一個操作數轉換爲字符串,然後再將兩個字符串拼接起來

所以值爲:“23”

3 * "5";
乘性操作符:如果有一個操作數不是數值,則在後臺調用 Number()將其轉換爲數值

[6, 3] + [3, 6];
Javascript中所有對象基本都是先調用valueOf方法,如果不是數值,再調用toString方法。

所以兩個數組對象的toString方法相加,值爲:"6,33,6"

"b" + + "c"; 
後邊的“+”將作爲一元操作符,如果操作數是字符串,將調用Number方法將該操作數轉爲數值,如果操作數無法轉爲數值,則爲NaN。

所以值爲:"bNaN"

第94題:寫出如下代碼的打印結果

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
Foo.prototype.a = function() {
    console.log(3)
}
Foo.a = function() {
    console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
  • 解析
function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
// 以上只是 Foo 的構建方法,沒有產生實例,此刻也沒有執行

Foo.prototype.a = function() {
    console.log(3)
}
// 現在在 Foo 上掛載了原型方法 a ,方法輸出值爲 3

Foo.a = function() {
    console.log(4)
}
// 現在在 Foo 上掛載了直接方法 a ,輸出值爲 4

Foo.a();
// 立刻執行了 Foo 上的 a 方法,也就是剛剛定義的,所以
// # 輸出 4

let obj = new Foo();
/* 這裏調用了 Foo 的構建方法。Foo 的構建方法主要做了兩件事:
1. 將全局的 Foo 上的直接方法 a 替換爲一個輸出 1 的方法。
2. 在新對象上掛載直接方法 a ,輸出值爲 2。
*/

obj.a();
// 因爲有直接方法 a ,不需要去訪問原型鏈,所以使用的是構建方法裏所定義的 this.a,
// # 輸出 2

Foo.a();
// 構建方法裏已經替換了全局 Foo 上的 a 方法,所以
// # 輸出 1

第93題:用 JavaScript 寫一個函數,輸入 int 型,返回整數逆序後的字符串。

如:輸入整型 1234,返回字符串“4321”。要求必須使用遞歸函數調用,不能用全局變量,輸入函數必須只有一個參數傳入,必須返回字符串。

function fun(num){
    let num1 = num / 10;
    let num2 = num % 10;
    if(num1<1){
        return num;
    }else{
        num1 = Math.floor(num1)
        return `${num2}${fun(num1)}`
    }
}
var a = fun(12345)
console.log(a)

第92題:寫出如下代碼的打印結果

function changeObjProperty(o) {
  o.siteUrl = "http://www.baidu.com"
  o = new Object()
  o.siteUrl = "http://www.google.com"
} 
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);

輸出:www.baidu.com //原因:函數的形參是值傳遞的

第91題:前端加密的常見場景和方法

加密的目的,簡而言之就是將明文轉換爲密文、甚至轉換爲其他的東西,用來隱藏明文內容本身,防止其他人直接獲取到敏感明文信息、或者提高其他人獲取到明文信息的難度。
通常我們提到加密會想到密碼加密、HTTPS 等關鍵詞

場景-密碼傳輸
前端密碼傳輸過程中如果不加密,在日誌中就可以拿到用戶的明文密碼,對用戶安全不太負責。
這種加密其實相對比較簡單,可以使用 PlanA-前端加密、後端解密後計算密碼字符串的MD5/MD6存入數據庫;也可以 PlanB-直接前端使用一種穩定算法加密成唯一值、後端直接將加密結果進行MD5/MD6,全程密碼明文不出現在程序中。

  • PlanA
    使用 Base64 / Unicode+1 等方式加密成非明文,後端解開之後再存它的 MD5/MD6

  • PlanB
    直接使用 MD5/MD6 之類的方式取 Hash ,讓後端存 Hash 的 Hash 。

場景-數據包加密

應該大家有遇到過:打開一個正經網站,網站底下蹦出個不正經廣告——比如X通的流量浮層,X信的插入式廣告……(我沒有針對誰)
但是這幾年,我們會發現這種廣告逐漸變少了,其原因就是大家都開始採用 HTTPS 了。
被人插入這種廣告的方法其實很好理解:你的網頁數據包被抓取->在數據包到達你手機之前被篡改->你得到了帶網頁廣告的數據包->渲染到你手機屏幕。
而 HTTPS 進行了包加密,就解決了這個問題。嚴格來說我認爲從手段上來看,它不算是一種前端加密場景;但是從解決問題的角度來看,這確實是前端需要知道的事情。

  • Plan
    全面採用 HTTPS

場景-展示成果加密
經常有人開發網頁爬蟲爬取大家辛辛苦苦一點一點發布的數據成果,有些會影響你的競爭力,有些會降低你的知名度,甚至有些出於惡意爬取你的公開數據後進行全量公開……比如有些食譜網站被爬掉所有食譜,站點被克隆;有些求職網站被爬掉所有職位,被拿去賣信息;甚至有些小說漫畫網站賴以生存的內容也很容易被爬取。

  • Plan
    將文本內容進行展示層加密,利用字體的引用特點,把拿給爬蟲的數據變成“亂碼”。
    舉個栗子:正常來講,當我們擁有一串數字“12345”並將其放在網站頁面上的時候,其實網站頁面上顯示的並不是簡單的數字,而是數字對應的字體的“12345”。這時我們打亂一下字體中圖形和字碼的對應關係,比如我們搞成這樣:

圖形:1 2 3 4 5
字碼:2 3 1 5 4

這時,如果你想讓用戶看到“12345”,你在頁面中渲染的數字就應該是“23154”。這種手段也可以算作一種加密。
具體的實現方法可以看一下《Web 端反爬蟲技術方案》

參考

第90題:模擬實現一個深拷貝,並考慮對象相互引用以及 Symbol 拷貝的情況

一個不考慮其他數據類型的公共方法,基本滿足大部分場景

function deepCopy(target, cache = new Set()) {
  if (typeof target !== 'object' || cache.has(target)) {
    return target
  }
  if (Array.isArray(target)) {
    target.map(t => {
      cache.add(t)
      return t
    })
  } else {
    return [...Object.keys(target), ...Object.getOwnPropertySymbols(target)].reduce((res, key) => {
      cache.add(target[key])
      res[key] = deepCopy(target[key], cache)
      return res
    }, target.constructor !== Object ? Object.create(target.constructor.prototype) : {})
  }
}

主要問題是

  1. symbol作爲key,不會被遍歷到,所以stringifyparse是不行的
  2. 有環引用,stringifyparse也會報錯
    我們另外用getOwnPropertySymbols可以獲取symbol key可以解決問題1,用集合記憶曾經遍歷過的對象可以解決問題2。當然,還有很多數據類型要獨立去拷貝。比如拷貝一個RegExp,lodash是最全的數據類型拷貝了,有空可以研究一下

另外,如果不考慮用symbolkey,還有兩種黑科技深拷貝,可以解決環引用的問題,比stringifyparse優雅強一些。

function deepCopyByHistory(target) {
  const prev = history.state
  history.replaceState(target, document.title)
  const res = history.state
  history.replaceState(prev, document.title)
  return res
}

async function deepCopyByMessageChannel(target) {
  return new Promise(resolve => {
    const channel = new MessageChannel()
    channel.port2.onmessage = ev => resolve(ev.data)
    channel.port1.postMessage(target)
  }).then(data => data)
}

無論哪種方法,它們都有一個共性:失去了繼承關係,所以剩下的需要我們手動補上去了,故有Object.create(target.constructor.prototype)的操作

第89題:vue 在 v-for 時給每項元素綁定事件需要用事件代理嗎?爲什麼?

事件代理作用主要是 2 個:

  1. 將事件處理程序代理到父節點,減少內存佔用率
  2. 動態生成子節點時能自動綁定事件處理程序到父節點
//不使用事件代理,每個 span 節點綁定一個 click 事件,並指向同一個事件處理程序
<div>
      <span 
        v-for="(item,index) of 100000" 
        :key="index" 
        @click="handleClick">
        {{item}}
      </span>
 </div>
//不使用事件代理,每個 span 節點綁定一個 click 事件,並指向不同的事件處理程序 
<div>
      <span 
        v-for="(item,index) of 100000" 
        :key="index" 
        @click="function () {}">
        {{item}}
      </span>
</div>
// 使用事件代理
<div  @click="handleClick">
      <span 
        v-for="(item,index) of 100000"  
        :key="index">
        {{item}}
      </span>
</div>
使用事件代理無論是監聽器數量和內存佔用率都比前兩者要少

第88題:給定兩個大小爲m和n的有序數組nums1和nums2。 請你找出這兩個有序數組的中位數,並且要求算法的時間複雜度爲 O(log(m + n))

const findMidNum = function(arr1,arr2) {
    for(let i=0;i<arr2.length;i++) {
        arr1.push(arr2[i]);
    }
    arr1 = arr1.sort((a,b)=>{return b-a;})
    if(arr1.length%2===0) {
        return (arr1[arr1.length/2]+arr1[arr1.length/2-1])/2
    }else {
        return arr1[(arr1.length-1)/2]
    }

}
console.log(findMidNum([1,2],[3,5,6]))

第87題:已知數據格式,實現一個函數 fn 找出鏈條中所有的父級 id

const data = [{
    id: '1',
    name: 'test1',
    children: [
        {
            id: '11',
            name: 'test11',
            children: [
                {
                    id: '111',
                    name: 'test111'
                },
                {
                    id: '112',
                    name: 'test112'
                }
            ]
        },
        {
            id: '12',
            name: 'test12',
            children: [
                {
                    id: '121',
                    name: 'test121'
                },
                {
                    id: '122',
                    name: 'test122'
                }
            ]
        }
    ]
}];

let res = [];

const findId = (list, value) => {
  let len = list.length;

  for (let i in list) {
    const item = list[i];

    if (item.id == value) {
      return res.push(item.id), [item.id];
    }

    if (item.children) {
      if (findId(item.children, value).length) {
        res.unshift(item.id);
        return res;
      }
    }

    if (i == len - 1) {
      return res;
    }
  }
};

第86題:介紹下 HTTPS 中間人攻擊

在這裏插入圖片描述
中間人攻擊過程如下:

  1. 服務器向客戶端發送公鑰。
  2. 攻擊者截獲公鑰,保留在自己手上。
  3. 然後攻擊者自己生成一個【僞造的】公鑰,發給客戶端。
  4. 客戶端收到僞造的公鑰後,生成加密hash值發給服務器。
  5. 攻擊者獲得加密hash值,用自己的私鑰解密獲得真祕鑰。
  6. 同時生成假的加密hash值,發給服務器。
  7. 服務器用私鑰解密獲得假祕鑰。
  8. 服務器用加祕鑰加密傳輸信息 。

防範方法:

服務端在發送瀏覽器的公鑰中加入CA證書,瀏覽器可以驗證CA證書的有效性


第85題:實現模糊搜索結果的關鍵詞高亮顯示

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>auto complete</title>
  <style>
    bdi {
      color: rgb(0, 136, 255);
    }

    li {
      list-style: none;
    }
  </style>
</head>
<body>
  <input class="inp" type="text">
  <section>
    <ul class="container"></ul>
  </section>
</body>
<script>

  function debounce(fn, timeout = 300) {
    let t;
    return (...args) => {
      if (t) {
        clearTimeout(t);
      }
      t = setTimeout(() => {
        fn.apply(fn, args);
      }, timeout);
    }
  }

  function memorize(fn) {
    const cache = new Map();
    return (name) => {
      if (!name) {
        container.innerHTML = '';
        return;
      }
      if (cache.get(name)) {
        container.innerHTML = cache.get(name);
        return;
      }
      const res = fn.call(fn, name).join('');
      cache.set(name, res);
      container.innerHTML = res;
    }
  }

  function handleInput(value) {
    const reg = new RegExp(`\(${value}\)`);
    const search = data.reduce((res, cur) => {
      if (reg.test(cur)) {
        const match = RegExp.$1;
        res.push(`<li>${cur.replace(match, '<bdi>$&</bdi>')}</li>`);
      }
      return res;
    }, []);
    return search;
  }
  
  const data = ["上海野生動物園", "上饒野生動物園", "北京巷子", "上海中心", "上海黃埔江", "迪士尼上海", "陸家嘴上海中心"]
  const container = document.querySelector('.container');
  const memorizeInput = memorize(handleInput);
  document.querySelector('.inp').addEventListener('input', debounce(e => {
    memorizeInput(e.target.value);
  }))
</script>
</html>

第84題:設計並實現 Promise.race()

Promise.myrace = function(iterator) {
    return new Promise ((resolve,reject) => {
        try {
            let it = iterator[Symbol.iterator]();
            while(true) {
                let res = it.next();
                console.log(res);
                if(res.done) break;
                if(res.value instanceof Promise) {
                    res.value.then(resolve,reject);
                } else {
                    resolve(res.value)
                }
                
            }
        } catch (error) {
            reject(error)
        }
    }) 
}

第83題:實現 convert 方法,把原始 list 轉換成樹形結構,要求儘可能降低時間複雜度

先生成新結構map,用原先的結構與其比較,對原結構改造。

function convert(list) {
			const res = []
			const map = list.reduce((res, v) => (res[v.id] = v, res), {})
			for (const item of list) {
				if (item.parentId === 0) {
					res.push(item)
					continue
				}
				if (item.parentId in map) {
					const parent = map[item.parentId]
					parent.children = parent.children || []
					parent.children.push(item)
				}
			}
			return res
		}
		let list =[
		    {id:1,name:'部門A',parentId:0},
		    {id:2,name:'部門B',parentId:0},
		    {id:3,name:'部門C',parentId:1},
		    {id:4,name:'部門D',parentId:1},
		    {id:5,name:'部門E',parentId:2},
		    {id:6,name:'部門F',parentId:3},
		    {id:7,name:'部門G',parentId:2},
		    {id:8,name:'部門H',parentId:4}
		];
		const result = convert(list);
		console.table(result);

第82題:在輸入框中如何判斷輸入的是一個正確的網址

主要解析http,https

function isUrl(url) {
	const a = document.createElement('a')
	a.href = url
	return [
		/^(http|https):$/.test(a.protocol),
		a.host,
		a.pathname !== url,
		a.pathname !== `/${url}`,
	].find(x => !x) === undefined
}

第81題:算法題之–兩數之和

給定一個整數數組和一個目標值,找出數組中和爲目標值的兩個數。
你可以假設每個輸入只對應一種答案,且同樣的元素不能被重複利用。

示例:

給定 nums = [2, 7, 11, 15], target = 9

因爲 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
  • 解析

(1). 直接遍歷兩次數組

//:時間複雜度爲O(N*N)
find2Num([2,7,11,15],9);
function find2Num(arr,sum){
    if(arr == '' || arr.length == 0){
        return false;
    }
 	let result = [];
    for(var i = 0; i < arr.length ; i++){
        for(var j = i + 1; j <arr.length; j++){
            if(arr[i] + arr[j] == sum){
            	result.push(i);
            	result.push(j);
                
            }
        }
    }
    console.log(result);
}

(2)

先將整型數組排序,排序之後定義兩個指針left和right。
left指向已排序數組中的第一個元素,right指向已排序數組中的最後一個元素, 將 arr[left]+arr[right]與
給定的元素比較,若前者大,right–;若前者小,left++; 若相等,則找到了一對整數之和爲指定值的元素。

//時間複雜度爲O(NlogN)
function find2Num(arr,sum){
    if(arr == '' || arr.length == 0){
        return false;
    }
    var left = 0, right = arr.length -1,result = [];
    while(left < right){
        if(arr[left] + arr[right] > sum){
            right--;
        }
        else if(arr[left] + arr[right] < sum){
            left++;
        }
        else{
            console.log(arr[left] + " + " + arr[right] + " = " + sum); 
            result.push(left);
            result.push(right);
            left++;
            right--;
        }
    }
    console.log(result);
}

第80題:react-router 裏的 標籤和 標籤有什麼區別

  • <Link>react-router 裏實現路由跳轉的鏈接,一般配合 <Route> 使用,react-router 接管了其默認的鏈接跳轉行爲,區別於傳統的頁面跳轉,<Link> 的“跳轉”行爲只會觸發相匹配的 <Route> 對應的頁面內容更新,而不會刷新整個頁面。

Link點擊事件handleClick部分源碼:

if (_this.props.onClick) _this.props.onClick(event);

      if (!event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore everything but left clicks
      !_this.props.target && // let browser handle "target=_blank" etc.
      !isModifiedEvent(event) // ignore clicks with modifier keys
      ) {
          event.preventDefault();

          var history = _this.context.router.history;
          var _this$props = _this.props,
              replace = _this$props.replace,
              to = _this$props.to;


          if (replace) {
            history.replace(to);
          } else {
            history.push(to);
          }
        }

Link做了3件事情:

  1. 有onclick那就執行onclick
  2. click的時候阻止a標籤默認事件(這樣子點擊<a href="/abc">123</a>就不會跳轉和刷新頁面)
  3. 再取得跳轉href(即是to),用history(前端路由兩種方式之一,history & hash)跳轉,此時只是鏈接變了,並沒有刷新頁面
  • <a> 標籤就是普通的超鏈接了,用於從當前頁面跳轉到 href 指向的另一個頁面(非錨點情況)。

如何禁掉 <a> 標籤默認事件,禁掉之後如何實現跳轉

  • 禁掉 a 標籤的默認事件,可以在點擊事件中執行 event.preventDefault();
  • 禁掉默認事件的 a 標籤 可以使用 history.pushState() 來改變頁面 url,這個方法還會觸發頁面的 hashchange 事件,Router 內部通過捕獲監聽這個事件來處理對應的跳轉邏輯。

第79題:柯里化函數

實現一個Add函數,滿足以下功能:
add(1); // 1
add(1)(2); // 3
add(1)(2)(3); // 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6

  • 解析:
function add(){
	let args = [...arguments];
	let addfun = function(){
		args.push(...arguments);
		return addfun;
	}
	addfun.toString = function(){
		return args.reduce((a,b)=>{
			return a + b;
		});
	}
	return addfun;
}

參考:

第78題:給定一個數組 nums,編寫一個函數將所有 0 移動到數組的末尾,同時保持非零元素的相對順序

const result = (arr) => arr.filter(Boolean).concat([...Array(arr.length - arr.filter(Boolean).length).fill(0)])
console.log(result([0,1,0,3,0,12,0,0]))
const arr = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, 1, 9, 9, 9, 0, 0, 0, 0, 1, 0, 3, 12, 0, 0, 0, 0];
    const len = arr.length;
    console.log(len)
    for (let i = len; i >= 0; i--) {
      if (arr[i] === 0) {
        arr.splice(i, 1);
        arr.push(0)
      }
    }
console.log(arr)

第77題:打印出 1 - 10000 之間的所有對稱數, 例如:121、1331 等

[...Array(10000).keys()].filter((x) => { 
  return x.toString().length > 1 && x === Number(x.toString().split('').reverse().join('')) 
})

在這裏插入圖片描述

let result=[]
for(let i=1;i<10;i++){
    result.push(i)
    result.push(i*11)
    for(let j=0;j<10;j++){
        result.push(i*101+j*10)
        result.push(i*1001+j*110)
    }
}

這個方法把1~9考慮在內:
在這裏插入圖片描述

第76題: Promise.all 的使用、原理實現及錯誤處理

Promise.all(iterable) 方法返回一個 Promise 實例,此實例在 iterable 參數內所有的 promise 都“完成(resolved)”或參數中不包含 promise 時回調完成(resolve);如果參數中 promise 有一個失敗(rejected),此實例回調失敗(reject),失敗原因的是第一個失敗 promise 的結果。

  • 如果傳入的參數是一個空的可迭代對象,則返回一個已完成(already resolved)狀態的 Promise。

  • 如果傳入的參數不包含任何 promise,則返回一個異步完成(asynchronously resolved) Promise。注意:Google Chrome 58 在這種情況下返回一個已完成(already resolved)狀態的 Promise。

  • 其它情況下返回一個處理中(pending)的Promise。這個返回的 promise 之後會在所有的 promise 都完成或有一個 promise 失敗時異步地變爲完成或失敗。 見下方關於“Promise.all 的異步或同步”示例。返回值將會按照參數內的 promise 順序排列,而不是由調用 promise 的完成順序決定。

  • 使用:

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
}); 

Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // [3, 1337, "foo"] 
});
  • 錯誤處理:
var p1 = new Promise((resolve, reject) => { 
  setTimeout(resolve, 1000, 'one'); 
}); 
var p2 = new Promise((resolve, reject) => { 
  setTimeout(resolve, 2000, 'two'); 
});
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, 'three');
});
var p4 = new Promise((resolve, reject) => {
  setTimeout(resolve, 4000, 'four');
});
var p5 = new Promise((resolve, reject) => {
  reject('reject');
});

Promise.all([p1, p2, p3, p4, p5]).then(values => { 
  console.log(values);
}, reason => {
  console.log(reason)
});

//From console:
//"reject"

//You can also use .catch
Promise.all([p1, p2, p3, p4, p5]).then(values => { 
  console.log(values);
}).catch(reason => { 
  console.log(reason)
});

//From console: 
//"reject"

Promise.all任意一個傳入的 promise 失敗時返回失敗。例如,如果你傳入的 promise中,有四個 promise 在一定的時間之後調用成功函數,有一個立即調用失敗函數,那麼 Promise.all 將立即變爲失敗。

參考:


第75題:input 搜索 如何處理中文輸入

觸發compositionstart時,文本框會填入 “虛擬文本”(待確認文本),同時觸發input事件;在觸發compositionend時,就是填入實際內容後已確認文本)。例如:中文輸入法輸入內容時還沒將中文插入到輸入框就驗證的問題,
爲此,我們可以在中文輸入完成以後才驗證。即:在compositionend發生後再進行邏輯的處理:

var cpLock = true;
$('.com_search_input').on('compositionstart', function () {
        cpLock = false;
//      console.log("compositionstart")
    });
    $('.com_search_input').on('compositionend', function () {
        cpLock = true;
//      console.log("compositionend")
    });
$(".com_search_input").on("input",function(e){
e.preventDefault();
var _this = this;
// console.log("input");
setTimeout(function(){
if (cpLock) {
//開始寫邏輯
  console.log("邏輯")
}
},0)
})

使用延時器的原因:

因爲選詞結束的時候input會比compositionend先一步觸發,此時cpLock還未調整爲true,所以不能觸發到console.log(“邏輯”),故用setTimeout將其優先級滯後。

第74題:Vue 的父組件和子組件生命週期鉤子執行順序是什麼

  • 加載渲染過程:

    父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

  • 子組件更新過程:

    父beforeUpdate->子beforeUpdate->子updated->父updated

  • 父組件更新過程:

    父beforeUpdate->父updated

  • 銷燬過程:

    父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

第73題:旋轉數組

問題描述:

將包含* n* 個元素的數組向右旋轉 *k *步。
例如,如果 n = 7 , k = 3,給定數組 [1,2,3,4,5,6,7] ,向右旋轉後的結果爲 [5,6,7,1,2,3,4]。

解析:利用解構數組和數組的splice方法

function rotateArr(arr,k) {
	return [...arr.splice(k+1),...arr];
}
rotateArr([1,2,3,4,5,6,7],3);

在這裏插入圖片描述

第72題:對象的鍵名的轉換

  1. 對象的鍵名只能是字符串和 Symbol 類型
  2. 其他類型的鍵名會被轉換成字符串類型
  3. 對象轉字符串默認會調用 toString 方法

考察:下列代碼輸出結果:

// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c'; // c 的鍵名會被轉換成字符串'123',這裏會把 b 覆蓋掉。
console.log(a[b]); // 'c'
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';// b 是 Symbol 類型,不需要轉換
a[c]='c';// c 是 Symbol 類型,不需要轉換。任何一個 Symbol 類型的值都是不相等的,所以不會覆蓋掉 b
console.log(a[b]);//'b'
// example 3
var a={}, b={key:'123'}, c={key:'456'};
// b 不是字符串也不是 Symbol 類型,需要轉換成字符串。
// 對象類型會調用 toString 方法轉換成字符串 [object Object]。
a[b]='b';
// c 不是字符串也不是 Symbol 類型,需要轉換成字符串。
// 對象類型會調用 toString 方法轉換成字符串 [object Object]。這裏會把 b 覆蓋掉。
a[c]='c';
console.log(a[b]);//'c'

第71題:數組裏面有10萬個數據,取第一個元素和第10萬個元素的時間相差多少

在jsperf中進行了測試,點擊查看。
在這裏插入圖片描述

第70題:用Proxys實現雙向數據綁定

參考

第69題:BFC、IFC、GFC和FFC

FC的全稱是:Formatting Contexts,是W3C CSS2.1規範中的一個概念。它是頁面中的一塊渲染區域,並且有一套渲染規則,它決定了其子元素將如何定位,以及和其他元素的關係和相互作用。

BFC

塊級格式化上下文:就是頁面上的一個隔離的渲染區域,容器裏面的子元素不會在佈局上影響到外面的元素,反之也是如此。怎樣會產生BFC:

  • float的值不爲none。
  • overflow的值不爲visible。
  • position的值不爲relative和static。
  • display的值爲table-cell, table-caption, inline-block中的任何一個。

IFC

內聯格式化上下文:高度由其包含行內元素中最高的實際高度計算而來(不受到豎直方向的padding/margin影響)

IFC中的line box一般左右都貼緊整個IFC,但是會因爲float元素而擾亂。float元素會位於IFC與與line box之間,使得line box寬度縮短。 同個ifc下的多個line box高度會不同。 IFC中不可能有塊級元素的,當插入塊級元素時(如p中插入div)會產生兩個匿名塊與div分隔開,即產生兩個IFC,每個IFC對外表現爲塊級元素,與div垂直排列。

  • IFC一般有什麼用:
    水平居中:當一個塊要在環境中水平居中時,設置其爲inline-block則會在外層產生IFC,通過text-align則可以使其水平居中。
    垂直居中:創建一個IFC,用其中一個元素撐開父元素的高度,然後設置其vertical-align:middle,其他行內元素則可以在此父元素下垂直居中。

GFC

網格佈局格式化上下文:當爲一個元素設置display值爲grid的時候,此元素將會獲得一個獨立的渲染區域,我們可以通過在網格容器(grid container)上定義網格定義行(grid definition rows)和網格定義列(grid definition columns)屬性各在網格項目(grid item)上定義網格行(grid row)和網格列(grid columns)爲每一個網格項目(grid item)定義位置和空間

GridLayout會有更加豐富的屬性來控制行列,控制對齊以及更爲精細的渲染語義和控制

FFC

自適應格式化上下文display值爲flex或者inline-flex的元素將會生成自適應容器(flex container),可惜這個牛逼的屬性只有谷歌和火狐支持,不過在移動端也足夠了,至少safari和chrome還是OK的,畢竟這倆在移動端纔是王道。

Flex Box 由伸縮容器和伸縮項目組成。通過設置元素的 display 屬性爲 flex 或 inline-flex 可以得到一個伸縮容器。設置爲 flex 的容器被渲染爲一個塊級元素,而設置爲 inline-flex 的容器則渲染爲一個行內元素。

伸縮容器中的每一個子元素都是一個伸縮項目。伸縮項目可以是任意數量的。伸縮容器外和伸縮項目內的一切元素都不受影響。簡單地說,Flexbox 定義了伸縮容器內伸縮項目該如何佈局。


第68題:for與forEach性能

  • for 循環沒有任何額外的函數調用棧和上下文;

  • forEach函數簽名實際上是

array.forEach(function(currentValue, index, arr), thisValue)

它不是普通的 for 循環的語法糖,還有諸多參數和上下文需要在執行的時候考慮進來,這裏可能拖慢性能;

參考


第67題:在字符串中查找匹配的字符串

const findStr = (s,t) => {
		let posArr = [];
		let index = s.search(t);
		while(index !== -1) {
			posArr.push(index);
			index = s.indexOf(t,index+t.length);
		}
		console.log(posArr);
	}
findStr('sdfsdf123er123','123');//[6,11]

第66題:webpack熱更新原理

參考

  • https://zhuanlan.zhihu.com/p/30669007

第65題:字符串大小寫操作

如何把一個字符串的大小寫取反(大寫變小寫小寫變大寫),例如 ’AbC’ 變成 ‘aBc’

	function trans2Case(str) {
		let arr = str.split('');
		arr = arr.map((item)=>{
			return item === item.toUpperCase() ? item.toLowerCase() : item.toUpperCase();
		});
		return arr.join('');
	}
	console.log(trans2Case('AbC'))

第64題: 隨機生成一個長度爲 10 的整數類型的數組,例如 [2, 10, 3, 4, 5, 11, 10, 11, 20],將其排列成一個新數組,要求新數組形式如下,例如 [[2, 3, 4, 5], [10, 11], [20]]。

function Array2Group(len) {
	//生成隨機整數型數組
	let arr = Array.from({length:len},(f)=>{return Math.floor(Math.random()*100)})
	arr = arr.sort((a,b)=>{//升序排序
		return a-b;
	})
	//去重
	arr = arr.filter((item,index)=>{
		return item !== arr[index+1];
	})
	
	//分組,將連續的放在一組
	let continueArr = [], tempArr = [];
	arr.map((item,index)=>{
	 	tempArr.push(item);
	 	if(arr[index+1] !== ++item) {
	 		continueArr.push(tempArr);
	 		tempArr = [];
	 	}
	})
	console.log(continueArr)
}

Array2Group(9);

第63題:Babe是將ES6轉換爲ES5的原理

Babel的功能非常純粹,以字符串的形式將源代碼傳給它,它就會返回一段新的代碼字符串(以及sourcemap)。他既不會運行你的代碼,也不會將多個代碼打包到一起,它就是個編譯器,輸入語言是ES6+,編譯目標語言是ES5。

Babel的編譯過程跟絕大多數其他語言的編譯器大致同理,分爲三個階段:

  1. 解析:將代碼字符串解析成抽象語法樹
  2. 變換:對抽象語法樹進行變換操作
  3. 再建:根據變換後的抽象語法樹再生成代碼字符串

第1步轉換的過程中可以驗證語法的正確性,同時由字符串變爲對象結構後更有利於精準地分析以及進行代碼結構調整。

第2步原理就很簡單了,就是遍歷這個對象所描述的抽象語法樹,遇到哪裏需要做一下改變,就直接在對象上進行操作,比如我把IfStatement給改成WhileStatement就達到了把條件判斷改成循環的效果。在.babelrc裏配置的presets和plugins都是在第2步工作的。

第3步也簡單,遞歸遍歷這顆語法樹,然後生成相應的代碼

  • 抽象語法樹是如何產生的:
  1. 分詞:將整個代碼字符串分割成 語法單元 數組
  2. 語義分析:在分詞結果的基礎之上分析 語法單元之間的關係

參考

  • https://zhuanlan.zhihu.com/p/27289600
  • http://www.ruanyifeng.com/blog/2016/01/babel.html
  • https://moyueating.github.io/2017/07/08/%E6%B5%85%E8%B0%88babel%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8A%E4%BD%BF%E7%94%A8/

第62題:a.b.c.d和a[‘b’][‘c’][‘d’],哪個性能更高

a[‘b’][‘c’]和a.b.c,轉換成AST前者的的樹是含計算的,後者只是string literal,天然前者會消耗更多的計算成本,時間也更長

參考


第 61 題: 模擬實現promise的finally方法

promise.finally方法用於指定不管 Promise 對象最後狀態如何,都會執行的操作。即finally方法裏面的操作,應該是與狀態無關的,不依賴於 Promise 的執行結果

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代碼中,不管promise最後的狀態,在執行完then或catch指定的回調函數以後,都會執行finally方法指定的回調函數。

實現:

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

參考


第 60 題: token加密的實現原理

1、後端生成一個secret(隨機數)
2、後端利用secret和加密算法(如:HMAC-SHA256)對payload(如賬號密碼)生成一個字符串(token),返回前端
3、前端每次request在header中帶上token
4、後端用同樣的算法解密

參考


第 59 題: !important

不改變下面代碼的情況下,設置width爲330px
<img src="1.jpg" style="width:480px!important;”>

img {max-width:330px;}

第 58 題:求兩個數組的交集

  • filter、include方法
ar nums1 = [1, 2, 2, 1], nums2 = [2, 2, 3, 4];
// 1.
// 有個問題, [NaN].indexOf(NaN) === -1
var newArr1 = nums1.filter(function(item) {
     return nums2.indexOf(item) > -1;
});
console.log(newArr1);
// 2.
var newArr2 = nums1.filter((item) => {
     return nums2.includes(item);
});
console.log(newArr2);

第 57 題:箭頭函數與普通函數(function)的區別是什麼?構造函數(function)可以使用 new 生成實例,那麼箭頭函數可以嗎?爲什麼?

箭頭函數是普通函數的簡寫,和普通函數相比,有以下幾點差異:

  1. 函數體內的 this 對象,就是定義時所在的對象,而不是使用時所在的對象。
  2. 不可以使用 arguments 對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。
  3. 不可以使用 yield 命令,因此箭頭函數不能用作 Generator 函數。
  4. 不可以使用 new 命令,因爲沒有自己的 this,無法調用 call,apply。
    同時,沒有 prototype 屬性 ,而 new 命令在執行時需要將構造函數的 prototype 賦值給新的對象的 __proto__,new 過程大致如下:
function newFunc(father, ...rest) {
  var result = {};
  result.__proto__ = father.prototype;
  var result2 = father.apply(result, rest);
  if (
    (typeof result2 === 'object' || typeof result2 === 'function') &&
    result2 !== null
  ) {
    return result2;
  }
  return result;
}

new運算符

js中,。new運算符創建了一個繼承於其運算數的原型的新對象,然後調用該運算數,把新創建的對象綁定給this。
如果你忘記使用new運算符,你得到的是一個普通的函數調用,並且this被綁定到全局對象,而不是新創建的對象。這意味着當你的函數嘗試去初始化新成員屬性時它將會污染全局變量。

如果你要使用new運算符,與new結合使用的函數應該以首字母大寫的形式命名,並且首字母大寫的形式應該只用來命名那些構造函數。

一個更好的做法是,不去使用new。

第 56 題:分析比較 opacity: 0、visibility: hidden、display: none 優劣和適用場景

  • 結構:
    display:none: 會讓元素完全從渲染樹中消失,渲染的時候不佔據任何空間, 不能點擊,
    visibility: hidden:不會讓元素從渲染樹消失,渲染元素繼續佔據空間,只是內容不可見,可以點擊
    opacity: 0: 不會讓元素從渲染樹消失,渲染元素繼續佔據空間,只是內容不可見,可以點擊

  • 繼承:
    display: none和opacity: 0:是非繼承屬性,子孫節點消失由於元素從渲染樹消失造成,通過修改子孫節點屬性無法顯示。
    visibility: hidden:是繼承屬性,子孫節點消失由於繼承了hidden,通過設置visibility: visible;可以讓子孫節點顯式。

  • 性能:
    displaynone : 修改元素會造成文檔迴流,讀屏器不會讀取display: none元素內容,性能消耗較大
    visibility:hidden: 修改元素只會造成本元素的重繪,性能消耗較少讀屏器讀取visibility: hidden元素內容
    opacity: 0 : 修改元素會造成重繪,性能消耗較少


第55題:class設計,設計LazyMan類,實現下述功能

LazyMan('Tony');
// Hi I am Tony

LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch

LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner

LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
  • 解析
class LazyManClass {
	constructor(name) {
		this.taskList = [];
		this.name = name;
		console.log('Hi I am',this.name);
		let that = this;
		setTimeout(()=>{
			that.next();
		},0);
	}
	eat(name) {
		let that = this;
		let fn = (function(name) {
			return function() {
				console.log('I am eating',name);
				that.next();
			}
		})(name);
		that.taskList.push(fn);
		console.log(that.taskList)
		return that;
	}
	sleepFirst(time) {
		let that = this;
		let fn = (function(time) {
			return function() {
				setTimeout(()=>{
					console.log(`等待了${time}秒`);
					that.next();
				},time*1000);
			}
		})(time);
		that.taskList.unshift(fn);
		console.log(that.taskList)
		return that;
		
	}
	sleep(time) {
		let that = this;
		let fn = (function(time){
			return function() {
				setTimeout(()=>{
					console.log(`等待了${time}秒`);
					that.next();
				},time*1000);
			}
		})(time);
		that.taskList.push(fn);
		console.log(that.taskList)
		return that;
	}
	next() {
		let that = this;
		let fn = that.taskList.shift();
		console.log(that.taskList)
		if(typeof fn ==='function')
			fn && fn()
	}
}
function LazyMan(name){
	return new LazyManClass(name);
}
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');

第 54 題:某公司 1 到 12 月份的銷售額存在一個對象裏面,如下:{1:222, 2:123, 5:888},請把數據處理爲如下結構:[222, 123, null, null, 888, null, null, null, null, null, null, null

  • Array.from():方法從一個類似數組或可迭代對象中創建一個新的數組實例,例如:
console.log(Array.from([1, 2, 3], x => x + x));
// expected output: Array [2, 4, 6]
  • 解析問題
let obj = {1:222, 2:123, 5:888};
const result = Array.from({ length: 12 })         //先生成一個長度爲12的數組,裏面的值爲undefined
					.map((_, index) => obj[index + 1] || null); //然後對數組賦值
console.log(result)

參考

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from

第 53 題:冒泡排序如何實現,時間複雜度是多少, 如何改進?

冒泡排序這個算法的名字由來是因爲越大的元素會經由交換慢慢“浮”到數列的頂端(升序或降序排列),就如同碳酸飲料中二氧化碳的氣泡最終會上浮到頂端一樣,故名“冒泡排序”。

冒泡排序算法的原理如下:

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  2. 對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
  3. 針對所有的元素重複以上的步驟,除了最後一個。
  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

冒泡排序最好的時間複雜度爲 O(n),冒泡排序的最壞時間複雜度爲 O(n^2) ,冒泡排序總的平均時間複雜度爲 O(n^2) ,是一種穩定排序算法。

以升序爲例:

// 升序冒泡
function bubbleSort(arr){
  const array = [...arr]
  for(let i = 0, len = array.length; i < len - 1; i++){
    for(let j = i + 1; j < len; j++) {
      if (array[i] > array[j]) {
        let temp = array[i]
        array[i] = array[j]
        array[j] = temp
      }
    }
  }
  return array
}

上述這種算法不太好,因爲就算你給一個已經排好序的數組,如[1,2,3,4,5,6] 它也會走一遍流程,白白浪費資源。

  • 改進:加個標識,如果已經排好序了就直接跳出循環。
functionbubbleSort(arr){
  const array = [...arr]
  let isOk = true
  for(let i = 0, len = array.length; i < len - 1; i++){
    for(let j = i + 1; j < len; j++) {
      if (array[i] > array[j]) {
        let temp = array[i]
        array[i] = array[j]
        array[j] = temp
        isOk = false
      }
    }
    if(isOk){
      break
    }
  }
  return array
}

參考

  • https://baike.baidu.com/item/%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F/4602306?fr=aladdin
  • https://www.jianshu.com/p/5d44186b5263

第 52 題:執行順序優先級

下列代碼的執行結果:

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) 	  //undefined
console.log(b.x)    // {n:2}
  • 1、優先級。.的優先級高於=,所以先執行a.x,堆內存中的{n: 1}就會變成{n: 1, x: undefined},改變之後相應的b.x也變化了,因爲指向的是同一個對象
  • 2、賦值操作是從右到左,所以先執行a = {n: 2},a的引用就被改變了,然後這個返回值又賦值給了a.x,需要注意的是這時候a.x是第一步中的{n: 1, x: undefined}那個對象,其實就是b.x,相當於b.x = {n: 2}

第 51 題:讓一個 div 水平垂直居中

// no.1
div.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}
// no.2
div.parent {
    position: relative; 
}
div.child {
    position: absolute; 
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);  
}
//no.3
div.child {
    width: 50px;
    height: 10px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-left: -25px;
    margin-top: -5px;
}
//no.4
div.child {
    width: 50px;
    height: 10px;
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}
//no.5
div.parent {
    display: grid;
}
div.child {
    justify-self: center;
    align-self: center;
}
//no.6
div.parent{
  display:flex;
}
div.child{
  margin:auto;
}

第50題:Vue 的響應式原理中 Object.defineProperty 有什麼缺陷?爲什麼在 Vue3.0 採用了 Proxy,拋棄了 Object.defineProperty?

Object.defineProperty無法監控到數組下標的變化,導致直接通過數組的下標給數組設置值,不能實時響應。 爲了解決這個問題,經過vue內部處理後可以使用以下幾種方法來監聽數組:

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

由於只針對了以上八種方法進行了hack處理,所以其他數組的屬性也是檢測不到的,還是具有一定的侷限性。

Object.defineProperty只能劫持對象的屬性,因此我們需要對每個對象的每個屬性進行遍歷。Vue 2.x裏,是通過 遞歸 +
遍歷 data 對象來實現對數據的監控的,如果屬性值也是對象那麼需要深度遍歷,顯然如果能劫持一個完整的對象是纔是更好的選擇。

而要取代它的Proxy有以下如下優點;:

  • 可以劫持整個對象,並返回一個新對象
  • 有13種劫持操作
  • Proxy不僅可以代理對象,還可以代理數組。還可以代理動態增加的屬性。

參考

  • https://www.jianshu.com/p/860418f0785c
  • https://juejin.im/post/5acd0c8a6fb9a028da7cdfaf
  • http://www.10tiao.com/html/780/201812/2650588659/1.html
  • http://es6.ruanyifeng.com/#docs/proxy
  • https://zhuanlan.zhihu.com/p/35080324

第49題:實現 (5).add(3).minus(2) 功能,例: 5 + 3 - 2,結果爲 6

Number.MAX_SAFE_DIGITS = Number.MAX_SAFE_INTEGER.toString().length-2
Number.prototype.digits = function(){
	let result = (this.valueOf().toString().split('.')[1] || '').length
	return result > Number.MAX_SAFE_DIGITS ? Number.MAX_SAFE_DIGITS : result
}
Number.prototype.add = function(i=0){
	if (typeof i !== 'number') {
        	throw new Error('請輸入正確的數字');
    	}
	const v = this.valueOf();
	const thisDigits = this.digits();
	const iDigits = i.digits();
	const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
	const result = (v * baseNum + i * baseNum) / baseNum;
	if(result>0){ return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result }
	else{ return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result }
}
Number.prototype.minus = function(i=0){
	if (typeof i !== 'number') {
        	throw new Error('請輸入正確的數字');
    	}
	const v = this.valueOf();
	const thisDigits = this.digits();
	const iDigits = i.digits();
	const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
	const result = (v * baseNum - i * baseNum) / baseNum;
	if(result>0){ return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result }
	else{ return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result }
}
  • 大數加減:直接通過 Number 原生的安全極值來進行判斷,超出則直接取安全極值
  • 超級多位數的小數加減:取JS安全極值位數-2作爲最高兼容小數位數

第 48 題:爲什麼通常在發送數據埋點請求的時候使用的是 1x1 像素的透明 gif 圖片

  1. 避免跨域(img 天然支持跨域)
  2. 利用空白gif或1x1 px的img是互聯網廣告或網站監測方面常用的手段,簡單、安全、相比PNG/JPG體積小,1px 透明圖,對網頁內容的影響幾乎沒有影響,這種請求用在很多地方,比如瀏覽、點擊、熱點、心跳、ID頒發等等,
  3. 圖片請求不佔用 Ajax 請求限額
  4. 不會阻塞頁面加載,影響用戶的體驗,只要new Image對象就好了,一般情況下也不需要append到DOM中,通過它的onerror和onload事件來檢測發送狀態。

示例:

<script type="text/javascript">
 var thisPage = location.href;
 var referringPage = (document.referrer) ? document.referrer : "none";
 var beacon = new Image();
 beacon.src = "http://www.example.com/logger/beacon.gif?page=" + encodeURI(thisPage)
 + "&ref=" + encodeURI(referringPage);
</script>

參考

  • https://segmentfault.com/a/1190000015863478
  • https://blog.csdn.net/zmx729618/article/details/58600620/
  • https://www.zhihu.com/question/25488619?sort=created
  • https://www.jishuwen.com/d/2CQv

第 47 題:call 和 apply 的區別是什麼,哪個性能更好一些

  1. Function.prototype.apply和Function.prototype.call 的作用是一樣的,區別在於傳入參數的不同;
  2. 第一個參數都是,指定函數體內this的指向
  3. 第二個參數開始不同,apply是傳入帶下標的集合,數組或者類數組,apply把它傳給函數作爲參數,call從第二個開始傳入的參數是不固定的,都會傳給函數作爲參數。
    例如:
fun.apply(thisArg, [argsArray])
fun.call(thisArg, arg1, arg2, ...)
  1. call比apply的性能要好,平常可以多用call, call傳入參數的格式正是內部所需要的格式

參考

  • https://segmentfault.com/a/1190000012772040

第 46 題:雙向綁定和 vuex 是否衝突

官方文檔解釋:https://vuex.vuejs.org/zh/guide/forms.html


第 45 題:輸出以下代碼執行的結果並解釋爲什麼

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
  • 解釋:我們現在控制檯輸入並輸出,對照來看:
    在這裏插入圖片描述
    很顯然,從結果上對照,obj.push(1) 訪問的是對象obj的push屬性,該push屬性具有數組的push方法,因此,obj.push(1)和obj.push(2)會向Array中追加並返回修改後的數組。但是,問題來了!怎麼追加?往哪個位置追加?
    我們看到打印輸出時,obj是一個數組形式輸出,但’obj instanceof Array’又不是一個真實的數組,所以這是一個類數組形式,我們給這個類數組起一個名稱S_Arr
    細心的你,一定注意到empty x 2,即輸出了2個空。所以,我們對此可知,在進行obj.push(1)和obj.push(2)操作時,S_Arr[2]=1,S_Arr[3]=2,可見S_Arr[0]和S_Arr[1]爲empty,但這兩個下標仍舊佔位。現在,可以解釋爲什麼length爲4了吧。

第 44 題:HTTPS 握手過程中,客戶端如何驗證證書的合法性

瀏覽器和系統會內置默認信任的證書。

如果只劫持了站點返回自己簽發的別的證書,證書因爲與域名不符會驗證失敗。證書是沒法僞造的,因爲你沒有證書的私鑰。除非用給原證書籤名的根證書重新給你簽發一個同域名的證書。

rsa 加密唯一的缺點就是不能防中間人攻擊。所以系統和瀏覽器內置了信任證書。像 ssh 連接第一次會提示指紋一樣。你得先信任指紋才能繼續操作,指紋發生改變後就會提示你指紋錯誤。

除此之外,證書頒發機構會驗證域名所有權,你得證明域名的所有權在你手裏:

  • 你往網站根目錄放置一個機構提供的特定的文件,然後機構會定時抓取這個文件,如果能抓取到說明你確實有這個網站的管理權限。注意,只支持 80 和 443 端口,8080 登端口不認。

  • 機構往域名信息裏的管理員郵箱發一封驗證郵件,郵件裏有驗證鏈接,域名管理員要點開鏈接輸入驗證碼確認。

  • 要求你在域名 DNS 控制面板裏添加一條特定的域名記錄。

以上操作都只有域名管理員或者網站管理員才能做到,避免了他人僞造證書。

還有一點很重要的:
如果發現申請的域名包含知名品牌、知名網站域名,證書機構會人工審覈,有可能會要求你提供相關證明文件。比如我想申請 www.nikeshop.com 的證書,因爲包含 Nike 字樣,極有可能會被拒絕。

參考

  • https://www.v2ex.com/amp/t/411144
  • https://www.cnblogs.com/StephenWu/p/5720954.html
  • https://blog.csdn.net/love_hot_girl/article/details/81164279

第 43題:介紹 HTTPS 握手過程

建議去看看計算機網絡講解HTTP,然後再看HTTPS。

HTTPS三次握手

握手要解決的問題

  • 客戶端和服務器身份的互相確認
  • 協商之後通信中對稱加密的祕鑰

握手流程
在這裏插入圖片描述

  • 步驟1: 客戶端發出請求(ClientHello)
    首先,客戶端(通常是瀏覽器)先向服務器發出加密通信的請求,這被叫做ClientHello請求。
    在這一步,客戶端主要向服務器提供以下信息。
  1. 客戶端支持的SSL的指定版本
  2. 客戶端產生的隨機數(Client Random, 稍後用於生成"對話密鑰"
  3. 客戶端支持的加密算法
  • 步驟2:服務器迴應(SeverHello)
    服務器收到客戶端請求後,向客戶端發出迴應,這叫做SeverHello。服務器的迴應包含以下內容。
  1. 確認使用的加密通信協議版本,比如TLS 1.0版本。如果瀏覽器與服務器支持的版本不一致,服務器關閉加密通信。
  2. 一個服務器生成的隨機數(Server Random),稍後用於生成"對話密鑰"。
  3. 確認使用的加密方法,比如RSA公鑰加密。
  4. 服務器證書

除了上面這些信息,如果服務器需要確認客戶端的身份,就會再包含一項請求,要求客戶端提供"客戶端證書"。比如,金融機構往往只允許認證客戶連入自己的網絡,就會向正式客戶提供USB密鑰,裏面就包含了一張客戶端證書。

第一次握手結束


  • 步驟3:客戶端迴應

客戶端收到服務器迴應以後,首先驗證服務器證書。如果證書不是可信機構頒佈、或者證書中的域名與實際域名不一致、或者證書已經過期,就會向訪問者顯示一個警告,由其選擇是否還要繼續通信。

如果證書沒有問題,客戶端就會從證書中取出服務器的公鑰。然後,向服務器發送下面三項信息。

  1. 一個隨機數(pre-master key), 稍後用於生成"對話密鑰"。
  2. 編碼改變通知,表示隨後的信息都將用雙方商定的加密方法和密鑰發送。
  3. 客戶端握手結束通知,表示客戶端的握手階段已經結束。這一項同時也是前面發送的所有內容的hash值,用來供服務器校驗。

上面第一項的隨機數,是整個握手階段出現的第三個隨機數,又稱"pre-master key"。有了它以後,客戶端和服務器就同時有了三個隨機數,接着雙方就用事先商定的加密方法,各自生成本次會話所用的同一把"會話密鑰"。

第二次握手結束


  • 步驟4:服務器的最後迴應
    服務器收到客戶端的第三個隨機數pre-master key之後,計算生成本次會話所用的"會話密鑰"。然後,向客戶端最後發送下面信息。
  1. 編碼改變通知,表示隨後的信息都將用雙方商定的加密方法和密鑰發送。
  2. 服務器握手結束通知,表示服務器的握手階段已經結束。這一項同時也是前面發送的所有內容的hash值,用來供客戶端校驗。

至此,整個握手階段全部結束。接下來,客戶端與服務器進入加密通信,就完全是使用普通的HTTP協議,只不過用"會話密鑰"加密內容,也就是對稱加密。

第三次握手結束

參考

  • Http協議理解
  • https://www.cnblogs.com/zxh930508/p/5432700.html
  • 協議理解之HTTPS
  • https://developers.weixin.qq.com/community/develop/article/doc/000046a5fdc7802a15f7508b556413
  • https://mp.weixin.qq.com/s/1ojSrhc9LZV8zlX6YblMtA

第 42 題:使用 sort() 對數組 [3, 15, 8, 29, 102, 22] 進行排序,輸出結果

let arr = [3, 15, 8, 29, 102, 22];
arr.sort((a,b)=>{a-b}); // 升序
arr.sort((a,b)=>{b-a}); //倒序
  • sort()函數解釋:

如果調用該方法時沒有使用參數,將按字母順序對數組中的元素進行排序,說得更精確點,是按照字符編碼的順序進行排序。要實現這一點,首先應把數組的元素都轉換成字符串(如有必要),以便進行比較。
如果想按照其他標準進行排序,就需要提供比較函數,該函數要比較兩個值,然後返回一個用於說明這兩個值的相對順序的數字。比較函數應該具有兩個參數 a 和 b,其返回值如下:

  • 若 a 小於 b,在排序後的數組中 a 應該出現在 b 之前,則返回一個小於 0 的值。
  • 若 a 等於 b,則返回 0。
  • 若 a 大於b,則返回一個大於 0 的值。

第 41 題:實現一個 sleep 函數,比如 sleep(1000) 意味着等待1000毫秒,可從 Promise、Generator、Async/Await 等角度實現

分別給出4中方式:

	//Promise
	const sleep = time => {
		return new Promise(resolve=>setTimeout(resolve,time))
	}
	sleep(1000).then(()=>{
		console.log(1)
	})
	//Generator
	function* sleepGenerator(time) {
		yield new Promise(function(resolve,reject){
			setTimeout(resolve,time);
		})
	}
	sleepGenerator(1000).next().value.then(()=>{console.log(1)})
	//async
	function sleep(time) {
		return new Promise(resolve=>setTimeout(resolve,time))
	}
	async function output() {
		let out = await sleep(1000);
		console.log(1);
		return out;
	}
	output();
	//ES5
	function sleep(callback,time) {
		if(typeof callback === 'function')
			setTimeout(callback,time)
	}
	
	function output(){
		console.log(1);
	}
	sleep(output,1000);

參考


第 40 題:下面代碼將打印什麼?

  var a = 10;
  (function () {
      console.log(a);    //undefined
      a = 5 
      console.log(window.a) // 10
      var a = 20;
      console.log(a) //20
  })()

原因:在內部聲名var a = 20;相當於先聲明var a;然後再執行賦值操作,這是在IIFE內形成的獨立作用域

B 情況:

  var a = 10;
  (function () {
      console.log(a);    //10
      a = 5 
      console.log(window.a) // 5
      //var a = 20;
      console.log(a) //5
  })()

C情況:

var a = 10;
(function () {
console.log(a); //10
//a = 5
console.log(window.a) // 10
//var a = 20;
console.log(a) //10
})()


第 39 題:在 Vue 中,子組件爲何不可以修改父組件傳遞的 Prop,如果修改了,Vue 是如何監控到屬性的修改並給出警告的

  1. 子組件爲何不可以修改父組件傳遞的 Prop:
    單向數據流,易於監測數據的流動,出現了錯誤可以更加迅速的定位到錯誤發生的位置。同時,因爲每當父組件屬性值修改時,該值都將被覆蓋;如果要有不同的改變,可以用基於prop的data或者computed
  2. Vue 是如何監控到屬性的修改並給出警告的
    initProps的時候,在defineReactive時通過判斷是否在開發環境,如果是開發環境,會在觸發set的時候判斷是否此key是否處於updatingChildren中被修改,如果不是,說明此修改來自子組件,觸發warning提示。
if (process.env.NODE_ENV !== 'production') {
      var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            "Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });
    }

需要特別注意的是,由於值傳遞與地址傳遞的原因當你從子組件修改的prop,屬於基礎類型時會觸發提示。 這種情況下,你是無法修改父組件的數據源的, 因爲基礎類型賦值時是值拷貝。你直接將另一個非基礎類型(Object, array)賦值到此key時也會觸發提示(但實際上不會影響父組件的數據源), 當你修改object的屬性時不會觸發提示,並且會修改父組件數據源的數據。(參考:https://blog.csdn.net/ImagineCode/article/details/54409272)


第 38 題:介紹下 BFC 及其應用

BFC 就是塊級格式上下文,是頁面盒模型佈局中的一種 CSS 渲染模式,相當於一個獨立的容器,裏面的元素和外部的元素相互不影響。創建 BFC 的方式有:

  • html 根元素
  • float 浮動
  • 絕對定位
  • overflow 不爲 visiable
  • display 爲表格佈局或者彈性佈局

BFC 主要的作用是:

  • 清除浮動
  • 防止同一 BFC 容器中的相鄰元素間的外邊距重疊問題

參考

第37題:下面代碼中 a 在什麼情況下會打印 1?

var a = ?;
if(a == 1 && a == 2 && a == 3){
 	console.log(1);
}

解析

因爲==會進行隱式類型轉換 。

  • 利用toString方式
let a = {
  i: 1,
  toString () {
    return a.i++
  }
}

if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}
  • 利用valueOf
let a = {
  i: 1,
  valueOf () {
    return a.i++
  }
}

if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}
  • 數組方式
var a = [1,2,3];
a.join = a.shift;
if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}
  • ES6的symbol
let a = {[Symbol.toPrimitive]: ((i) => () => ++i) (0)};
if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}

參考:

第36題:爲什麼 Vuex 的 mutation 和 Redux 的 reducer 中不能做異步操作?

參考:

第35題:使用迭代的方式實現 flatten 函數

flatten函數,即扁平化函數,是使一個嵌套數組變成一維數組的方式。

例如:

	function flatten(arr=[1,[2],[[3]],[[[4]]]]) {
		return arr.toString().split(',')
	}
	alert(flatten());

那麼怎麼使用迭代的方式實現flatten函數呢?往下看。

	function flatten(arr,result=[]) {
		for(let item of arr){
			if(Array.isArray(item))
				flatten(item,result)
			else
				result.push(item)
		}
		return result
	}
	var array = [[1,2,3],4,5,6,[[7]],[]];
	var result = flatten(array);
	console.log(result);

上面我們使用迭代遞歸的方式,使用 result 變量存儲結果,然後迭代當前數組,如果值也是數組則繼續扁平化,否則將值放入 result 裏

參考:

第34題:瀏覽器緩存可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那請求的時候 from memory cache 和 from disk cache 的依據是什麼,哪些數據什麼時候存放在 Memory Cache 和 Disk Cache中?

在這裏插入圖片描述

參考

第33題:下面的代碼打印什麼內容,爲什麼?

var b = 10;
(function b() {
  b = 20;
  console.log(b)
})()
  • 解析:
    這上面的代碼中,我們知道這裏面涉及到作用域和IIFE的知識。
    首先我們回顧下IIFE(立即調用函數表達式),參考MDN

IIFE

IIFE( 立即調用函數表達式)是一個在定義時就會立即執行的 JavaScript 函數。

下面這句話很重要:

第一部分是包圍在 圓括號運算符() 裏的一個匿名函數,這個匿名函數擁有獨立的詞法作用域。這不僅避免了外界訪問此 IIFE 中的變量,而且又不會污染全局作用域

第二部分再一次使用 () 創建了一個立即執行函數表達式,JavaScript 引擎到此將直接執行函數。

當函數變成立即執行的函數表達式時,表達式中的變量不能從外部訪問

(function () { 
    var name = "Barry";
})();
// 外部不能訪問變量 name
name // undefined

將 IIFE 分配給一個變量,不是存儲 IIFE 本身,而是存儲 IIFE 執行後返回的結果:

var result = (function () { 
    var name = "Barry"; 
    return name; 
})(); 
// IIFE 執行後返回的結果:
result; // "Barry"

回頭看

OK,瞭解完了IIFE,我們回過頭來看這道題:

var b = 10;
    (function b() {
      b = 20;
      console.log(b)
})()
  • b()是個IIFE具名函數,而非匿名函數。它將被立即執行。
  • 在內部作用域中,IIFE函數無法對進行賦值,有些類似於const的意思。所以b=20無效
  • console.log(b) 中,訪問變量b,首先在IIFE內部中查找已聲明的變量b,結果查找到b(),於是,輸出b()這個具名函數。而b=20並沒有進行聲明,所以,無效。

現在我們對代碼進行改造,再來看看其他種情況:

var b = 10;
(function b() {
    window.b = 20; 
    console.log(b); // [Function b]
    console.log(window.b); // 20是必然的
})();

分別打印20與10:

var b = 10;
(function b() {
    var b = 20; // IIFE內部變量
    console.log(b); // 20
   console.log(window.b); // 10 
})();

第 32 題:Virtual DOM 真的比操作原生 DOM 快嗎?

參考

第 31 題:改造下面的代碼,使之輸出0 - 9,寫出你能想到的所有解法

for (var i = 0; i< 10; i++){
    setTimeout(() => {
	console.log(i);
    }, 1000)
}

改造

  • 改造1
	function fn() {
		for(var i=0;i<10;i++){//可以將for循環用while循環替換
			console.log(i);
			clearTimeout(timer);
		}
		
	}
	var timer = setTimeout(fn,1000);
或者:
for (let i = 0; i< 10; i++){
	setTimeout(() => {
		console.log(i);
	}, 1000)
}
或者 簡寫:
for (var i = 0; i< 10; i++){
    setTimeout(console.log, 1000, i)
}
  • 改造2
(function fn(i){
    if(i<10){
        console.log(i);
        i++;
        setTimeout(fn, 1000,i);
   }
})(0)

-改造3 - 利用閉包特性

for(var i=0;i<10;i++){
		(function(i){
			setTimeout(()=>{
				console.log(i);
			},1000);
		})(i);
	}
或者:
for(var i=0;i<10;i++){
		(function(){
		var j = i; 
			setTimeout(()=>{
				console.log(i);
			},1000);
		})();
	}

第30題:數組合並

題目描述:請把倆個數組 [‘A1’, ‘A2’, ‘B1’, ‘B2’, ‘C1’, ‘C2’, ‘D1’, ‘D2’, ‘E1’, ‘E2’] 和 [‘A’, ‘B’, ‘C’, ‘D’,‘E’],合併爲 [“A1”, “A2”, “A”, “B1”, “B2”, “B”, “C1”, “C2”, “C”, “D1”, “D2”, “D”, “E1”, “E2”, “E”]

解析: 觀察題目,數組是有規律的!

var arr1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2', 'E1', 'E2'];
var arr2 = ['A', 'B', 'C', 'D','E'];
var arr3 = [];
	arr1.forEach(function(item,index){
		
		if((index+1)%2===0) {
			arr3.push(arr1[index]);
			arr3.push(arr2[(index+1)/2-1]);
		}else {
			arr3.push(arr1[index]);
		}
	});
	console.log(arr3);

優化:

	arr1.map(function(item,index){
		(index+1)%2===0 ? arr3.push(arr1[index]) && arr3.push(arr2[(index+1)/2-1]) : arr3.push(arr1[index])
	});

第29題:Vue 的雙向數據綁定,Model 如何改變 View,View 又是如何改變 Model 的?

vue 是如何實現視圖與viewmodel的雙向綁定的?爲什麼數據一變化,視圖就會立即更新,視圖產生用戶操作,viewmodel就能馬上得知?

  • VUE實現雙向數據綁定的原理就是利用了 Object.defineProperty() 這個方法重新定義了對象獲取屬性值(get)和設置屬性值(set)的操作來實現的。它接收三個參數:要操作的對象要定義或修改的對象屬性名屬性描述符

重點是:屬性描述符

  • 屬性描述符是一個對象,主要有兩種形式:數據描述符和存取描述符。這兩種對象只能選擇一種使用,不能混合兩種描述符的屬性同時使用。例如,get和set就是屬於存取描述符對象的屬性。

示例:

//defineProperty的用法
var obj = { };
var name;
//第一個參數:定義屬性的對象。
//第二個參數:要定義或修改的屬性的名稱。
//第三個參數:將被定義或修改的屬性描述符。

	Object.defineProperty(obj, "data", {
		//獲取值
		get: function () {return name;},
		//設置值
		set: function (val) {
			name = val;
			console.log(val)
		}
	})
	//賦值調用get
	obj.data = 'aaa';
	//取值調用set
	console.log(obj.data);

//defineProperty的雙向綁定
	var obj={};
	Object.defineProperty(obj, 'val',{
		set:function (newVal) {
			document.getElementById("a")
			.value = newVal == undefined ? '' : newVal;
			document.getElementById("b")
			.innerHTML = newVal == undefined ? '' : newVal;
	}
	});
	document.getElementById("a")
	.addEventListener("keyup",function (e) {
		obj.val = e.target.value;
	})

參考


第28題:cookie 和 token 都存放在 header 中,爲什麼不會劫持 token?

簡單理解:

  • cookie 相當於編號。例如,你去銀行櫃檯辦事要先“叫號”,只涉及到簡單的業務,到了櫃檯銀行工作人員只需要根據你的編號就可以給你辦理。
    而:
  • token 相當於身份證,或者唯一標識。同樣一個例子,你在銀行櫃檯辦事,不僅僅要叫號給自己先編上一個編號,但是你去辦理需要身份證的業務時,你需要出示證件才能給你辦理。

所以,綜上可知 cookie是大家都可以知道的。而,tooken是自有你自己知道的。這也就反映在爲什麼在進行請求驗證時,需要加入token這個標識。就是爲了讓Server端能夠證實你的身份。結合到現實生活中,大家要知道(劫持)你的叫號編號是容易的,而要知道(劫持)你的身份證是難的!

參考


第27題:關於 const 和 let 聲明的變量不在 window 上

在ES5中,頂層對象的屬性和全局變量是等價的,var 命令和 function 命令聲明的全局變量,自然也是頂層對象。

var a = 12;
function f(){};

console.log(window.a); // 12
console.log(window.f); // f(){}

但ES6規定,var 命令和 function 命令聲明的全局變量,依舊是頂層對象的屬性,但 let命令、const命令、class命令聲明的全局變量,不屬於頂層對象的屬性

let aa = 1;
const bb = 2;

console.log(window.aa); // undefined
console.log(window.bb); // undefined

在哪裏?通過在設置斷點,看看瀏覽器是怎麼處理的:

在這裏插入圖片描述
通過上圖也可以看到,在全局作用域中,用 let 和 const 聲明的全局變量並沒有在全局對象中,只是一個塊級作用域(Script)中。

怎麼獲取?在定義變量的塊級作用域中就能獲取,既然不屬於頂層對象,那就不加 window(global)。

let aa = 1;
const bb = 2;

console.log(aa); // 1
console.log(bb); // 2

參考

【ECMAScript6】es6 要點(一)剩餘參數 | 數組方法 | 解構賦值 | 字符串模板 | 面向對象 | 模塊


第 26 題:介紹模塊化發展歷程,可從IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、< script type=“module”>這幾個角度考慮

模塊化的好處就是,抽離代碼,重複使用,如現在很直觀的代表 npm 包。

參考: 【NodeJS】歸納篇(二)模塊化

模塊化的發展歷程

Long Long ago

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>So UI - A Component Library for Vue.js.</title>
</head>
<body>
    <div id="app"></div>
    <script src="a.js"></script>
    <script src="b.js"></script>
    <script src="c.js"></script>
    <script src="d.js"></script>
    <script src="e.js"></script>
</body>
</html>

如上,引入了 a/b/c/d/e 五個文件,這五個文件如果相互之間有依賴,還要注意引入的順序,並且還需要注意它們裏面的變量名,若是重複利用到其他的項目,其他項目也需要注意到以上兩點問題。爲了解決這一問題,就有了模塊化的規範。

CMD 與 AMD

  • CMD (Common Module Definition), 是sea.js在推廣過程中對模塊定義的規範化產出,主要用於瀏覽器端。它主要特點是:對於依賴的模塊是延遲執行,依賴可以就近書寫,等到需要用這個依賴的時候再引入這個依賴,應用有sea.js.

  • AMD規範(Asynchronous Module Definition):是 RequireJS 在推廣過程中對模塊定義的規範化產出,也是主要用於瀏覽器端。其特點是:依賴前置,需要在定義時就寫好需要的依賴,提前執行依賴,應用有require.js。它需要依次的加載模塊然後去進行相應的操作,加載模塊就是要引入這個文件,那麼這裏也還是通過動態加載 script 的方法,並通過 onload 去執行後面的回調了。

ES6 export 和 import

  • export 導出你定義的模塊變量
export { 
 	one, 
 	two
 }
 export default three;
  • import 引入一個模塊變量
import  { one, two }  three from 'a.js'

可以看到 export 可以導出一個默認的變量,也可以導出變量對象,這裏引入的時候名字不要寫錯了。 那麼 es6 的模塊化通過babel 轉碼其實就是 umd 模塊規範, 它是一個兼容 cmd 和 amd 的模塊化規範, 同時還支持老式的“全局”變量規範

示例:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS之類的
        module.exports = factory(require('jquery'));
    } else {
        // 瀏覽器全局變量(root 即 window)
        root.returnExports = factory(root.jQuery);
    }
}(this, function ($) {
    //    方法
    function myFunc(){};
 
    //    暴露公共方法
    return myFunc;
}));

瀏覽器是如何支持這種規範的呢?——是實現了根據這種規範定製出來的功能

例如,AMD 定義一個模塊的方法是 define(id?, dependencies?, factory)

define = function (name, deps, callback) {
        var node, context;
        
        //Allow for anonymous modules
        if (typeof name !== 'string') {
            //Adjust args appropriately
            callback = deps;
            deps = name;
            name = null;
        }

        //This module may not have dependencies
        if (!isArray(deps)) {
            callback = deps;
            deps = null;
        }

        //If no name, and callback is a function, then figure out if it a
        //CommonJS thing with dependencies.
        if (!deps && isFunction(callback)) {
            deps = [];
            //移除註釋
            //查找 require 語句,收集依賴到 deps 裏面
            // but only if there are function args.
            if (callback.length) {
                callback
                    .toString()
                    .replace(commentRegExp, commentReplace)
                    .replace(cjsRequireRegExp, function (match, dep) {
                        deps.push(dep);
                    });

                //May be a CommonJS thing even without require calls, but still
                //could use exports, and module. Avoid doing exports and module
                //work though if it just needs require.
                //REQUIRES the function to expect the CommonJS variables in the
                //order listed below.
                deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
            }
        }

        //If in IE 6-8 and hit an anonymous define() call, do the interactive
        //work.
        if (useInteractive) {
            node = currentlyAddingScript || getInteractiveScript();
            if (node) {
                if (!name) {
                    name = node.getAttribute('data-requiremodule');
                }
                context = contexts[node.getAttribute('data-requirecontext')];
            }
        }

        //Always save off evaluating the def call until the script onload handler.
        //This allows multiple modules to be in a file without prematurely
        //tracing dependencies, and allows for anonymous module support,
        //where the module name is not known until the script onload event
        //occurs. If no context, use the global queue, and get it processed
        //in the onscript load callback.
        if (context) {
            context.defQueue.push([name, deps, callback]);
            context.defQueueMap[name] = true;
        } else {
            globalDefQueue.push([name, deps, callback]);
        }
    };

    define.amd = {
        jQuery: true
    };
    
	req.exec = function (text) {
        /*jslint evil: true */
        return eval(text);
    };

    //Set up with config info.
    req(cfg);

這一段代碼是解析定義是模塊所需的依賴放置 context 的模塊定義隊列中。然後我們就要通過 req 去執行加載依賴,我們來看看 req 的定義。

req = requirejs = function (deps, callback, errback, optional) {

        //Find the right context, use default
        var context, config,
            contextName = defContextName;

        // Determine if have config object in the call.
        if (!isArray(deps) && typeof deps !== 'string') {
            // deps is a config object
            config = deps;
            if (isArray(callback)) {
                // Adjust args if there are dependencies
                deps = callback;
                callback = errback;
                errback = optional;
            } else {
                deps = [];
            }
        }

        if (config && config.context) {
            contextName = config.context;
        }
       
        if (config) {
            context.configure(config); // 完善配置
        }

        return context.require(deps, callback, errback); 

這裏的代碼把 依賴,回調, 錯誤處理和配置項都傳進來了,進行了配置上的處理之後,我們可以看到最後再去根據配置加載。
我們再來看 context.require 方法:

makeRequire: function (relMap, options) {
		options = options || {};
		function localRequire(deps, callback, errback) {
			.... 當前 require 的轉換
      	 	return localRequire;
  		 }
		completeLoad: function (moduleName) {
			判斷 context 的依賴隊列,是繼續加載還是執行回調
		}
		 nameToUrl: function (moduleName, ext, skipExt) {
		 	根據模塊名和配置得到加載的路徑
		 }
		 load: function (id, url) {
	               req.load(context, id, url);
	      },
	      execCb: function (name, callback, args, exports) {
	                return callback.apply(exports, args);
	        },
		onScriptLoad: function (evt) {
			腳本加載完成後得到數據,執行 context.completeLoad(data.id);
		}
		onScriptError: function (evt) {
			加載錯誤執行錯誤處理
		}
	};
   context.require = context.makeRequire();

以上原文轉載自:https://blog.csdn.net/dadadeganhuo/article/details/86777249


第 25 題:說說瀏覽器和 Node 事件循環(Event Loop)的區別

瀏覽器的event loop 和nodejs的event loop 在處理異步事件的順序是不同的。
nodejs中有micro event;其中Promise屬於micro event 該異步事件的處理順序就和瀏覽器不同.
nodejs V11.0以上 這兩者之間的順序就相同了。

瀏覽器

關於微任務和宏任務在瀏覽器的執行順序是這樣的:

  • 執行一隻task(宏任務)
  • 執行完micro-task隊列 (微任務)

如此循環往復下去。

  • 常見的 task(宏任務):

比如:setTimeout、setInterval、script(整體代碼)、 I/O 操作、UI 渲染等。

  • 常見的 micro-task :

比如: new Promise().then(回調)、MutationObserver(html5新特性) 等。

Node

Node的事件循環是libuv實現的。
在這裏插入圖片描述

大體的task(宏任務)執行順序是這樣的:

  • timers定時器:本階段執行已經安排的 setTimeout() 和 setInterval() 的回調函數。
  • pending callbacks待定回調:執行延遲到下一個循環迭代的 I/O 回調。
  • idle, prepare:僅系統內部使用。
  • poll 輪詢:檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎所有情況下,除了關閉的回調函數,它們由計時器和 setImmediate()
    排定的之外),其餘情況 node 將在此處阻塞。
  • check 檢測:setImmediate() 回調函數在這裏執行。
  • close callbacks 關閉的回調函數:一些準備關閉的回調函數,如:socket.on(‘close’, …)。

示例:

function test () {
   console.log('start')
    setTimeout(() => {
        console.log('children2')
        Promise.resolve().then(() => {console.log('children2-1')})
    }, 0)
    setTimeout(() => {
        console.log('children3')
        Promise.resolve().then(() => {console.log('children3-1')})
    }, 0)
    Promise.resolve().then(() => {console.log('children1')})
    console.log('end') 
}
test()

node11以下版本的執行結果(先執行所有的宏任務,再執行微任務)

// start
// end
// children1
// children2
// children3
// children2-1
// children3-1

node11+及瀏覽器的執行結果(順序執行宏任務和微任務)

// start
// end
// children1
// children2
// children2-1
// children3
// children3-1

參考


第 24 題:聊聊 Redux 和 Vuex 的設計思想

狀態管理對於前端單頁應用的管理思想的精髓:

  • Web應用是一個狀態機,視圖與狀態是一一對應的。
    一旦認同這種模式並在項目組使用了狀態管理,就要嚴格的在整個應用中都採用這種模式。因此,基於這種特性,我們需要一種機制或者框架:使得我們能夠管理狀態,感知變化,並將狀態映射爲頁面表現。

VUEX是吸收了Redux的經驗,放棄了一些特性並做了一些優化,代價就是VUEX只能和VUE配合。
而Redux則是一個純粹的狀態管理系統,React利用React-Redux將它與React框架結合起來。但是,它們必然在都具備常規的狀態管理的功能之外,針對性地對各自所對應的框架還會有一些更優的特性,並且React-Redux還有一些衍生項目。例如:dva.js

Redux

一個單純的狀態管理者。它提供一個全局的對象store,store中包含state對象用以包含所有應用數據,並且store提供了一些reducer方法。這些方法可以自定義,使用調用者得以改變state的值。state的值僅爲只讀,如果需要更改則必須只能通過reducer。

React-Redux,簡單來說,它提供了一些接口,用於Redux的狀態和React的組件展示結合起來,以用於實現狀態與視圖的一一對應。

DVA,則是對React-Redux進行了封裝,並結合了Redux-Saga等中間件,而且使用了model概念,也相當於在React-Redux的基礎上針對web應用開發做了優化。

DVA數據流向圖

在這裏插入圖片描述

Vuex

在這裏插入圖片描述

Redux與Vuex對比

Redux

  • 核心對象:store
  • 數據存儲:state
  • 狀態更新提交接口:dispatch
  • 狀態更新提交參數:帶type和payload的Action
  • 狀態更新計算:reducer
  • 限制:reducer必須是純函數,不支持異步
  • 特性:支持中間件

VUEX

  • 核心對象:store
  • 數據存儲:state
  • 狀態更新提交接口:commit
  • 狀態更新提交參數:帶type和payload的mutation提交對象/參數
  • 狀態更新計算:mutation handler
  • 限制:mutation handler必須是非異步方法
  • 特性:支持帶緩存的getter,用於獲取state經過某些計算後的值

store和state是最基本的概念,VUEX沒有做出改變。其實VUEX對整個框架思想並沒有任何改變,只是某些內容變化了名稱或者叫法,通過改名,以圖在一些細節概念上有所區分。

  • VUEX弱化了dispatch的存在感。VUEX認爲狀態變更的觸發是一次“提交”而已,而調用方式則是框架提供一個提交的commit API接口。
  • VUEX取消了Redux中Action的概念。不同於Redux認爲狀態變更必須是由一次"行爲"觸發,VUEX僅僅認爲在任何時候觸發狀態變化只需要進行mutation即可。Redux的Action必須是一個對象,而VUEX認爲只要傳遞必要的參數即可,形式不做要求。
  • VUEX也弱化了Redux中的reducer的概念。reducer在計算機領域語義應該是"規約",在這裏意思應該是根據舊的state和Action的傳入參數,“規約"出新的state。在VUEX中,對應的是mutation,即"轉變”,只是根據入參對舊state進行"轉變"而已。

總的來說,VUEX通過弱化概念,在任何東西都沒做實質性削減的基礎上,使得整套框架更易於理解了。

另外VUEX支持getter,運行中是帶緩存的,算是對提升性能方面做了些優化工作,言外之意也是鼓勵大家多使用getter。


第 23 題:介紹下觀察者模式和訂閱-發佈模式的區別,各自適用於什麼場景

  • 觀察者模式中主體和觀察者是互相感知的;

    假設你正在找一份軟件工程師的工作,對“香蕉公司”很感興趣。所以你聯繫了他們的HR,給了他你的聯繫電話。他保證如果有任何職位空缺都會通知你。這裏還有幾個候選人也你一樣很感興趣。所以職位空缺大家都會知道,如果你迴應了他們的通知,他們就會聯繫你面試。
    這裏的“香蕉公司”就是Subject,用來維護Observers(和你一樣的候選人),爲某些event(比如職位空缺)來通知(notify)觀察者。

  • 發佈-訂閱模式是藉助第三方來實現調度的,發佈者和訂閱者之間互不感知

    在發佈-訂閱模式,消息的發送方,叫做發佈者(publishers),消息不會直接發送給特定的接收者,叫做訂閱者。

    意思就是發佈者和訂閱者不知道對方的存在。需要一個第三方組件,叫做信息中介,它將訂閱者和發佈者串聯起來,它過濾和分配所有輸入的消息。換句話說,發佈-訂閱模式用來處理不同系統組件的信息交流,即使這些組件不知道對方的存在。

用一張圖片進行解釋:
在這裏插入圖片描述

參考

第22題:介紹下重繪和迴流(Repaint & Reflow),以及如何進行優化

1. 瀏覽器渲染機制

瀏覽器採用流式佈局模型(Flow Based Layout)
瀏覽器會把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合併就產生了渲染樹(Render Tree)。
有了RenderTree,我們就知道了所有節點的樣式,然後計算他們在頁面上的大小和位置,最後把節點繪製到頁面上。
由於瀏覽器使用流式佈局,對Render Tree的計算通常只需要遍歷一次就可以完成,但table及其內部元素除外,他們可能需要多次計算,通常要花3倍於同等元素的時間,這也是爲什麼要避免使用table佈局的原因之一。

瀏覽器渲染過程如下:

  • 解析HTML,生成DOM樹,解析CSS,生成CSSOM樹
  • 將DOM樹和CSSOM樹結合,生成渲染樹(Render Tree)
  • Layout(迴流):根據生成的渲染樹,進行迴流(Layout),得到節點的幾何信息(位置,大小)
  • Painting(重繪):根據渲染樹以及迴流得到的幾何信息,得到節點的絕對像素
  • Display:將像素髮送給GPU,展示在頁面上。(這一步其實還有很多內容,比如會在GPU將多個合成層合併爲同一個層,並展示在頁面中。而css3硬件加速的原理則是新建合成層)

爲了構建渲染樹,瀏覽器主要完成了以下工作:

從DOM樹的根節點開始遍歷每個可見節點。
對於每個可見的節點,找到CSSOM樹中對應的規則,並應用它們。
根據每個可見節點以及其對應的樣式,組合生成渲染樹。
第一步中,既然說到了要遍歷可見的節點,那麼我們得先知道,什麼節點是不可見的。

不可見的節點包括:

一些不會渲染輸出的節點,比如script、meta、link等。
一些通過css進行隱藏的節點。比如display:none。注意,利用visibility和opacity隱藏的節點,還是會顯示在渲染樹上的。只有display:none的節點纔不會顯示在渲染樹上。

從上面的例子來講,我們可以看到span標籤的樣式有一個display:none,因此,它最終並沒有在渲染樹上。

注意:渲染樹只包含可見的節點

2. 迴流

迴流是佈局或者幾何屬性需要改變就稱爲迴流。迴流是影響瀏覽器性能的關鍵因素,因爲其變化涉及到部分頁面(或是整個頁面)的佈局更新。一個元素的迴流可能會導致了其所有子元素以及DOM中緊隨其後的節點、祖先節點元素的隨後的迴流。

通過構造渲染樹,我們將可見DOM節點以及它對應的樣式結合起來,可是我們還需要計算它們在設備視口(viewport)內的確切位置和大小,這個計算的階段就是迴流。

示例:

<body>
<div class="error">
    <h4>我的組件</h4>
    <p><strong>錯誤:</strong>錯誤的描述…</p>
    <h5>錯誤糾正</h5>
    <ol>
        <li>第一步</li>
        <li>第二步</li>
    </ol>
</div>
</body>

在上面的HTML片段中,對該段落(<p>標籤)迴流將會引發強烈的迴流,因爲它是一個子節點。這也導致了祖先的迴流(div.errorbody·–視瀏覽器而定)。此外,<h5><ol>也會有簡單的迴流,因爲其在DOM中在迴流元素之後。大部分的迴流將導致頁面的重新渲染。

迴流必定會發生重繪,重繪不一定會引發迴流。

何時發生迴流重繪

我們前面知道了,迴流這一階段主要是計算節點的位置和幾何信息,那麼當頁面佈局和幾何信息發生變化的時候,就需要回流。比如以下情況:

  • 添加或刪除可見的DOM元素
  • 元素的位置發生變化
  • 元素的尺寸發生變化(包括外邊距、內邊框、邊框大小、高度和寬度等)
  • 內容發生變化,比如文本變化或圖片被另一個不同尺寸的圖片所替代。
  • 頁面一開始渲染的時候(這肯定避免不了)
  • 瀏覽器的窗口尺寸變化(因爲迴流是根據視口的大小來計算元素的位置和大小的)

3. 重繪

由於節點的幾何屬性發生改變或者由於樣式發生改變而不會影響佈局的,稱爲重繪,例如outline, visibility, color、background-color等,重繪的代價是高昂的,因爲瀏覽器必須驗證DOM樹上其他節點元素的可見性

我們通過構造渲染樹和迴流階段,我們知道了哪些節點是可見的,以及可見節點的樣式和具體的幾何信息(位置、大小),那麼我們就可以將渲染樹的每個節點都轉換爲屏幕上的實際像素,這個階段就叫做重繪節點。

4. 瀏覽器優化

現代瀏覽器大多都是通過隊列機制來批量更新佈局,瀏覽器會把修改操作放在隊列中至少一個瀏覽器刷新(即16.6ms)纔會清空隊列,但當你獲取佈局信息的時候,隊列中可能有會影響這些屬性或方法返回值的操作,即使沒有,瀏覽器也會強制清空隊列,觸發迴流與重繪來確保返回正確的值。

主要包括以下屬性或方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • width、height
  • getComputedStyle()
  • getBoundingClientRect()

所以,我們應該避免頻繁的使用上述的屬性,他們都會強制渲染刷新隊列

5. 減少重繪與迴流

1、 CSS

使用 transform 替代 top

使用visibility替換display:none,因爲前者只會引起重繪,後者會引發迴流(改變了佈局

  • 避免使用table佈局,可能很小的一個小改動會造成整個table的重新佈局。

  • 儘可能在DOM樹的最末端改變class,迴流是不可避免的,但可以減少其影響。儘可能在DOM樹的最末端改變class,可以限制了迴流的範圍,使其影響儘可能少的節點。

  • 避免設置多層內聯樣式,CSS 選擇符從右往左匹配查找,避免節點層級過多

<div>
  <a> <span></span> </a>
</div>
<style>
  span {
    color: red;
  }
  div > a > span {
    color: red;
  }
</style>

對於第一種設置樣式的方式來說,瀏覽器只需要找到頁面中所有的span標籤然後設置顏色,但是對於第二種設置樣式的方式來說,瀏覽器首先需要找到所有的span標籤,然後找到span標籤上的a標籤,最後再去找到div標籤,然後給符合這種條件的span標籤設置顏色,這樣的遞歸過程就很複雜。所以我們應該儘可能的避免寫過於具體的 CSS 選擇器,然後對於 HTML 來說也儘量少的添加無意義標籤,保證層級扁平。

  • 將動畫效果應用到position屬性爲absolute或fixed的元素上,避免影響其他元素的佈局,這樣只是一個重繪,而不是迴流,同時,控制動畫速度可以選擇 requestAnimationFrame,詳見探討 requestAnimationFrame。

  • 避免使用CSS表達式,可能會引發迴流。

  • 將頻繁重繪或者回流的節點設置爲圖層,圖層能夠阻止該節點的渲染行爲影響別的節點,例如**will-change、video、iframe等標籤,瀏覽器會自動將該節點變爲圖層。

  • CSS3 硬件加速(GPU加速),使用css3硬件加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪。但是對於動畫的其它屬性,比如background-color這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的性能。

2、JavaScript

  • 避免頻繁操作樣式,最好一次性重寫style屬性,或者將樣式列表定義爲class並一次性更改class屬性。
  • 避免頻繁操作DOM,創建一個documentFragment,在它上面應用所有DOM操作,最後再把它添加到文檔中。
  • 避免頻繁讀取會引發迴流/重繪的屬性,如果確實需要多次使用,就用一個變量緩存起來。
  • 對具有複雜動畫的元素使用絕對定位,使它脫離文檔流,否則會引起父元素及後續元素頻繁迴流。

最小化重繪和重排

由於重繪和重排可能代價比較昂貴,因此最好就是可以減少它的發生次數。爲了減少發生次數,我們可以合併多次對DOM和樣式的修改,然後一次處理掉。考慮這個例子

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';

例子中,有三個樣式屬性被修改了,每一個都會影響元素的幾何結構,引起迴流。當然,大部分現代瀏覽器都對其做了優化,因此,只會觸發一次重排。但是如果在舊版的瀏覽器或者在上面代碼執行的時候,有其他代碼訪問了佈局信息(上文中的會觸發迴流的佈局信息),那麼就會導致三次重排。

因此,我們可以合併所有的改變然後依次處理,比如我們可以採取以下的方式:

  • 使用cssText
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
  • 修改CSS的class
const el = document.getElementById('test');
el.className += ' active';

批量修改DOM

當我們需要對DOM對一系列修改的時候,可以通過以下步驟減少迴流重繪次數:

  1. 使元素脫離文檔流
  2. 對其進行多次修改
  3. 將元素帶回到文檔中。

該過程的第一步和第三步可能會引起迴流,但是經過第一步之後,對DOM的所有修改都不會引起迴流重繪,因爲它已經不在渲染樹了。

有三種方式可以讓DOM脫離文檔流:

  • 隱藏元素,應用修改,重新顯示
  • 使用文檔片段(document fragment)在當前DOM之外構建一個子樹,再把它拷貝迴文檔。
  • 將原始元素拷貝到一個脫離文檔的節點中,修改節點後,再替換原始的元素。

考慮我們要執行一段批量插入節點的代碼:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
    	li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

const ul = document.getElementById('list');
appendDataToElement(ul, data);

如果我們直接這樣執行的話,由於每次循環都會插入一個新的節點,會導致瀏覽器迴流一次。

我們可以使用這三種方式進行優化:

隱藏元素,應用修改,重新顯示:

這個會在展示和隱藏節點的時候,產生兩次迴流

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
    	li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

使用文檔片段(document fragment)在當前DOM之外構建一個子樹,再把它拷貝迴文檔:

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);

將原始元素拷貝到一個脫離文檔的節點中,修改節點後,再替換原始的元素。

const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);

避免觸發同步佈局事件

當我們訪問元素的一些屬性的時候,會導致瀏覽器強制清空隊列,進行強制同步佈局。舉個例子,比如說我們想將一個p標籤數組的寬度賦值爲一個元素的寬度,我們可能寫出這樣的代碼:

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
}

在每次循環的時候,都讀取了box的一個offsetWidth屬性值,然後利用它來更新p標籤的width屬性。這就導致了每一次循環的時候,瀏覽器都必須先使上一次循環中的樣式更新操作生效,才能響應本次循環的樣式讀取操作。每一次循環都會強制瀏覽器刷新隊列。我們可以優化爲:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}

對於複雜動畫效果,使用絕對定位讓其脫離文檔流

對於複雜動畫效果,由於會經常的引起迴流重繪,因此,我們可以使用絕對定位,讓它脫離文檔流。否則會引起父元素以及後續元素頻繁的迴流。

    1. 使用css3硬件加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪 。
    1. 對於動畫的其它屬性,比如background-color這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的性能。

常見的觸發硬件加速的css屬性:

  • transform
  • opacity
  • filters
  • Will-change
css3硬件加速的坑

當然,任何美好的東西都是會有對應的代價的,過猶不及。css3硬件加速還是有坑的:

  • 如果你爲太多元素使用css3硬件加速,會導致內存佔用較大,會有性能問題。
  • 在GPU渲染字體會導致抗鋸齒無效。這是因爲GPU和CPU的算法不同。因此如果你不在動畫結束的時候關閉硬件加速,會產生字體模糊。

詳見瀏覽器的重繪與迴流(Repaint、Reflow)

參考

第21題:有以下 3 個判斷數組的方法,請分別介紹它們之間的區別和優劣Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

  1. Object.prototype.toString.call()

每一個繼承 Object 的對象都有 toString 方法,如果 toString 方法沒有重寫的話,會返回 [Object type],其中 type 爲對象的類型。但當除了 Object 類型的對象外,其他類型直接使用 toString 方法時,會直接返回都是內容的字符串,所以我們需要使用call或者apply方法來改變toString方法的執行上下文。

const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"

這種方法對於所有基本的數據類型都能進行判斷,即使是 null 和 undefined 。

更多實現可見 談談 Object.prototype.toString
2. instanceof

instanceof 的內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype
使用 instanceof判斷一個對象是否爲數組,instanceof 會判斷這個對象的原型鏈上是否會找到對應的 Array 的原型,找到返回 true,否則返回 false

[]  instanceof Array; // true

instanceof 只能用來判斷對象類型,原始類型不可以。並且所有對象類型 instanceof Object 都是 true。

[]  instanceof Object; // true
  1. Array.isArray()
  • 功能:用來判斷對象是否爲數組
  • instanceof 與 isArray

當檢測Array實例時,Array.isArray 優於 instanceof ,因爲 Array.isArray 可以檢測出 iframes

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
Object.prototype.toString.call(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false
  • Array.isArray()Object.prototype.toString.call()

  • Array.isArray()是ES5新增的方法,當不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 實現。

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}   

Array.isArray 的性能最好,instanceof 比 toString.call 稍微好了一點點

參考

https://www.cnblogs.com/onepixel/p/5126046.html

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray

談談 Object.prototype.toString

第20題:介紹下 npm 模塊安裝機制,爲什麼輸入 npm install 就可以自動安裝對應的模塊?

1. npm 模塊安裝機制:

  • 發出npm install命令

  • 查詢node_modules目錄之中是否已經存在指定模塊

    • 若存在,不再重新安裝

    • 若不存在

      • npm 向 registry 查詢模塊壓縮包的網址
      • 下載壓縮包,存放在根目錄下的.npm目錄裏
  • 解壓壓縮包到當前項目的node_modules目錄

2. npm 實現原理

輸入 npm install 命令並敲下回車後,會經歷如下幾個階段(以 npm 5.5.1 爲例):

  1. 執行工程自身 preinstall

當前 npm 工程如果定義了 preinstall 鉤子此時會被執行。

  1. 確定首層依賴模塊

首先需要做的是確定工程中的首層依賴,也就是 dependencies 和 devDependencies 屬性中直接指定的模塊(假設此時沒有添加 npm install 參數)。

工程本身是整棵依賴樹的根節點,每個首層依賴模塊都是根節點下面的一棵子樹,npm 會開啓多進程從每個首層依賴模塊開始逐步尋找更深層級的節點。

  1. 獲取模塊

獲取模塊是一個遞歸的過程,分爲以下幾步:

  • 獲取模塊信息。在下載一個模塊之前,首先要確定其版本,這是因爲 package.json 中往往是 semantic version(semver,語義化版本)。此時如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有該模塊信息直接拿即可,如果沒有則從倉庫獲取。如 packaeg.json 中某個包的版本是 ^1.1.0,npm 就會去倉庫中獲取符合 1.x.x 形式的最新版本。
  • 獲取模塊實體。上一步會獲取到模塊的壓縮包地址(resolved 字段),npm 會用此地址檢查本地緩存,緩存中有就直接拿,如果沒有則從倉庫下載
  • 查找該模塊依賴,如果有依賴則回到第1步,如果沒有則停止。
  1. 模塊扁平化(dedupe)

上一步獲取到的是一棵完整的依賴樹,其中可能包含大量重複模塊。比如 A 模塊依賴於 loadsh,B 模塊同樣依賴於 lodash。在 npm3 以前會嚴格按照依賴樹的結構進行安裝,因此會造成模塊冗餘。

從 npm3 開始默認加入了一個 dedupe 的過程。它會遍歷所有節點,逐個將模塊放在根節點下面,也就是 node-modules 的第一層。當發現有重複模塊時,則將其丟棄。

這裏需要對重複模塊進行一個定義,它指的是模塊名相同semver 兼容。每個 semver 都對應一段版本允許範圍,如果兩個模塊的版本允許範圍存在交集,那麼就可以得到一個兼容版本,而不必版本號完全一致,這可以使更多冗餘模塊在 dedupe 過程中被去掉。

比如 node-modules 下 foo 模塊依賴 lodash@^1.0.0,bar 模塊依賴 lodash@^1.1.0,則 ^1.1.0 爲兼容版本。

而當 foo 依賴 lodash@^2.0.0,bar 依賴 lodash@^1.1.0,則依據 semver 的規則,二者不存在兼容版本。會將一個版本放在 node_modules 中,另一個仍保留在依賴樹裏。

舉個例子,假設一個依賴樹原本是這樣:

node_modules
– foo
---- lodash@version1

– bar
---- lodash@version2

假設 version1 和 version2 是兼容版本,則經過 dedupe 會成爲下面的形式:

node_modules
– foo

– bar

– lodash(保留的版本爲兼容版本)

假設 version1 和 version2 爲非兼容版本,則後面的版本保留在依賴樹中:

node_modules
– foo
– lodash@version1

– bar
---- lodash@version2

  1. 安裝模塊

這一步將會更新工程中的 node_modules,並執行模塊中的生命週期函數(按照 preinstall、install、postinstall 的順序)。

  1. 執行工程自身生命週期

當前 npm 工程如果定義了鉤子此時會被執行(按照 install、postinstall、prepublish、prepare 的順序)。

最後一步是生成或更新版本描述文件,npm install 過程完成。

參考

npm 模塊安裝機制簡介

詳解npm的模塊安裝機制

npm install的實現原理

轉載參考

https://github.com/Advanced-Frontend/Daily-Interview-Question
2019 前端面試題彙總(主要爲 Vue)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章