1. 程式人生 > 實用技巧 >35道面向初中級前端的基礎面試題

35道面向初中級前端的基礎面試題

如需獲取完整版229頁PDF面試題,請直接滑到文末。

1. 什麼是同源策略?

同源策略可防止 JavaScript 發起跨域請求。源被定義為協議、主機名和埠號的組合。此策略可防止頁面上的惡意指令碼通過該頁面的文件物件模型,訪問另一個網頁上的敏感資料。

參考資料:

2. 跨域是什麼?

  • 原因

瀏覽器的同源策略導致了跨域

  • 作用

用於隔離潛在惡意檔案的重要安全機制

  • 解決
  1. jsonp ,允許 script 載入第三方資源
  2. 反向代理(nginx 服務內部配置 Access-Control-Allow-Origin *)
  3. cors 前後端協作設定請求頭部,Access-Control-Allow-Origin 等頭部資訊
  4. iframe 巢狀通訊,postmessage

參考資料:

3. JSONP 是什麼?

這是我認為寫得比較通俗易懂的一篇文章jsonp原理詳解——終於搞清楚jsonp是啥了

4. 事件繫結的方式

  • 嵌入dom
<button onclick="func()">按鈕</button>
  • 直接繫結
btn.onclick = function(){}
  • 事件監聽
btn.addEventListener('click',function(){})

5. 事件委託

事件委託利用了事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。所有用到按鈕的事件(多數滑鼠事件和鍵盤事件)都適合採用事件委託技術, 使用事件委託可以節省記憶體。

<ul>
  <li>蘋果</li>
  <li>香蕉</li>
  <li>鳳梨</li>
</ul>

// good
document.querySelector('ul').onclick = (event) => {
  let target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
}) 

6. 事件迴圈

事件迴圈是一個單執行緒迴圈,用於監視呼叫堆疊並檢查是否有工作即將在任務佇列中完成。如果呼叫堆疊為空並且任務佇列中有回撥函式,則將回調函數出隊並推送到呼叫堆疊中執行。

7. 如何自定義事件

新模式

const div = document.createElement('div') // 不建立元素,直接用 window 物件也可以
const event = new Event('build')

div.addEventListener('build', function(e) {
    console.log(111)
})

div.dispatchEvent(event)

過時的模式

  1. 原生提供了3個方法實現自定義事件
  2. document.createEvent('Event')建立事件
  3. initEvent初始化事件
  4. dispatchEvent觸發事件
const events = {}

function registerEvent(name) {
    const event = document.createEvent('Event')
    event.initEvent(name, true, true) // 事件名稱,是否允許冒泡,該事件的預設動作是否可以被取消
    events[name] = event
}

function triggerEvent(name) {
    window.dispatchEvent(events[name])
}

registerEvent('resize') // 註冊 resize 事件
triggerEvent('resize') // 觸發 resize 事件

MDN

8. target 和 currentTarget 區別

  • event.target 返回觸發事件的元素
  • event.currentTarget 返回繫結事件的元素

9. prototype 和proto的關係是什麼

  1. prototype用於訪問函式的原型物件。
  2. __proto__用於訪問物件例項的原型物件(或者使用Object.getPrototypeOf())。
function Test() {}
const test = new Test()
test.__proto__ == Test.prototype // true

也就是說,函式擁有prototype屬性,物件例項擁有__proto__屬性,它們都是用來訪問原型物件的。

函式有點特別,它不僅是個函式,還是個物件。所以它也有__proto__屬性。

為什麼會這樣呢?因為函式是內建建構函式Function的例項:

const test = new Function('function Test(){}')
test.__proto__ == Function.prototype // true

所以函式能通過__proto__訪問它的原型物件。

由於prototype是一個物件,所以它也可以通過__proto__訪問它的原型物件。物件的原型物件,那自然是Object.prototype了。

function Test() {}
Test.prototype.__proto__ == Object.prototype // true

這樣看起來好像有點複雜,我們可以換個角度來看。Object其實也是內建建構函式:

const obj = new Object()
obj.__proto__ == Object.prototype // true

從這一點來看,是不是更好理解一點。

為了防止無休止的迴圈下去,所以Object.prototype.__proto__是指向null的,null是萬物的終點。

Object.prototype.__proto__ == null // true

既然null是萬物的終點,那使用Object.create(null)建立的物件是沒有__proto__屬性的,也沒有prototype屬性。

10. 原型繼承

所有的 JS 物件(JS 函式是 prototype)都有一個__proto__屬性,指向它的原型物件。當試圖訪問一個物件的屬性時,如果沒有在該物件上找到,它還會搜尋該物件的原型,以及該物件的原型的原型,依次層層向上搜尋,直到找到一個名字匹配的屬性或到達原型鏈的末尾。

11. 繼承

寄生組合式繼承

function SuperType(name) {
    this.name = name
    this.colors = ['red']
}

SuperType.prototype.sayName = function() {
    console.log(this.name)
}
// 繼承例項屬性
function SubType(name, age) {
    SuperType.call(this, name)
    this.age = age
}

function inheritPrototype(subType, superType) {
    let prototype = Object.create(superType.prototype)
    prototype.constructor = subType
    subType.prototype = prototype
}
// 繼承原型方法
inheritPrototype(SubType, SuperType)

// 定義自己的原型方法
SubType.prototype.sayAge = function() {
    console.log(this.age)
}

12. 閉包

閉包是指有權訪問另一個函式作用域中的變數的函式。

function sayHi(name) {
    return () => {
       console.log(`Hi! ${name}`)
    }
}
const test = sayHi('xiaoming')
test() // Hi! xiaoming

雖然sayHi函式已經執行完畢,但是其活動物件也不會被銷燬,因為test函式仍然引用著sayHi函式中的變數name,這就是閉包。

但也因為閉包引用著另一個函式的變數,導致另一個函式即使不使用了也無法銷燬,所以閉包使用過多,會佔用較多的記憶體,這也是一個副作用。

利用閉包實現私有屬性

const test = (function () {
    let value = 0
    return {
        getVal() { return value },
        setVal(val) { value = val }
    } 
})()

上面的程式碼實現了一個私有屬性value,它只能用過getVal()來取值,通過setVal(val)來設定值。

13. 記憶體回收

在 JS 中,有兩種記憶體回收演算法。第一種是引用計數垃圾收集,第二種是標記-清除演算法(從2012年起,所有現代瀏覽器都使用了標記-清除垃圾回收演算法)。

引用計數垃圾收集

如果一個物件沒有被其他物件引用,那它將被垃圾回收機制回收。

let o = { a: 1 }

一個物件被建立,並被 o 引用。

o = null

剛才被 o 引用的物件現在是零引用,將會被回收。

迴圈引用

引用計數垃圾收集有一個缺點,就是迴圈引用會造成物件無法被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

在 f() 執行後,函式的區域性變數已經沒用了,一般來說,這些區域性變數都會被回收。但上述例子中,o 和 o2 形成了迴圈引用,導致無法被回收。

標記-清除演算法

這個演算法假定設定一個叫做根(root)的物件(在Javascript裡,根是全域性物件)。垃圾回收器將定期從根開始,找所有從根開始引用的物件,然後找這些物件引用的物件……從根開始,垃圾回收器將找到所有可以獲得的物件和收集所有不能獲得的物件。

對於剛才的例子來說,在 f() 執行後,由於 o 和 o2 從全域性物件出發無法獲取到,所以它們將會被回收。

高效使用記憶體

在 JS 中能形成作用域的有函式、全域性作用域、with,在 es6 還有塊作用域。區域性變數隨著函式作用域銷燬而被釋放,全域性作用域需要程序退出才能釋放或者使用 delete 和賦空值nullundefined

在 V8 中用 delete 刪除物件可能會干擾 V8 的優化,所以最好通過賦值方式解除引用。

參考資料:

14. 有一個函式,引數是一個函式,返回值也是一個函式,返回的函式功能和入參的函式相似,但這個函式只能執行3次,再次執行無效,如何實現

這個題目是考察閉包的使用

function sayHi() {
    console.log('hi')
}

function threeTimes(fn) {
    let times = 0
    return () => {
        if (times++ < 3) {
            fn()
        }
    }
}

const newFn = threeTimes(sayHi)
newFn()
newFn()
newFn()
newFn()
newFn() // 後面兩次執行都無任何反應

通過閉包變數times來控制函式的執行

15. 實現add函式,讓add(a)(b)和add(a,b)兩種呼叫結果相同

實現1

function add(a, b) {
    if (b === undefined) {
        return function(x) {
            return a + x
        }
    }

    return a + b
}

實現2——柯里化

function curry(fn, ...args1) {
    // length 是函式物件的一個屬性值,指該函式有多少個必須要傳入的引數,即形參的個數。
    if (fn.length == args1.length) {
        return fn(...args1)
    }

    return function(...args2) {
        return curry(fn, ...args1, ...args2)
    }
}

function add(a, b) {
    return a + b
}

console.log(curry(add, 1)(2)) // 3
console.log(curry(add, 1, 2)) // 3

16. 使用Ajax的優缺點分別是什麼

優點

  • 互動性更好。來自伺服器的新內容可以動態更改,無需重新載入整個頁面。
  • 減少與伺服器的連線,因為指令碼和樣式只需要被請求一次。
  • 狀態可以維護在一個頁面上。JavaScript 變數和 DOM 狀態將得到保持,因為主容器頁面未被重新載入。
  • 基本上包括大部分 SPA 的優點。

缺點

  • 動態網頁很難收藏。
  • 如果 JavaScript 已在瀏覽器中被禁用,則不起作用。
  • 有些網路爬蟲不執行 JavaScript,也不會看到 JavaScript 載入的內容。
  • 基本上包括大部分 SPA 的缺點。

參考資料:

17. Ajax和Fetch區別

  • ajax是使用XMLHttpRequest物件發起的,但是用起來很麻煩,所以ES6新規範就有了fetch,fetch發一個請求不用像ajax那樣寫一大堆程式碼。
  • 使用fetch無法取消一個請求,這是因為fetch基於Promise,而Promise無法做到這一點。
  • 在預設情況下,fetch不會接受或者傳送cookies
  • fetch沒有辦法原生監測請求的進度,而XMLHttpRequest可以
  • fetch只對網路請求報錯,對400,500都當做成功的請求,需要封裝去處理
  • fetch由於是ES6規範,相容性上比不上XMLHttpRequest

18. 變數提升

var會使變數提升,這意味著變數可以在宣告之前使用。let和const不會使變數提升,提前使用會報錯。

變數提升(hoisting)是用於解釋程式碼中變數宣告行為的術語。使用var關鍵字宣告或初始化的變數,會將宣告語句“提升”到當前作用域的頂部。 但是,只有宣告才會觸發提升,賦值語句(如果有的話)將保持原樣。

19. 使用let、var和const建立變數有什麼區別

用 var 宣告的變數的作用域是它當前的執行上下文,它可以是巢狀的函式,也可以是宣告在任何函式外的變數。let 和 const 是塊級作用域,意味著它們只能在最近的一組花括號(function、if-else 程式碼塊或 for 迴圈中)中訪問。

var 宣告的全域性變數和函式都會成為 window 物件的屬性和方法。使用 let 和 const 的頂級宣告不會定義在全域性上下文中,但在作用域鏈解析上效果是一樣的。

function foo() {
  // 所有變數在函式中都可訪問
  var bar = 'bar';
  let baz = 'baz';
  const qux = 'qux';

  console.log(bar); // bar
  console.log(baz); // baz
  console.log(qux); // qux
}

console.log(bar); // ReferenceError: bar is not defined
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined
if (true) {
  var bar = 'bar';
  let baz = 'baz';
  const qux = 'qux';
}

// 用 var 宣告的變數在函式作用域上都可訪問
console.log(bar); // bar
// let 和 const 定義的變數在它們被定義的語句塊之外不可訪問
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined

var會使變數提升,這意味著變數可以在宣告之前使用。let和const不會使變數提升,提前使用會報錯。

console.log(foo); // undefined

var foo = 'foo';

console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization

let baz = 'baz';

console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization

const bar = 'bar';

用var重複宣告不會報錯,但let和const會。

var foo = 'foo';
var foo = 'bar';
console.log(foo); // "bar"

let baz = 'baz';
let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared

let和const的區別在於:let允許多次賦值,而const只允許一次。

// 這樣不會報錯。
let foo = 'foo';
foo = 'bar';

// 這樣會報錯。
const baz = 'baz';
baz = 'qux';

20. 物件淺拷貝和深拷貝有什麼區別

JS中,除了基本資料型別,還存在物件、陣列這種引用型別。 基本資料型別,拷貝是直接拷貝變數的值,而引用型別拷貝的其實是變數的地址。

let o1 = {a: 1}
let o2 = o1

在這種情況下,如果改變o1o2其中一個值的話,另一個也會變,因為它們都指向同一個地址。

o2.a = 3
console.log(o1.a) // 3

而淺拷貝和深拷貝就是在這個基礎之上做的區分,如果在拷貝這個物件的時候,只對基本資料型別進行了拷貝,而對引用資料型別只是進行了引用的傳遞,而沒有重新建立一個新的物件,則認為是淺拷貝。反之,在對引用資料型別進行拷貝的時候,建立了一個新的物件,並且複製其內的成員變數,則認為是深拷貝。

21. 怎麼實現物件深拷貝

這種方法有缺陷,詳情請看關於JSON.parse(JSON.stringify(obj))實現深拷貝應該注意的坑

let o1 = {a:{
    b:1
  }
}
let o2 = JSON.parse(JSON.stringify(o1))

基礎版

function deepCopy(target) {
    if (typeof target == 'object') {
        const result = Array.isArray(target)? [] : {}
        for (const key in target) {
            if (typeof target[key] == 'object') {
                result[key] = deepCopy(target[key])
            } else {
                result[key] = target[key]
            }
        }

        return result
    } else if (typeof target == 'function') {
        return eval('(' + test.toString() + ')')
    } else {
        return target
    }
}

完整版

const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'
const symbolTag = '[object Symbol]'

function deepCopy(origin, map = new WeakMap()) {
    if (!origin || !isObject(origin)) return origin
    if (typeof origin == 'function') {
        return eval('(' + origin.toString() + ')')
    }

    const objType = getObjType(origin)
    const result = createObj(origin, objType)

    // 防止迴圈引用,不會遍歷已經在 map 中的物件,因為在上一層正在遍歷
    if (map.get(origin)) {
        return map.get(origin)
    }

    map.set(origin, result)

    // set
    if (objType == setTag) {
        for (const value of origin) {
            result.add(deepCopy(value, map))
        }

        return result
    }

    // map
    if (objType == mapTag) {
        for (const [key, value] of origin) {
            result.set(key, deepCopy(value, map))
        }

        return result
    }

    // 物件或陣列
    if (objType == objectTag || objType == arrayTag) {
        for (const key in origin) {
            result[key] = deepCopy(origin[key], map)
        }

        return result
    }

    return result
}

function getObjType(obj) {
    return Object.prototype.toString.call(obj)
}

function createObj(obj, type) {
    if (type == objectTag) return {}
    if (type == arrayTag) return []
    if (type == symbolTag) return Object(Symbol.prototype.valueOf.call(obj))

    return new obj.constructor(obj)
}

function isObject(origin) {
    return typeof origin == 'object' || typeof origin == 'function'
}

如何寫出一個驚豔面試官的深拷貝?

22. 陣列去重

ES5

function unique(arry) {
    const temp = []
    arry.forEach(function(item) {
        if (temp.indexOf(item) == -1) {
            temp.push(item)
        }
    })

    return temp
}

ES6

function unique(arry) {
   return Array.from(new Set(arry))
}

23. 資料型別

  1. Undefined
  2. Null
  3. Boolean
  4. Number
  5. String
  6. Object
  7. symbol(ES6新增)

24. 內建函式(原生函式)

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
  • Symbol

原始值 "I am a string" 並不是一個物件,它只是一個字面量,並且是一個不可變的值。

如果要在這個字面量上執行一些操作,比如獲取長度、訪問其中某個字元等,那需要將其轉換為 String 物件。

幸好,在必要時語言會自動把字串字面量轉換成一個 String 物件,也就是說你並不需要顯式建立一個物件。

25. 如何判斷陣列與物件

Array.isArray([]) // true
Array.isArray({}) // false

typeof [] // "object"
typeof {} // "object"

Object.prototype == [].__proto__ // false
Object.prototype == {}.__proto__ // true
Array.prototype == [].__proto__ // true
Array.prototype == {}.__proto__ // false

26. 自動分號

有時 JavaScript 會自動為程式碼行補上缺失的分號,即自動分號插入(Automatic SemicolonInsertion,ASI)。

因為如果缺失了必要的 ; ,程式碼將無法執行,語言的容錯性也會降低。ASI 能讓我們忽略那些不必要的;

請注意,ASI 只在換行符處起作用,而不會在程式碼行的中間插入分號。

如果 JavaScript 解析器發現程式碼行可能因為缺失分號而導致錯誤,那麼它就會自動補上分號。並且,只有在程式碼行末尾與換行符之間除了空格和註釋之外沒有別的內容時,它才會這樣做。

27. 浮點數精度

www.css88.com/archives/73…

28. cookie、localStorage、sessionStorage區別

特性cookielocalStoragesessionStorage
由誰初始化 客戶端或伺服器,伺服器可以使用Set-Cookie請求頭。 客戶端 客戶端
資料的生命週期 一般由伺服器生成,可設定失效時間,如果在瀏覽器生成,預設是關閉瀏覽器之後失效 永久儲存,可清除 僅在當前會話有效,關閉頁面後清除
存放資料大小 4KB 5MB 5MB
與伺服器通訊 每次都會攜帶在HTTP頭中,如果使用cookie儲存過多資料會帶來效能問題 僅在客戶端儲存 僅在客戶端儲存
用途 一般由伺服器生成,用於標識使用者身份 用於瀏覽器快取資料 用於瀏覽器快取資料
訪問許可權 任意視窗 任意視窗 當前頁面視窗

29. 自執行函式?用於什麼場景?好處?

自執行函式:
  1. 宣告一個匿名函式
  2. 馬上呼叫這個匿名函式。

作用:建立一個獨立的作用域。

好處
  • 防止變數彌散到全域性,以免各種js庫衝突。
  • 隔離作用域避免汙染,或者截斷作用域鏈,避免閉包造成引用變數無法釋放。
  • 利用立即執行特性,返回需要的業務函式或物件,避免每次通過條件判斷來處理。
場景

一般用於框架、外掛等場景

30. 多個頁面之間如何進行通訊

有如下幾個方式:

  • cookie
  • web worker
  • localeStorage和sessionStorage

31. css動畫和js動畫的差異

  1. 程式碼複雜度,js 動畫程式碼相對複雜一些
  2. 動畫執行時,對動畫的控制程度上,js 能夠讓動畫,暫停,取消,終止,css動畫不能新增事件
  3. 動畫效能看,js 動畫多了一個js 解析的過程,效能不如 css 動畫好

32. new一個物件經歷了什麼

function Test(){}
const test = new Test()
  1. 建立一個新物件:
const obj = {}
  1. 設定新物件的constructor屬性為建構函式的名稱,設定新物件的proto屬性指向建構函式的prototype物件
obj.constructor = Test
obj.__proto__ = Test.prototype
複製程式碼
  1. 使用新物件呼叫函式,函式中的this被指向新例項物件
Test.call(obj)
  1. 將初始化完畢的新物件地址,儲存到等號左邊的變數中

33. bind、call、apply的區別

call和apply其實是一樣的,區別就在於傳參時引數是一個一個傳或者是以一個數組的方式來傳。
call和apply都是在呼叫時生效,改變呼叫者的this指向。

let name = 'Jack'
const obj = {name: 'Tom'}
function sayHi() {console.log('Hi! ' + this.name)}

sayHi() // Hi! Jack
sayHi.call(obj) // Hi! Tom

bind也是改變this指向,不過不是在呼叫時生效,而是返回一個新函式。

const newFunc = sayHi.bind(obj)
newFunc() // Hi! Tom

34. 實現 bind call apply 函式

bind

Function.prototype.bind = function(context, ...extra) {
    const self = this
    // 這裡不能用箭頭函式,防止繫結函式為建構函式
    return function(...arg) {
        return self.call(context, ...extra.concat(arg))
    }
}

call

Function.prototype.call = function(context, ...args) {
    if (context === null || context === undefined) {
        context = window
    } else if (!context || context.toString() != '[object Object]') {
        context = {}
    }

    let key = Math.random()
    while (context[key]) {
        key = Math.random()
    }

    context[key] = this
    const result = context[key](...args)
    delete context[key]
    return result
}

apply

Function.prototype.apply = function(context, args) {
    if (args !== undefined && !Array.isArray(args)) throw '引數必須為陣列'
    if (context === null || context === undefined) {
        context = window
    } else if (!context || context.toString() != '[object Object]') {
        context = {}
    }

    let key = Math.random()
    while (context[key]) {
        key = Math.random()
    }

    context[key] = this
    let result
    if (args === undefined) {
        const result = context[key]()
    } else {
        const result = context[key](...args)
    }

    delete context[key]
    return result
}

35. 請簡述JavaScript中的this

JS 中的this是一個相對複雜的概念,不是簡單幾句能解釋清楚的。粗略地講,函式的呼叫方式決定了this的值。我閱讀了網上很多關於this的文章,Arnav Aggrawal寫的比較清楚。this取值符合以下規則:

  1. 在呼叫函式時使用new關鍵字,函式內的this是一個全新的物件。
  2. 如果applycallbind方法用於呼叫、建立一個函式,函式內的 this 就是作為引數傳入這些方法的物件。
  3. 當函式作為物件裡的方法被呼叫時,函式內的this是呼叫該函式的物件。比如當obj.method()被呼叫時,函式內的 this 將繫結到obj物件。
  4. 如果呼叫函式不符合上述規則,那麼this的值指向全域性物件(global object)。瀏覽器環境下this的值指向window物件,但是在嚴格模式下('use strict'),this的值為undefined
  5. 如果符合上述多個規則,則較高的規則(1 號最高,4 號最低)將決定this的值。
  6. 如果該函式是 ES2015 中的箭頭函式,將忽略上面的所有規則,this被設定為它被建立時的上下文。

我平時一直有整理面試題的習慣,有隨時跳出舒適圈的準備,不知不覺整理了229頁了,在這裡分享給大家,有需要的點選這裡免費領取題目+解析PDF

篇幅有限,僅展示部分內容

如果你需要這份完整版的面試題+解析,【點選我】就可以了。

希望大家明年的金三銀四面試順利,拿下自己心儀的offer!