1. 程式人生 > 實用技巧 >第十八章-面試真題

第十八章-面試真題

1. 何為變數提升?

1.1 var 和 let const 的區別?
  • var 是 ES5 語法,let 和 const ES6 語法
  • var 存在變數提升的情況(可以先使用再賦值),let 和 const 不存在變數提升
  • var 和 let 是變數,可修改;const 是常量,必須賦初始值而且不可修改
  • let const 有塊級作用域,有暫時性死區的特性,var 沒有塊級作用域

使用 var 定義的變數或者函式表示式會存在變數提升的情況:將變數或者函式的定義提升到程式碼塊的頂部,但不會提升賦值。

console.log(a) // 輸出 undefined。原因是使用 var 定義的變數存在變數提升的情況
var a = 10
-----相當於下面的程式碼-----
var a // 將變數的定義提升到了程式碼塊的頂部
console.log(a)
a = 10

// 塊級作用域
for (var i = 0; i < 10; i++) {
	var j = i + 1
}
console.log(i, j) // 輸出 10, 10
// 換成 let 就會報錯 -- 原因:let 存在塊級作用域
for (let i = 0; i < 10; i++) {
	let j = i + 1
}
console.log(i, j) // 報錯
1.2 typeof 返回哪些型別?
  • 值型別:undefined string number boolean symbol
  • 引用型別:object (注意 typeof null === 'object' )
  • 函式:function
1.3 列舉強制型別轉換和隱式型別轉換?

強制型別轉換:parseInt() parseFloat() toString()

隱式型別轉換:if、邏輯判斷、==、 + 字串拼接

2. 手寫深度比較 isEqual

2.1 手寫深度比較,模擬 lodash 的 isEqual
		// 判斷傳過來的引數是不是一個物件
        function isObject(obj) {
            return typeof obj === 'object' && obj !== null
        }

        function isEqual(obj1, obj2) {
            // 如果有一個不是物件,那麼就直接判斷就好了
            if (!isObject(obj1) || !isObject(obj2)) {
                // 參與 equal 的一般不會是函式
                return obj1 === obj2
            }
            // 如果傳遞過來的引數是 同一個物件,直接返回 true 
            if (obj1 === obj2) {
                return true
            }

            // 如果兩個都是物件或者陣列,而且不是同一個引數
            // 1. 先取出 obj1 和 obj2 的 keys ,比較個數
            const obj1Keys = Object.keys(obj1)
            const obj2Keys = Object.keys(obj2)
            if (obj1Keys.length !== obj2Keys.length) {
                return false
            }   
            // 2. 以 obj1 為基準,和 obj2 依次遞迴比較
            for (let key in obj1 ) {
                // 比較當前 key 的 value --遞迴
                const res = isEqual(obj1[key], obj2[key])
                if (!res) {
                    return false
                }
            }
            // 3. 全相等
            return true
        }
        
        const obj1 = {
            a: 100,
            b: { x: 10,y: 20 }
        }
        const obj2 = {
            a: 100,
            b: { x: 10,y: 20 }
        }
        // console.log(isEqual(obj1, obj1))
        const arr1 = [1,2,3,4]
        const arr2 = [1,2,3,4]
        console.log(isEqual(arr1, arr2))
2.2 split() 和 join() 的區別

split() 將字串以某個字元分割成陣列

join() 將陣列以某個字元組合成一個字串

'1-2-3'.split('-') // [1,2,3]
[1,2,3].join('-') // '1-2-3'
2.3 陣列的 pop push unshift shift 分別做什麼

參考:https://aurorablog.top/archives/ec6bfcc0.html

功能是什麼?

返回值是什麼?

是否會對原陣列造成影響?

方法 功能 返回值 是否影響原陣列
pop 刪除陣列的最後一個值 返回被刪除的那個值
push 在陣列最後增加一項 返回增加一項後的陣列長度
unshift 在陣列最前面增加一項 返回增加一項後的陣列長度
shift 刪除陣列的第一個值 返回刪除的那個值

陣列的 api 有那些事純函式?

純函式:1. 不改變原陣列(改變原函式有副作用) 2. 返回一個數組

const arr = [1, 2, 3, 4, 5]
// concat 
const arr1 = arr.concat([6, 7, 8])
console.log(arr1) // [1, 2, 3, 4, 5, 6, 7, 8]

// map
const arr2 = arr.map(num => num * 10) 
console.log(arr2) // [10, 20, 30, 40, 50]

// filter
const arr3 = arr.filter(num => num > 2)
console.log(arr3) // [3, 4, 5]

// slice 引數是索引,擷取一個新的陣列
const arr4 = arr.slice(1, 3)
console.log(arr4) // [2, 3]
const arr5 = arr.slice()
console.log(arr5) // [1, 2, 3, 4, 5]

非純函式:

  • pop push unshift shift
  • forEach
  • some every
  • reduce

3. 陣列 map 方法

3.1 slice() 和 splice() 方法的區別?

功能區別:

​ 英文含義:slice - 切片; splice - 剪接

​ slice:擷取陣列的某一個片段

​ splice:剪接原函式的某一部分,返回值是剪接的部分,原函式修改為去除被剪接的部分。

引數和返回值:

slice():如果不傳入引數,則相當於將原陣列複製了一份;可以接受兩個引數,第一個引數是 startIndex ,第二個引數是 endIndex ,左閉右開;如果沒有第二個引數則會將 startIndex 後面所有的內容擷取。如果想要擷取最後幾個數,可以寫成 slice(-num) 意思是擷取最後的 num 個內容。

const arr = [10, 20, 30, 40, 50]
const arr1 = arr.slice() // [10, 20, 30, 40, 50]
const arr2 = arr.slice(1, 4) // [20, 30, 40]
const arr3 = arr.slice(2) // [30, 40, 50]
const arr3 = arr.slice(-2) // [40, 50]

splice():第一個引數是開始的位置,第二個引數的剪接的個數,第三個引數是要替換的元素值。返回值是被剪接的內容。

const arr = [1, 2, 3, 4, 5]
// 返回值 arr :[2, 3] 原陣列 arr 變為: [1, 'a', 'b', 4, 5]
const arr1 = arr.splice(1, 2, 'a', 'b')
// 從索引為 1 的位置插入兩項內容,arr:[1, "a", "b", 2, 3, 4, 5]
const arr2 = arr.splice(1, 0, 'a', 'b') // 返回值為空
// 從索引為 1 的位置刪除兩項內容,arr:  [1, 4, 5]
const arr3 = arr.splice(1, 2) // 返回值為 [2, 3]

是否是純函式:

​ slice() 是純函式, splice() 不是純函式。

3.2 [10, 20, 30].map(parseInt) 返回的結果是什麼?

map 的引數和返回值:

map() 方法定義在JavaScript的Array中, 它返回一個新的陣列, 陣列中的元素為原始陣列呼叫函式處理後的值。

array.map(function (currentValue, index, arr), thisIndex).

callback()

​ currentValue: 必須。 當前元素的的值。
​ index: 可選。 當前元素的索引。
​ arr: 可選。 當前元素屬於的陣列物件。
thisIndex 可選。 物件作為該執行回撥時使用, 傳遞給函式, 用作 "this"的值。

parseInt 的引數和返回值:

parseInt(string, radix) 解析一個字串並返回指定基數的十進位制整數, radix 是2-36之間的整數,表示被解析字串的基數。

string:要被解析的字串,如果不是一個字串,則將其轉換為字串(使用 ToString 抽象操作)。字串開頭的空白符將會被忽略。

radix:可選。該值介於 2~36 之間。如果省略該引數或其值為 0, 則數字將以 10 為基礎來解析。 如果它以“ 0x” 或“ 0X” 開頭, 將以 16 為基數。如果該引數小於 2 或者大於 36, 則 parseInt() 將返回 NaN

程式碼拆解:

[10, 20, 30].map((num,index)=>{
    return parseInt(num,index)
})

問題解析:

parseInt方法有兩個引數,預設接受了來自map方法的前兩個引數,map的前兩個引數分別是遍歷的值和索引;
所以parseInt接收到的三個組值得情況分別是:

parseInt(10,0):數字基數為0,數字以 10進位制解析,故結果為 10;
parseInt(20,1):數字基數為1,數字以 1進位制解析,1進製出現了2,1進位制無法解析,結果返回NaN;
parseInt(30,2):數字基數為2,數字以 2進位制解析,2進製出現了3,3進位制無法解析,結果返回NaN;

所以最終結果為:[10, NaN, NaN]。參考 MDN這篇文章

ajax 請求 get 和 post 的區別?
  • get 一般用於查詢操作,post 一般用於使用者提交操作
  • get 引數拼接在 url 上(可能會受限於某些瀏覽器對 url 長度的限制),post 引數放在請求體內(資料體積會更大)
  • 安全性:post 易於防止 CSRF

參考:Ajax中POST和GET的區別

4. 再學閉包

4.1 函式 call 和 apply 的區別?

call() 和 apply() 的作用相同,都可以改變 this 的指向,但是引數有所不同。第一個引數都是 作為函式上下文的物件,call() 的第二個引數是一個引數列表;apply() 的第二個引數是一個數組或者類陣列。

func.apply(obj, ['A', 'B']);
func.call(obj, 'C', 'D');

用法:

  1. 改變 this 指向

    var obj = {
        name: 'linxin'
    }
    function func() {
        console.log(this.name);
    }
    func.call(obj);
    

    call 方法的第一個引數是作為函式上下文的物件,這裡把 obj 作為引數傳給了 func,此時函式裡的 this 便指向了 obj 物件。此處 func 函式裡其實相當於

    function func() {
        console.log(obj.name);
    }
    
  2. 借用別的物件的方法

    var Person1  = function () {
        this.name = 'linxin';
    }
    var Person2 = function () {
        this.getname = function () {
            console.log(this.name);
        }
        Person1.call(this);
    }
    var person = new Person2();
    person.getname();       // linxin
    

    從上面我們看到,Person2 例項化出來的物件 person 通過 getname 方法拿到了 Person1 中的 name。因為在 Person2 中,Person1.call(this) 的作用就是使用 Person1 物件代替 this 物件,那麼 Person2 就有了 Person1 中的所有屬性和方法了,相當於 Person2 繼承了 Person1 的屬性和方法。

  3. 呼叫函式

    apply、call 方法都會使函式立即執行,因此它們也可以用來呼叫函式。

    function func() {
        console.log('linxin');
    }
    func.call();// linxin
    

call 和 bind 的區別

在 EcmaScript5 中擴充套件了叫 bind 的方法,在低版本的 IE 中不相容。它和 call 很相似,接受的引數有兩部分,第一個引數是是作為函式上下文的物件,第二部分引數是個列表,可以接受多個引數。
它們之間的區別有以下兩點。

  1. bind() 的返回值是函式。

    var obj = {
        name: 'linxin'
    }
    function func() {
        console.log(this.name);
    }
    var func1 = func.bind(obj);
    func1();  // linxin
    

    bind 方法不會立即執行,而是返回一個改變了上下文 this 後的函式。而原函式 func 中的 this 並沒有被改變,依舊指向全域性物件 window。

  2. 引數的使用

    function func(a, b, c) {
        console.log(a, b, c);
    }
    var func1 = func.bind(null,'linxin');
    
    func('A', 'B', 'C');            // A B C
    func1('A', 'B', 'C');           // linxin A B
    func1('B', 'C');                // linxin B C
    func.call(null, 'linxin');      // linxin undefined undefined
    

    call 是把第二個及以後的引數作為 func 方法的實參傳進去,而 func1 方法的實參實則是在 bind 中引數的基礎上再往後排。

4.2 事件代理(事件委託)是什麼?

事件捕獲和事件冒泡都是為了解決頁面中事件流(事件的執行順序)的問題。

<div id="outer">
    <p id="inner">Click me!</p>
</div>

事件捕獲:事件從最外層觸發,直到找到最具體的元素。

如上面的程式碼,在事件捕獲下,如果點選p標籤,click事件的順序應該是 document->html->body->div->p

事件冒泡:事件會從最內層的元素開始發生,一直向上傳播,直到觸發document物件。

因此在事件冒泡下,p元素髮生click事件的順序為 p->div->body->html->document

事件繫結:

js通過 addEventListener 繫結事件。 addEventListener 的第三個引數就是為冒泡和捕獲準備的。

addEventListener 有三個引數:

element.addEventListener(event, function, useCapture)

第一個引數是:需要繫結的事件

第二個引數是:觸發事件後要執行的函式

第三個引數是:預設值是 false,表示在 事件冒泡階段 呼叫事件處理函式;如果設定引數為 true,則表示在 事件捕獲階段 呼叫事件處理函式。

事件代理(事件委託)

對於事件代理來說,在事件捕獲或者事件冒泡階段處理並沒有明顯的優劣之分,但是由於事件冒泡的事件流模型被所有主流的瀏覽器相容,從相容性角度來說通常使用事件冒泡模型。

事件代理的好處(為什麼要使用事件代理)?

比如100個(甚至更多)li標籤繫結事件,如果一個一個繫結,不僅會相當麻煩,還會佔用大量的記憶體空間,降低效能。使用事件代理的作用如下:

  • 程式碼簡潔
  • 減少瀏覽器記憶體佔用

事件代理的原理:

事件代理(事件委託) 是利用事件的冒泡原理來實現的,比如當我們點選內部的li標籤時,會冒泡到外層的ul,div等標籤上。因此,當我們想給很多個li標籤新增同一個事件的時候,可以給它的父級元素新增對應的事件,當觸發任意li元素時,會冒泡到其父級元素,這時繫結在父級元素的事件就會被觸發,這就是事件代理(委託),委託他們的父級代為執行事件。

demo

<ul id="ul1">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>
<script>
	window.onload = function(){
    	var oUl = document.getElementById("ul1");
   		oUl.onclick = function(){
        	alert(123);
    	}
	}
</script>

封裝通用的事件繫結函式

// 通用的事件繫結函式:
function bindEvent(elem,type,selector,fn){
    if(fn == null){
      fn = selector
      selector = null
    }
    elem.addEventListener(type,event=>{
      const target = event.target
      if(selector){
          //代理繫結
          if(target.matches(selector)){
            fn.call(target,event)
           }else{
           	//普通繫結
           	fn.call(target,event)
           }
      }
    })
}
4.3 閉包是什麼,有什麼特性?有什麼負面影響?

影響:變數會常駐記憶體,得不到釋放。

閉包函式:宣告在一個函式中的函式,叫做閉包函式。

閉包:內部函式總是可以訪問到其所在的外部函式中,宣告的引數和變數,即使是在其外部函式被返回之後。

函式內部定義函式,並且這個內部函式能夠訪問到外層函式中定義的變數,就叫做閉包

特點

1、讓外部訪問函式內部變數成為可能。

2、區域性變數會常駐在記憶體中。

3、可以避免使用全域性變數,防止全域性變數汙染。

4、會造成記憶體洩漏(記憶體空間長期被佔用,而不被釋放)

閉包的建立

閉包就是可以建立一個獨立的環境,每個閉包裡面的環境都是獨立的,互不干擾。閉包會發生記憶體洩漏,每次外部函式執行的時候,外部函式的引用地址不同,都會重新建立一個新的地址。但凡是當前活動物件中又被內部子集引用的資料,那麼這個時候,這個資料不刪除,保留一根指標給內部活動物件

閉包使用場景:防抖函式

5. 回顧 DOM 操作和優化

5.1 阻止事件冒泡和預設行為
// 阻止預設
event.stopPropagation()
// 阻止預設行為
event.preventDefault()
5.2 DOM 操作

6. jsonp 是 ajax 嗎

6.1 解釋 jsonp 的原理,為何 jsonp 不是跨域?

jsonp 是通過 script 標籤來實現跨域的(script 標籤預設可以訪問外域連結),而 ajax 是通過 XMLHttpRequest 這個 api 實現的。

同源策略:協議、域名、埠號都相同

載入圖片 css js 可以無視同源策略

  • <img src=跨域的圖片地址> 如果引用的圖片網站做了圖片的防盜鏈,那麼瀏覽器就無法載入這個圖片
  • <link href=跨域的css地址>
  • <script src=跨域的js地址><script>
  • <img /> 可用於統計打點,可使用第三方統計服務
  • <link /> 和 <script> 可使用 CDN,CDN一般都是外域
  • <script> 可實現 JSONP

跨域實現

6.2 document load 和 ready 的區別
window.addEventListener('load', function() {
    // 頁面的全部資源載入完才會執行,包括圖片、視訊等
})

document.addEventListener('DOMContentLoaded', function () {
    // DOM 渲染完成即可執行,此時圖片、視訊可能沒有載入完畢
})
6.3 == 和 === 的區別
  • == 會嘗試進行型別轉換

  • === 嚴格相等

  • 那些場景下用 == :除了 == null 都用 ===

    • const obj = {x: 10}
      if (obj.a == null) {
      	// 相當於
          // if (obj.a === null || obj.a === null) {}
      }
      

7. 是否使用過 Object.create()

7.1 函式宣告和函式表示式的區別

函式宣告:function fn() {...}

函式表示式:const fn = function () {...}

函式宣告會在程式碼執行前預載入,而函式表示式不會

// 函式宣告
var res = sum(10, 20)
console.log(res) // 30

function sum(num1, num2) {
    return num1 + num2
}
----------------------
// 函式表示式
var res = sum(10, 20)
console.log(res) // 報錯:sum 不是一個函式

var sum = function(num1, num2) {
    return num1 + num2
}
7.2 new Object() 和 Object.create() 的區別
  • {} 等同於 new Object() , 原型是 Object.prototype
  • Object.create(null) 沒有原型
  • Object.create({...}) 可以指定原型

Object.create() 建立一個空物件,Object.create(obj) 把建立的空物件的原型設定為 obj

8. 常見的正則表示式

8.1 判斷字串以字母開頭,後面字母數字下劃線,長度 6-30
	/^[a-zA-Z]\w{5, 29}$/ 

正則表示式

8.2 作用域和自由變數的場景題
let i
for (i = 1; i <=3; i++) {
    setTimeout(function() { // 非同步程式碼
        console.log(i)
    }, 0)
}
// 輸出:4 4 4


let a = 100
function test() {
    alert(a)
    a = 10
    alert(a)
}
test() // 100 10
alert(a) // 10

9. 如何獲取最大值

9.1 手寫字串 trim 保證瀏覽器相容性
String.prototype.trim = funciton() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '')
}
9.2 如何獲取一組數中的最大值
function max() {
    nums = Array.prototype.slice.call(arguments) // 變為陣列
    let max = 0
    nums.forEach(num => {
        if (num > max) {
            max = num
        }
    });
    return max
}
max(1, 2, 3, 4, 5) // 5

使用 Math.max() 方法

9.3 如何使用 js 實現繼承
  • class 繼承
  • prototype 繼承

10. 解析 URL 引數

10.1 如何捕獲 js 中的異常
try {
    // todo
} catch(ex) {
    // 手動捕獲異常
    console.log(ex)
} finally {
	// todo
}

// 自動捕獲
window.onerror = function (message, source, lineNum, colNum, error) {
    // 1. 對於跨域的 js ,如 cdn 不會有詳細的報錯資訊
    // 2. 對於壓縮的 js ,還要配合 sourceMap 反查到未壓縮程式碼的行和列
}
10.2 什麼是 JSON
  • json 是一種資料格式,本質上是一段字串。
  • json 格式和 js 物件結構一致,對 js 語言更加友好
  • window.JSON 是一個全域性物件:JSON.stringify 和 JSON.parse
10.3 獲取當前頁面 URL 的引數
// 傳統方式
function query(name) {
    const search = location.search.substr(1)
    // search: 'a=10&b=20&c=30'
    const reg = new RegExp(`(^|&)${name}=([^&*])(&|$)`, 'i')
    const res = search.match(reg)
    console.log(res)
    if (res === null) {
		return null
    }
    return res[2]
}
query('a') // 10

// URLSearchParams
function query(name) {
    const search = location.search
    const p = new URLSearchParams(search)
    return p.get(name)
}
console.log(query('a')) // 10

11. 陣列去重

11.1 將 url 引數轉換成 js 物件
// 傳統方式,分析 search
function queryToObj() {
    const res = {}
    const search = location.search.substr(1) // 去除 ? 
    search.split('&').forEach( paramsStr => {
        const arr = paramsStr.split('=')
        const key = arr[0]
        const value = arr[1]
        res[key] = value
    })
    return res
}

// URLSearchParams
function queryToObj() {
    const res = {}
    const paramsList = new URLSearchParams(location.search)
    paramsList.forEach((val, key) => {
        res[key] = val
    })
    return res
}
11.2 手寫 flatern ,考慮多層級(把多層巢狀陣列拍平成為一個數組)
function flat(arr) {
    // 判斷 arr 中有沒有深層陣列
    const isDeep = arr.some(item => item instanceof Array)
    // 如果沒有深層陣列,直接返回
    if (!isDeep) {
        return arr
    }
    const res = Array.prototype.concat.apply([], arr)
    return flat(res) // 遞迴
}
const res = flat( [1, 2, [10, 20, [300,400]]] )
console.log(res)
11.3 陣列去重
  • 傳統方式:遍歷元素,挨個比較
  • 使用 Set
  • 考慮計算效率:資料量很大的時候使用 Set 效率更高
function unique(arr) {
    const res = [] 
    arr.forEach(item => {
        if (res.indexOf(item) < 0) {
            res.push(item)
        }
    })
    return res
}
const res = unique([1,1,,1,1,2,3,3,3])
console.log(res)

// 使用 Set
function unique(arr) {
    return new Set(arr)
}
const res = unique([1, 1, 2, 3, 3, 2])
console.log(res)

12. 是否使用過 requestAnimationFrame(RAF)

12.1 手寫深拷貝
function deepClone(obj) {
    if ( typeof obj !== 'object' || obj == null) {
        // 如果 obj 不是物件,或者 obj 為 null 或者 undefined,直接返回
        return obj
    }
    let result
    if (obj instanceof Array) {
		result = []
    } else {
        result = {}
    }
    for (let key in obj) {
        // 保證 key 不是原型的屬性
        if (obj.hasOwnProperty(key)) {
            result[key] = deepClone(obj[key])
        }
    }
    // 返回結果
    return result
}
const res = deepClone({name: 'aurora', age: 21, address: {province: 'heilongjiang', city: 'haerbin'}})
console.log(res)

Object.assign() 不是深拷貝!!

12.2 是否使用過 requestAnimationFrame(RAF)
  • 想要動畫流暢,更新頻率要 60幀/s,即 16.7ms 更新一次檢視
  • setTimeout 需要手動控制頻率,而使用 RAF 瀏覽器會自動控制
  • 後臺標籤或者隱藏在 frame 標籤中, RAF 會自動停止,但 setTimeout 不會自動停止
<style>
    #div1 {
		width: 100px;
        height: 50px;
        background-color: red;
    }
</style>
<div id='div1'></div>

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
	// 3s 把寬度從 100px 變為 640px, 即增加 540px
    // 60幀/s 3s 180幀 每次增加 3px
    const $div = $('#div1')
    let curWidth = 100
    const maxWidth = 640
    
    // function animation() {
       // curWidth = curWidth + 3
       // $div1.css('width', curWidth)
       // if (curWidth < maxWidth) {
         //   // 自己控制時間
         //  setTimeout(animation, 16.7)
       // }
    // }
    
    function animation() {
        curWidth = curWidth + 3
        $div1.css('width', curWidth)
        if (curWidth < maxWidth) {
            window.requestAnimationFrame(animation)
        }
    }
    animation()
</script>
12.3 前端效能優化?一般從哪幾個方面考慮?

原則:多使用記憶體、快取,減少計算、減少網路請求

方向:載入頁面,頁面渲染,頁面操作流暢度