1-10
第 1 題:寫 React / Vue 項目時爲什麼要在列表組件中寫 key,其作用是什麼?
vue和react都是採用diff算法來對比新舊虛擬節點,從而更新節點。在vue的diff函數中(建議先了解一下diff算法過程)。
在交叉對比中,當新節點跟舊節點頭尾交叉對比
沒有結果時,會根據新節點的key去對比舊節點數組中的key,從而找到相應舊節點(這裏對應的是一個key => index 的map映射)。如果沒找到就認爲是一個新增節點。而如果沒有key,那麼就會採用遍歷查找的方式去找到對應的舊節點。一種一個map映射,另一種是遍歷查找。相比而言。map映射的速度更快。
vue部分源碼如下:
// vue項目 src/core/vdom/patch.js -488行
// 以下是爲了閱讀性進行格式化後的代碼
// oldCh 是一箇舊虛擬節點數組
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
// map 方式獲取
idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
// 遍歷方式獲取
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}
創建map函數
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
遍歷尋找
// sameVnode 是對比新舊節點是否相同的函數
function findIdxInOld (node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
第 2 題:[‘1’, ‘2’, ‘3’].map(parseInt) what & why ?
map((item, index, thisArr) => ( newArr ))
【參數解析】
item: callback 的第一個參數,數組中正在處理的當前元素。
index: callback 的第二個參數,數組中正在處理的當前元素的索引。
thisArr: callback 的第三個參數,map 方法被調用的數組。
【返回】
一個新數組,每個元素都是執行回調函數的結果。
parseInt(string, radix)
【參數解析】
string: 必需。要被解析的字符串。
radix: 可選。表示要解析的字符串的基數。該值介於 2 ~ 36 之間。如果省略該參數或其值爲 0,則數字將以 10 爲基礎來解析。如果它以 “0x” 或 “0X” 開頭,將以 16 爲基數。如果該參數小於 2 或者大於 36,則 parseInt() 將返回 NaN。
第 3 題:什麼是防抖和節流?有什麼區別?如何實現?
- 防抖
觸發高頻事件後n秒內函數只會執行一次,如果n秒內高頻事件再次被觸發,則重新計算時間
- 思路:
每次觸發事件時都取消之前的延時調用方法
function debounce(fn) {
let timeout = null; // 創建一個標記用來存放定時器的返回值
return function () {
clearTimeout(timeout); // 每當用戶輸入的時候把前一個 setTimeout clear 掉
timeout = setTimeout(() => { // 然後又創建一個新的 setTimeout, 這樣就能保證輸入字符後的 interval 間隔內如果還有字符輸入的話,就不會執行 fn 函數
fn.apply(this, arguments);
}, 500);
};
}
function sayHi() {
console.log('防抖成功');
}
var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖
- 節流
高頻事件觸發,但在n秒內只會執行一次,所以節流會稀釋函數的執行頻率
- 思路:
每次觸發事件時都判斷當前是否有等待執行的延時函數
function throttle(fn) {
let canRun = true; // 通過閉包保存一個標記
return function () {
if (!canRun) return; // 在函數開頭判斷標記是否爲true,不爲true則return
canRun = false; // 立即設置爲false
setTimeout(() => { // 將外部傳入的函數的執行放在setTimeout中
fn.apply(this, arguments);
// 最後在setTimeout執行完畢後再把標記設置爲true(關鍵)表示可以執行下一次循環了。當定時器沒有執行的時候標記永遠是false,在開頭被return掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
第 4 題:介紹下 Set、Map、WeakSet 和 WeakMap 的區別?
Set
- 成員之間不能重複
- 只有鍵值,沒有鍵名,有點類似數組。
- 可以遍歷,方法有add, delete,has
weakSet
- 成員都是對象
- 成員都是弱引用,隨時可以消失。 可以用來保存DOM節點,不容易造成內存泄漏
- 不能遍歷,方法有add, delete,has
Map
- 本質上是鍵值對的集合,類似集合
- 可以遍歷,方法很多,可以幹跟各種數據格式轉換
weakMap
- 直接受對象作爲鍵名(null除外),不接受其他類型的值作爲健名
- 鍵名所指向的對象,不計入垃圾回收機制
- 不能遍歷,方法同get,set,has,delete
第 5 題:介紹下深度優先遍歷和廣度優先遍歷,如何實現?
-
深度優先遍歷(DFS)
DFS就是從圖中的一個節點開始追溯,直到最後一個節點,然後回溯,繼續追溯下一條路徑,直到到達所有的節點,如此往復,直到沒有路徑爲止。
注意:深度DFS屬於盲目搜索,無法保證搜索到的路徑爲最短路徑,也不是在搜索特定的路徑,而是通過搜索來查看圖中有哪些路徑可以選擇。
/*深度優先遍歷三種方式*/
let deepTraversal1 = (node, nodeList = []) => {
if (node !== null) {
nodeList.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
deepTraversal1(children[i], nodeList)
}
}
return nodeList
}
let deepTraversal2 = (node) => {
let nodes = []
if (node !== null) {
nodes.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
nodes = nodes.concat(deepTraversal2(children[i]))
}
}
return nodes
}
// 非遞歸
let deepTraversal3 = (node) => {
let stack = []
let nodes = []
if (node) {
// 推入當前處理的node
stack.push(node)
while (stack.length) {
let item = stack.pop()
let children = item.children
nodes.push(item)
// node = [] stack = [parent]
// node = [parent] stack = [child3,child2,child1]
// node = [parent, child1] stack = [child3,child2,child1-2,child1-1]
// node = [parent, child1-1] stack = [child3,child2,child1-2]
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i])
}
}
}
return nodes
}
-
廣度優先遍歷
BFS從一個節點開始,嘗試訪問儘可能靠近它的目標節點。本質上這種遍歷在圖上是逐層移動的,首先檢查最靠近第一個節點的層,再逐漸向下移動到離起始節點最遠的層let widthTraversal2 = (node) => { let nodes = [] let stack = [] if (node) { stack.push(node) while (stack.length) { let item = stack.shift() let children = item.children nodes.push(item) // 隊列,先進先出 // nodes = [] stack = [parent] // nodes = [parent] stack = [child1,child2,child3] // nodes = [parent, child1] stack = [child2,child3,child1-1,child1-2] // nodes = [parent,child1,child2] for (let i = 0; i < children.length; i++) { stack.push(children[i]) } } } return nodes }
*第 6 題:請分別用深度優先思想和廣度優先思想實現一個拷貝函數?
DFS用常規的遞歸問題不大,需要注意下重複引用的問題,不用遞歸的話就用棧
BFS就用隊列,整體代碼倒是差不多
// 如果是對象/數組,返回一個空的對象/數組,
// 都不是的話直接返回原對象
// 判斷返回的對象和原有對象是否相同就可以知道是否需要繼續深拷貝
// 處理其他的數據類型的話就在這裏加判斷
function getEmpty(o){
if(Object.prototype.toString.call(o) === '[object Object]'){
return {};
}
if(Object.prototype.toString.call(o) === '[object Array]'){
return [];
}
return o;
}
function deepCopyBFS(origin){
let queue = [];
let map = new Map(); // 記錄出現過的對象,用於處理環
let target = getEmpty(origin);
if(target !== origin){
queue.push([origin, target]);
map.set(origin, target);
}
while(queue.length){
let [ori, tar] = queue.shift();
for(let key in ori){
// 處理環狀
if(map.get(ori[key])){
tar[key] = map.get(ori[key]);
continue;
}
tar[key] = getEmpty(ori[key]);
if(tar[key] !== ori[key]){
queue.push([ori[key], tar[key]]);
map.set(ori[key], tar[key]);
}
}
}
return target;
}
function deepCopyDFS(origin){
let stack = [];
let map = new Map(); // 記錄出現過的對象,用於處理環
let target = getEmpty(origin);
if(target !== origin){
stack.push([origin, target]);
map.set(origin, target);
}
while(stack.length){
let [ori, tar] = stack.pop();
for(let key in ori){
// 處理環狀
if(map.get(ori[key])){
tar[key] = map.get(ori[key]);
continue;
}
tar[key] = getEmpty(ori[key]);
if(tar[key] !== ori[key]){
stack.push([ori[key], tar[key]]);
map.set(ori[key], tar[key]);
}
}
}
return target;
}
// test
[deepCopyBFS, deepCopyDFS].forEach(deepCopy=>{
console.log(deepCopy({a:1}));
console.log(deepCopy([1,2,{a:[3,4]}]))
console.log(deepCopy(function(){return 1;}))
console.log(deepCopy({
x:function(){
return "x";
},
val:3,
arr: [
1,
{test:1}
]
}))
let circle = {};
circle.child = circle;
console.log(deepCopy(circle));
})
第 7 題:ES5/ES6 的繼承除了寫法以外還有什麼區別
1.class
聲明會提升,但不會初始化賦值。Foo
進入暫時性死區,類似於 let
、const
聲明變量。
const bar = new Bar(); // it's ok
function Bar() {
this.bar = 42;
}
const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
constructor() {
this.foo = 42;
}
}
2.class
聲明內部會啓用嚴格模式。
// 引用一個未聲明的變量
function Bar() {
baz = 42; // it's ok
}
const bar = new Bar();
class Foo {
constructor() {
fol = 42; // ReferenceError: fol is not defined
}
}
const foo = new Foo();
3.class
的所有方法(包括靜態方法和實例方法)都是不可枚舉的。
// 引用一個未聲明的變量
function Bar() {
this.bar = 42;
}
Bar.answer = function() {
return 42;
};
Bar.prototype.print = function() {
console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']
class Foo {
constructor() {
this.foo = 42;
}
static answer() {
return 42;
}
print() {
console.log(this.foo);
}
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []
4.class
的所有方法(包括靜態方法和實例方法)都沒有原型對象 prototype,所以也沒有[[construct]]
,不能使用 new
來調用。
function Bar() {
this.bar = 42;
}
Bar.prototype.print = function() {
console.log(this.bar);
};
const bar = new Bar();
const barPrint = new bar.print(); // it's ok
class Foo {
constructor() {
this.foo = 42;
}
print() {
console.log(this.foo);
}
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
5.必須使用 new
調用 class
。
function Bar() {
this.bar = 42;
}
const bar = Bar(); // it's ok
class Foo {
constructor() {
this.foo = 42;
}
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
6.class
內部無法重寫類名。
function Bar() {
Bar = 'Baz'; // it's ok
this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}
class Foo {
constructor() {
this.foo = 42;
Foo = 'Fol'; // TypeError: Assignment to constant variable
}
}
const foo = new Foo();
Foo = 'Fol'; // it's ok