前端新手的初級前端面試學習筆記(有答案,有些我自己覺得可以)


千里之行始於足下, 基礎知識就要一個腳印一個腳印的走才能走的遠.我,前端新手,找了一點可能常出現的初級前端面試題,注意,是初級前端面試題.

每個問題的答案都是由我,也就是前端新手的我寫的,所以肯定有和大家相悖的地方,歡迎大家支出錯誤,大家一起成長,一起進步.

前端新手的我寫的

前端新手的我寫的

前端新手的我寫的

var和let const的區別

var,letconst我認爲這是ES6出來以後,肯定需要明白的知識點.

1 var是ES5的語法,let,const是ES6的語法; var存在變量提升.

那什麼是變量提升?

在JavaScript中,具體是ES5之前(ES6中是沒有變量提升這個說法了,假設你只使用letconst),var聲明的變量(函數內var聲明的變量)/函數表達式/函數聲明的變量在詞法分析階段會被提升到變量當前的作用域的頂部,舉個🌰

console.log(a);
b();
console.log(c)
var a = 5

function b() {
  console.log('function b')
}

var c = function() {
  console.log('c')
}
c()

// 輸出結果
// undefined
// function b
// undefined
// c

從這裏代碼就能夠看出,雖然在變量,函數聲明之前就使用了,但是程序是不會報錯的,函數正常運行,變量輸入undefined.

作用域

變量提升裏面,我們說了變量在詞法分析階段會被提升到變量當前的作用域的頂部,這裏也有一個作用域的知識點,
作用域就是規定了變量的合法使用範圍,也就是說,同級的函數作用域/塊級作用域內的變量是不能互相直接訪問的(全局作用域除外),但是可以訪問上級作用域,依次向上,找到需要的變量爲止.
🌰

var a = 1;
function test(){
  function test1(){
    function test2(){
      console.log(a);
      console.log(b);
    }
    function test3() {
      var b = 2;
    }

    test2();
    test3();
  }
  test1()
}
test()
// 輸出結果
// 1
// error: Uncaught ReferenceError: b is not defined

2 letvar都是變量,可以修改其值,const常量,不能修改其值.
const聲明一個值類型的常量時, 是不能被修改的, 但如果是引用類型,比如Array,Set,Map,Object等,是可以使用引用類型堆棧存儲數據和地址的特性來修改一個const聲明的常量.

const聲明的常量必須初始化

3 letconst有塊級作用域,var沒有
在ES6之前,JavaScript只用兩種作用域,全局作用域和函數作用域.

全局作用域: 就是函數外聲明的變量時全局的.
函數作用域: 就是函數內部聲明的變量只能在函數內部使用.

而塊級作用域顧名思義就是隻能在一個塊裏面使用,{}代表一個塊級作用域,塊級作用域中的使用let,const聲明的變量常量在外部是不能不爲訪問到的,但是有個例外,就是在塊級作用域中使用var聲明的變量,可以在外部訪問.
🌰

{
    var x = 1;
    let y = 2;
}
console.log(x)
console.log(y)

// 輸出
// 1
// error: Uncaught ReferenceError: y is not defined

2. typeof返回那些類型

首先,需要只要typeof的作用是什麼.
typeof可以檢測變量的數據類型.具體如下:

  1. 所用的值類型
  2. 函數類型
  3. 判斷是否是引用類型(具體的引用類型需要使用instanceof來判斷)

🌰:

console.log(typeof "a");// string
console.log(typeof 1);// number
console.log(typeof true);// boolean
console.log(typeof Symbol("symbol"));// symbol
console.log(typeof a);// undefined
console.log(typeof {});// object
console.log(typeof []);// object
console.log(typeof new Set());// object
console.log(typeof new Map());// object
console.log(typeof b); // function

function b(){

}

所以typeof返回的類型如下:
值類型包含: string, boolean, number, symbol, undefined
函數類型: function
引用類型: set, map, array, null

這裏有一個undefinednull的知識點.

undefined和null

undefined: 是作用沒有進行賦值的變量的默認值
null: 表示一個沒有任何地址的對象

什麼時候使用null

釋放內存時,可以使用null賦值.

3. 列舉強制類型轉換和隱式類型轉換

強制類型轉換: 在代碼中明確指出需要把一個值的類型轉爲另一類型.
比如:paseInt, paseFloat, toString等系統提供的強制類型轉換方法.

隱式類型轉換: 代碼沒有明確,但是在使用過程中發生了類型轉換.
比如: if, 邏輯運算, ==, +拼接字符串.

🌰:

let a = 1

if(a) {
    console.log(a) // 這裏輸入了 1
}

console.log(a == "1") // true
console.log(a+"abc") // 1abc

let b = a * "2"
console.log(b); // 2

隱式類型轉換在平時開發中也是常常用到的.

手寫深度比較

一般來說, 在對比兩個值的是否相等的時候,我們一般使用的是===,但是這個是有侷限性的,比如對象,因爲對象即使值是一樣的,他們的地址也是不同的.

這個題主要的知識點是遞歸,兩個對象的深度比較,需要將他們的每一項都進行對比.
上🌰:

function deepCompare(obj1, obj2){
    // 判斷傳入的參數不爲空, 並且是object類型
    if(obj1 == null || obj2 == null || typeof obj2 !== 'object' || typeof obj1 !== 'object') {
        return obj1 === obj2
    }
    if(obj1 === obj2) return true
    const obj1Keys = Object.keys(obj1)
    const obj2Keys = Object.keys(obj2)
    if(obj1Keys.length !== obj2Keys.length) return false
    for(let key of obj1Keys) {
        let result = deepCompare(obj1[key], obj2[key])
        if(!result) {
            return false
        }
    } 
    return true
}

const obj1 = {
    a: 100,
    b: 200,
    c: {
        d: 300
    }
}

const obj2 = {
    a: 100,
    b: 200,
    c: {
        d: 400
    }
}

console.log(deepCompare(obj1, obj2)); // false

split()區別和join()的區別

split: 將字符串分割成數組
join: 將數組合併爲一個字符串
🌰

'a,b,c'.split(',') // ['a','b','c']
['a','b','c'].join('') // 'abc'

數組的pop(),push(),unshift(),shift()分別做什麼

先上一個🌰,再來說:

let arr = [1,2,3]
console.log(arr.pop()) // 3
console.log(arr) // [ 1, 2 ]
console.log(arr.push(4)) //3
console.log(arr) // [ 1, 2, 4 ]
console.log(arr.unshift(-1,0)) // 5
console.log(arr) // [ -1, 0, 1, 2, 4 ]
console.log(arr.shift()) //-1
console.log(arr) // [ 0, 1, 2, 4 ]

接下來我們一個一個來說:
pop(): 從數組的尾部去一個元素,並返回這個元素.
push(): 向數組添加元素,從數組的尾部插入,可以傳多個, 返回數組的長度.
shift(): 從數組的頭部去一個元素,並返回這個元素.
unshift(): 向數組添加元素,從數組的頭部插入,可以傳多個, 返回數組的長度.

講道理,這道題很簡單的, 但是這道題包含了另一個知識點,那就是純函數,
純函數就是不改變原來的數組,這裏就是arr,並且返回的也是一個數組,可以看到,上訴代碼中的四個方法,都改變原數組,也不返回一個數組,也就是說,這個幾個函數都不是純函數.還有forEach, some, every, reduce都不是純函數.

當然,在數組的方法中,是有純函數的方法的.

concat

concat可以向一個數組追加一個數組,並且在不改變原數組返回一個新的數組.
🌰

let arr = [1,2,3]
let new_arr = arr.concat({4,5,6})
console.log(arr) // [1,2,3]
console.log(new_arr) //[1,2,3,4,5,6]

map

遍歷數組的每一項,並返回一個數組:
🌰

let new_arr = arr.map(item => return item * 10) // [10,20,30]

filter

遍歷數組的每一項, 並返回一個經過條件判斷過濾後的數組.
🌰

let new_arr = arr.filter(item => return item > 1) // [2,3]

slice

slice(切片)可以在不修改原數組情況複製數組在某個範圍內的元素.
slice在不傳遞值的時候,類似於一個深拷貝的操作,是類似於.
🌰

let new_arr = arr.slice() // [1,2,3]
new_arr = arr.slice(1) // [2,3]
new_arr = arr.slice(0, 2) // [1,2]
new_arr = arr.slice(2) // [3]
new_arr = arr.slice(-1) // [3]

slice只傳一個值的時候, 會默認第一個值爲數組的長度

splice(非純函數)

因爲slicesplice命名有點相似,這也說一下splice
🌰

let new_arr = arr.aplice(1,2,0,0,0,0)
console.log(arr) // [3]
console.log(new_arr) // [0,1,0,0,0,0] 

splice剪接原數組,並且可以在剪接的同時在原來的位置填充元素.
或者不填充元素arr.aplice(1,2),或者是第二隻默認爲數組的長度arr.aplice(1,0,10,10,10,10)

ajax請求get和post的區別

  • get一般用於查詢操作,post一般用於提交數據操作
  • get參數拼接在url上,post放在請求體內
  • post可以防止CSRF

函數call和apply的區別,手寫call, apply, bind

首先,callapply都是用來修改函數中this的指向的,並且立即執行函數.🌰

function fruits(){}
            
fruits.prototype = {
    color: "red",
    say: function(){
        console.log(this.color);
    }
};

var another = {
    color: "yellow"
};

var apple = new fruits;
apple.say();                // red
apple.say.call(another);    // yellow
apple.say.apply(another);   // yellow 

他們的區別就在於傳入的參數方式不同, call參數是一個一個的,apply是一個數組或者類數組.還是上面的🌰

function fruits(){}
            
fruits.prototype = {
    color: "red",
    say: function(){
        console.log(this.color, arguments);
    }
};

var another = {
    color: "yellow"
};

var apple = new fruits;
apple.say();                // red
apple.say.call(another,1,1,1);    // yellow { '0': 1, '1': 1, '2': 1 }
apple.say.apply(another, [2,2,2]);   // yellow { '0': 2, '1': 2, '2': 2 }

事件代理(委託)是什麼

事件代理是基於事件冒泡來做的,所以,需要知道,什麼是事件冒泡.

事件冒泡

事件的操作沿着DOM結構一級一級向上傳遞,這種傳遞過程就叫事件冒泡🐶
🌰

<div>
  <p id='p1'>p1</p>
  <p id='p2'>p2</p>
  <p id='p3'>p3</p>
</div>
const p1 = document.getElementById('p1')
const div = document.querySelector('div')
p1.addEventListener('click', e => {
  alert('p1')
})
div.addEventListener('click', e => {
  alert('div')
})

在瀏覽中,會先彈出p1,再彈出div.

當然,也可以阻止事件冒泡, 只需要添加stopPropagation方法.

p1.addEventListener('click', e => {
  e.stopPropagation()
  alert('p1')
})

事件冒泡說了,就繼續來說事件代理.

事件代理

場景: 瀑布流
比如有這樣一個需求,一個div裏面可以無限制的添加a標籤,但是需要在點擊a標籤的時候知道點擊的是哪個a標籤,我們不可能一個一個的去綁定是事件,所以,就需要事件代理機制來實現這個需求.

把事件綁定到div上面,利用事件冒泡機制,觸發div的事件,利用特殊手段得到點擊的a標籤.
🌰

<div>
  <a href='#'>a1</p>
  <a href='#'>a2</p>
  <a href='#'>a3</p>
  <a href='#'>a4</p>
  <a href='#'>a5</p>
  .........
</div>
const div = document.querySelector('div')
div.addEventListener('click', e => {
  e.preventDefault() // 阻止默認行爲
  const target = event.target
  if(target.nodeName === 'A'){
   alert(target.innerHTML)
  }
})

這個就是事件代理了.

事件代理優點

  • 代碼簡潔
  • 減少瀏覽器內存佔用

閉包是什麼,有什麼特性,有什麼影響

閉包實際上只作用的一個特殊情況,一般有兩種表現:

  1. 函數作爲參數被傳遞
  2. 函數作爲返回值被返回

🌰

function test() {
 let a = 1
 return function(){
  console.log(a)
 }
}
let t = test()
let a = 2
t() // 1
function test(fn) {
 let a = 1
 fn()
}
let a = 2
function b(){
  console.log(a)
}
test(b()) // 2

有人可能有疑惑了,爲什麼第一個輸入1,第二個輸入2.
這個就是閉包的特性了,閉包內使用的自由變量,是在定義的時候使用的,而不是執行的時候.

影響

變量會常駐內存,得不到釋放. 所以閉包不要亂用

如何阻止事件冒泡和默認行爲

阻止冒泡:e.stopPropagation()
阻止默認行爲:e.preventDefault()

如何減少DOM操作

原因: 因爲DOM操作非常的消耗性能
緩存DOM查詢

const list = document.getElementById('a')

多次DOM操作,合併到一次插入

const d1 = document.getElementById('d1')
const div = document.createDocumentFragment()
for(let i = 0; i < 10; i++){
 const a = document.createElement('a')
 a.innerHTML = 'a'+i
 div.appendChild(a)
}
d1.appendChild(div)

解析jsonp的原理,爲什麼不是真正的ajax

ajax是通過XMLHttpRequest,jsonp是通過script標籤來使用的.
首先看看jsonp的代碼實現:

<script>
 window.a = function(data){ console.log(data) }
</script>
<script src="./jsonp.js"></script>

jsonp.js

a({name: '1234567'})

運行打印出{name: '1234567'}這數據,這個就是jsop的實現方式.這裏也能看出來,它不是真正的ajax.

document load 和 ready 的區別

window.addEventListener('load', function(){
  // 頁面的全部的資源加載完纔會執行,包括圖片和視頻
})
document.addEventListener('DOMContentLoaded', function(){
 // DOM渲染完就執行, 圖片,視頻可能還沒加載完
})

== 和 === 的區別

== 會嘗試類型轉換
=== 是嚴格相等的

函數聲明跟函數表達式的區別

代碼說明最有效:

// 函數聲明
function a(){}
// 函數表達式
let b = function(){}

首先: 函數聲明式function在前,函數名在後,函數表達式是function在後面,賦值給一個變量或者常量.

其次: 函數聲明式會在代碼執行前預加載(相當於變量提升),函數表達式是把這個聲明的變量提升到頂部,而函數不會預加載,如果在函數表達式之前調用函數,是無法成功調用的.


a()
console.log(b) // 這裏只能使用b這個變量,而不是b() 

// 函數聲明
function a(){}
// 函數表達式
let b = function(){}

new Object()和Object.create()的區別

new Object()相當於我們平常直接使用{}來定義一個對象,原型是Object.prototype

let a = new Object()
console.log(a.__proto__ === Object.prototype); // true

Object.create()是可以指定原型的,當傳遞null是,就表示這個由Object.create()創建的對象是沒有原型的.
🌰

let a = Object.create(null)
console.log(a.__proto__); // undefined

let b = {c: 1}
let d = Object.create(b);
console.log(d.__proto__ === b); // true

關於this的場景

這句話要記住: this取什麼值,是在函數執行的時候定義的,不是在函數定義的定義的

  1. 普通函數中, this指向window
  2. call, apply, bind指向特指的對象
  3. 對象方法中,this指向對象. 但是在對象方法中使用了例如setTimeOut, setInterval的這樣全局對象的函數,this還是指向的window
  4. class中的this指向這個class對象
  5. 箭頭函數,箭頭函數的this永遠取它上級作用域的this

手寫apply, call, bind

bind:

Function.prototype.bind1 = function() {
    const args = Array.prototype.slice.call(arguments)
    const t = args.shift()
    const self = this
    return function() {
        self.apply(t, args)
    }
}

function fn() {
    console.log(this.x);
}

let nfn = fn.bind1({x: 10})
nfn()

apply, call

function fnS(context) {
    let fnSymbol = Math.random() + new Date().getTime().toString()
    if (context.hasOwnProperty(fnSymbol)) {
        return fnS(context)
    } else {
        return fnSymbol
    }
}

Function.prototype.call1 = function () {
    let args = [...arguments]
    let context = args.shift() || window
    let fn = fnS(context)
    context[fn] = this
    context[fn](...args)
    delete context[fn]
}

fn.call1({ x: 10 }, 1, 2, 3)

Function.prototype.apply1 = function () {
    let args = [...arguments]
    let context = args.shift() || window
    let fn = fnS(context)
    context[fn] = this
    context[fn](...args[0])
    delete context[fn]
}

fn.apply1({ x: 10 }, [2, 3, 4])

判斷字符串以字母開頭,後面是字母數字下劃線,長度有限制

一般來說,考慮正則表達式:

let reg = '/^[a-zA-Z]\w{2,20}$/'

這個表示匹配一個字符串,第一個是字母,後面是字母數字下劃線,長度爲3-21.
這個題的知識點在於會不會正則表達式的使用.

手寫字符串trim方法,保證瀏覽器的兼容器

String.replace(/^\s+/,'').replace(/\s+$/,'')

如何獲取多個數字的最大值

考慮瀏覽的兼容性:

function max(){
  const nums = Array.prototype.slice.call(arguments)
  let max = 0
  nums.forEach(n=>{
    if(n > max) max = n
  })
  return max
}
max(2,3,1,6,7,2,10) // 10

不考慮兼容性:
Math.max(2,3,1,6,7,2,10) // 10
最小值:
Math.min(2,3,1,6,7,2,10) // 1

如何用js實現繼承

js實現繼承有兩種方式,一個是ES6的class實現繼承,一種是之前的prototype實現繼承.

class

class A{
    constructor(){}
    say(){
        console.log('A')
    }
}
class B extends A {
    constructor(){
        super()
    }
}
let b = new B()
b.say() // A

prototype

function A(){}
A.prototype.say = function(){
    console.log('A');
}
function B(){}
B.prototype = new A()

let b = new B()
b.say() // A

如何捕獲js程序中的異常

常用的方式:try...catch()...

try{
    
} catch (err) {
    console.error(err)
} finally {
    
}

利用window.onerror自動捕獲

window.onerror = function(message, source, line, col, error){
    
}

什麼是JSON

  • json是一種數據格式,本質是一段字符串
  • json格式和JS對象結構一致,對JD語言更友好.畢竟json的全稱是(JavaScript Obejct Model)
  • window.JSON是一個全局對象, JSON.stringify, JSON.parse可以進行json和字符串相互轉換

獲取當前頁面url參數

傳統方式: location.search
新的API: URLSearchParams(還是新的好用,注意兼容性就行)

🌰
indexl.html?a=1&b=2, location.search獲取的數據爲?a=1&b=2

將url參數解析爲js對象

傳統方法:

function queryToObject(){
    const search = location.search.substr(1)
    const res = {}
    search.split('&').forEach(param => {
        const arr = param.split('=')
        res[arr[0]] = arr[1]
    })
    return res
}

使用URLSearchParams

function queryToObject(){
    const res =t}
    const pList = new URLSearchParams(location.search)
    pList.forEach((val, key) =>{
        res [key] = val
    })
    return res
}

手寫數組flatern,考慮多層級

這樣一個多層級的數組.[[1,2], 3, [4,5, [6,7, [8, 9, [10, 11]]]]]
按照順序將這個順序展開到一個數組中.

function flatern(arr){
    const isArray = arr.some(item => return item instanceof Array)
    if(!isArray) {
        return arr
    }
    const res = Array.prototype.concat.apply([], arr)
    return flatern(res)
}

數組去重

let a = [1,1,2,3,4,5,6,7,5,3,2]
使用Set:

let arr = [...new Set(..a)] // [1,2,3,4,5,6,7]

傳統方式,遍歷去重:

function deduplication(arr){
    let res = []
    arr.forEach(item => {
        if(res.indexOf(item) == -1) {
            res.push(item)
        }
    })
    return res
}

console.log(deduplication(a));

手寫深拷貝

function deepClone(obj = {}) {
    if(typeof obj !== 'object' || obj == null) {
        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
}

let obj = {
    name: "Name",
    address:{
        city: 'chengdu'
    }
}

let obj2 = deepClone(obj)
obj2.address.city = "yibin"
console.log(obj);

介紹一下RAF requestAnimationFrame

前端很多時候會使用動畫,而一個流暢的動畫,更新的頻率需要60幀/s,也就是16ms就要更新視圖.人眼才能感覺流暢,不卡頓.
一般來說,是使用setTimeout來手動更新的,而requestAnimationFrame是靠瀏覽器自動控制的.
requestAnimationFrame還會自動暫停不需要更新的動畫,而setTimeout不會停止

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