第十八章-面試真題
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
4. 再學閉包
4.1 函式 call 和 apply 的區別?
call() 和 apply() 的作用相同,都可以改變 this 的指向,但是引數有所不同。第一個引數都是 作為函式上下文的物件,call() 的第二個引數是一個引數列表;apply() 的第二個引數是一個數組或者類陣列。
func.apply(obj, ['A', 'B']);
func.call(obj, 'C', 'D');
用法:
-
改變 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); }
-
借用別的物件的方法
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 的屬性和方法。
-
呼叫函式
apply、call 方法都會使函式立即執行,因此它們也可以用來呼叫函式。
function func() { console.log('linxin'); } func.call();// linxin
call 和 bind 的區別
在 EcmaScript5 中擴充套件了叫 bind 的方法,在低版本的 IE 中不相容。它和 call 很相似,接受的引數有兩部分,第一個引數是是作為函式上下文的物件,第二部分引數是個列表,可以接受多個引數。
它們之間的區別有以下兩點。
-
bind() 的返回值是函式。
var obj = { name: 'linxin' } function func() { console.log(this.name); } var func1 = func.bind(obj); func1(); // linxin
bind 方法不會立即執行,而是返回一個改變了上下文 this 後的函式。而原函式 func 中的 this 並沒有被改變,依舊指向全域性物件 window。
-
引數的使用
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 前端效能優化?一般從哪幾個方面考慮?
原則:多使用記憶體、快取,減少計算、減少網路請求
方向:載入頁面,頁面渲染,頁面操作流暢度