看看這些前端面試題,帶你搞定高頻知識點(八)

2023-03-06 22:00:27

本篇文章給大家整理總結一些JavaScript 面試題,帶你搞定高頻知識點,希望對大家有所幫助!

var、let、const

三者的區別

區別letconstvar
重複宣告不能重複宣告,會報SyntaxError錯const 定義常數,值不能修改的變數叫做常數,一定要賦初始值,因為不能修改。可以重複宣告
塊級作用域擁有擁有不擁有
會不會汙染全域性變數(掛載在window上)不會不會

說明
1.let和const也存在變數提升,只是提升的方式不同

  • var變數提升:變數的宣告提升到頂部,值為undefined
  • let、const變數提升: 變數宣告提升到頂部,只不過將該變數標記為尚未初始化
    let 和 const存在暫時性死區,程式碼執行過程中的一段時間內,在此期間無法使用識別符號,也不能參照外層作用域的變數。
let answer;
function fn(){
	//如果此時沒有將變數變數提升到這裡,answer應該取外層answer的值
	console.log(answer); //Uncaught ReferenceError: Cannot access 'answer' before initialization
	let answer=42;
}
登入後複製

2.var建立的全域性變數->全域性物件的屬性,let和const在全域性作用域宣告的變數->不是全域性物件的屬性

3.如果常數是個陣列或物件,對其內部元素修改,不算對常數的修改,不會報錯。常數指向了一個地址,地址不變就不會報錯。

變數提升和函數提升

  • 變數宣告升級
    通過var定義(宣告)的變數,在定義語句之前的就可以存取到
    但是值是undefined

  • 函數宣告提升
    通過function宣告的函數,在之前就可以直接呼叫。
    值是函數體

//變數提升先於函數提升,提升後被函數宣告function覆蓋,所以就算換了順序也是function
function a(){
}
var a ;
console.log(typeof a); //function


var f1 = function () {
    console.log(1);
}
function f1 () {
    console.log(2);
}
f1() ; //1
//變數提升後
var f1;//變數提升
function f1(){};//函數提升
f1 = function () {
    console.log(1);
}
f1() ;
登入後複製

作用域和作用域鏈

理解:一個程式碼段所在的區域,是靜態的,在編寫程式碼時就確定了
作用:變數繫結在這個作用域內有效,隔離變數,不同作用域下同名變數不會有衝突。
作用域分類

  • 全域性作用域

  • 函數作用域

  • 塊級作用域

作用域鏈:多個作用域巢狀,就近選擇,先在自己作用域找,然後去就近的作用域找。

函數的作用域在宣告的時候就已經決定了,與呼叫位置無關
所以執行aaa()的時候先在aaa的作用域裡面找,沒有找到a,再去父級作用域window裡面找,找到a=10

var a = 10;  
function aaa() {
    alert(a);
}
function bbb() {
    var a = 20;
    aaa();
}
bbb();
登入後複製

執行上下文

對當前JavaScript的執行環境的抽象,每當JavaScript開始執行的時候,它都在執行上下文中執行。

  • 全域性執行上下文:在執行全域性程式碼前將window確定為全域性執行上下文

對全域性資料進行預處理

  • var定義的全域性變數 --> undefined,新增為window的屬性

  • function宣告的全域性函數 --> 賦值(函數體),新增為window的方法

  • this --> 賦值window

  • 開始執行全域性程式碼

  • 函數執行上下文:在呼叫函數,準備執行函數體之前,建立對應的函數執行上下文物件

對區域性資料進行預處理

  • 形參變數 --> 賦值(實參)–> 新增到函數執行上下文的屬性
  • arguments(形參列表封裝成的偽陣列)–>賦值(實參列表),新增到函數執行上下文的屬性
  • var定義的區域性變數–>undefined,新增為函數執行上下文的屬性
  • function宣告的函數–>賦值(函數體),新增為函數執行上下文的方法
  • this–>賦值(呼叫函數的物件)
  • 開始執行函數體程式碼

執行上下文棧
1.在全域性程式碼執行前,JS引擎就會建立一個棧來儲存管理所有的執行上下文物件
2.在全域性執行上下文(window)確定後,將其新增到棧中(壓棧)
3.在函數執行上下文建立後,將其新增到棧中(壓棧)
4.在當前函數執行完成後,將棧頂的物件移除(出棧)
5.當所有的程式碼執行完後,棧中只剩下window

作用域執行上下文
定義了幾個函數 + 1 = 幾個作用域執行了幾個函數 + 1 = 幾個執行上下文
函數定義時就確定了,一直存在,不會再變化,是靜態的全域性執行上下文環境實在全域性作用域確定之後,js程式碼執行之前建立的
呼叫函數時建立,函數呼叫結束被釋放,是動態的

在這裡插入圖片描述

var foo = 1;
function bar () {
    console.log(foo);
    var foo = 10;
    console.log(foo);
}

bar();

//變數提升後
var foo = 1;
function bar () {
	var foo = undefined;
    console.log(foo); //undefined
    foo = 10;
    console.log(foo);//10
}

bar();
登入後複製

如何用ES5實現let和const

let :使用立即執行函數創造出一個塊級作用域

(function(){
  var a = 1;
  console.log('內部a:', a);
})();
登入後複製

const
1.使用立即執行函數創造出一個塊級作用域。
2.對於不可變性,可以利用Object.defineProperty 將變數掛載在物件上

var __const = function __const(data, value) {
	this.data = value // 把要定義的data掛載到某個物件,並賦值value
	Object.defineProperty(this,data, { // 利用Object.defineProperty的能力劫持當前物件,並修改其屬性描述符
		enumerable: false,
		configurable: false,
		get: function () {
			return value
		},
		set: function (data) {
		if (data !== value) { // 當要對當前屬性進行賦值時,則丟擲錯誤!
			throw new TypeError('Assignment to constant variable.')	
		} else {
			return value
		}
		}
		})
	}
	//然後和立即執行函數結合
	(function(){
	 	var obj = {}
		_const.call(obj,'a',10)
	})()
	//當執行完畢後,全域性上就不會有obj,也不會有obj.a這個變數,進而實現了塊級作用域的功能
登入後複製

程式碼輸出題

function a(){
  a.name ='aaa';
  return this.name;
}
var b = {
  a,
  name:'bbb',
  getName:function(){
    return this.name;
  }
}
var c =b.getName;
console.log(a()); //this指向window,window上沒有name,所以輸出undefined
console.log(b.a()); //b.a 是function,b呼叫a函數,所以this指向b,所以輸出'bbb'
console.log(b.getName);//通過b呼叫getName,所以getName指向b,所以輸出'bbb'
console.log(c());//c是function,this指向window,window上沒有name,所以輸出undefined
登入後複製

資料型別

  • JS資料型別有哪些
  • 介紹一下Symbol和Bigint
  • 如何判斷一個資料型別
  • Object.prototype.toString.call() 的缺點?
  • 各個方法的原理是什麼
  • typeof(NaN) typeof(Null)
  • 手寫 instanceof 方法
  • null==undefined 和 null===undefined
  • 隱式轉換規則 === 和 == 的區別
  • map和weakmap區別
  • map和object區別
  • for in、for of 區別,分別對物件和陣列使用問結果
  • 講一下陣列的遍歷方法,filter與map的使用場景,some,every的區別
  • map的操作原理
  • map和forEach的區別
  • 使用迭代器實現for-of
  • 手寫陣列去重
  • 手寫陣列扁平化
  • map和filter的區別
  • 陣列的常用方法
  • 用reduce實現map

ES6 class和 ES5類的區別

ES5 function類ES6 class
可以new
可以呼叫
必須new呼叫,不能直接執行
function存在變數提升class不存在變數提升
static靜態方法只能通過類呼叫,不會出現在範例上

this的指向

  • 一般函數中this的指向會在呼叫時向函數傳遞執行上下文物件中設定。
    • 以函數形式呼叫,指向window
    • 以方法形式呼叫,this指向呼叫的方法
    • 以建構函式的形式呼叫,this是新建立的物件
  • 箭頭函數:本身沒有this,它的this可以沿作用域鏈(定義時就確定了的)查詢

bind、call、apply的區別與實現

apply、call、bind 函數可以改變 this 的指向。

區別callapplybind
呼叫函數×
引數從第二個引數開始依次傳遞封裝成陣列傳遞從第二個引數開始依次傳遞

bind函數的特殊點
多次繫結,只指向第一次繫結的obj物件。
多次繫結,一次生效。
原因:返回函數,後續bind修改的是返回函數的this

call函數的實現

//從第二個引數開始依次傳入,所以接收時使用rest引數
Function.prototype.call=function(obj,...args){
	obj = obj || window;
	args = args ? args : [];
	//給obj新增一個獨一無二的屬性以免覆蓋原有屬性
    const key = Symbol()
	obj[key] = this;
	const res = obj[key](...args);
	delete obj[key];
	return res;	
}
登入後複製

apply函數的實現

Function.prototype.apply=function(obj,args){
	obj = obj || window;
	args = args ? args : [];
	//給obj新增一個獨一無二的屬性以免覆蓋原有屬性
    const key = Symbol()
	obj[key] = this;
	const res = obj[key](...args);
	delete obj[key];
	return res;	
}
登入後複製

bind函數的實現

Function.prototype.bind=function(obj,...args){
	obj = obj || window;
	args = args ? args : [];
	return (...args2) => {
		return this.apply(obj,[...args,...args2])
	}
}
登入後複製

一般函數和箭頭函數

箭頭函數的作用:確保函數內部的this和外部的this是一樣的

箭頭函數是普通函數的語法糖,書寫要更加簡潔

區別一般函數箭頭函數
this指向呼叫時確定定義時確定,沒有自己的this,沿著作用域鏈找父級的this
改變this指向call,apply,bind不能改變,靜態
arguments沒有,可以用rest引數代替
作為建構函式× 沒有prototype屬性
匿名函數可以匿名可以不匿名匿名函數

閉包

是什麼
閉包就是在函數中能夠讀取其他函數內部變數

本質就是上級作用域內變數的生命週期,因為被下級作用域內參照,而沒有被釋放。
正常情況下,程式碼執行完成之後,函數的執行上下文出棧被回收。但是如果當前函數執行上下文執行完成之後中的某個東西被執行上下文以外的東西佔用,則當前函數執行上下文就不會出棧釋放,也就是形成了不被銷燬的上下文,閉包。

function foo(){
	var a=2;
	function bar(){ //覆蓋foo()內部作用域的閉包
		console.log(a++);
	}
	return bar;
}
var bar = foo(); //foo執行建立一個執行上下文環境,由於bar參照了其內部變數,也就是bar持有foo本次執行上下文的參照,foo本次的執行上下文不會被銷魂
bar();//2
bar();//3
var fn = foo(); //foo執行建立一個新的執行上下文環境,fn持有了foo本次執行上下文的參照
fn();//2
登入後複製

有什麼用
1.可以讀取函數內部的變數
2.使函數的內部變數執行完後,仍然存活在棧記憶體中(延長了區域性變數的生命週期)。

JavaScript閉包就是在另一個作用域中儲存了一份它從上一級函數或者作用域得到的變數,而這些變數是不會隨上一級函數的執行完成而銷燬

常用場景:節流防抖
缺點是什麼
1.函數執行完後,函數內的區域性變數沒有釋放,佔用記憶體時間會變長
2.容易造成記憶體洩露
怎麼解決
及時釋放:讓內部函數成為垃圾物件(將閉包手動設定為null)–> 回收閉包

手寫節流和防抖函數

作用是:控制回撥函數觸發的頻率,進行效能優化
引數: 控制觸發頻率的回撥函數和時間wait
輸出: 到時間後,返回callback函數

節流:在函數被頻繁觸發時, 函數執行一次後,只有大於設定的執行週期後才會執行第二次。一個時間段,只觸發一次
語法:throttle(callback, wait)
常用場景:比如拖動、捲動和輸入框聯想

//使用形式,繫結時候throttle函數就會執行,所以this是window
window.addEventListener('scroll',throttle(()=>{},500))

/*
思路
需要記錄上一次觸發的時間,才可以和當前時間比較,是否超過了間隔時間
第一次必然立刻觸發
*/
function throttle(callback,wait){
	let pre = new Date();
	//這裡的this是window
	return function(...args){
		//這裡的this是繫結的DOM
		const now = new Date();
		if(now-pre>=wait){
			callback.apply(this,args);
			pre = now;
		}
	}
}


/*
使用setTimeout實現
第一次需要延遲delay後觸發
*/
function throttle(callback,delay){
	let timer = null;
	//這裡的this是window
	return function(...args){
		if(timer){//說明已經觸發了
			return;
		}
		timer = setTimeout(()=>{
			callback.apply(this,args);
			timer = null;
		},delay)
	}
}
登入後複製

函數防抖:指定時間間隔內只會執行一次任務。如果在等待的過程中再一次觸發了事件,計時器重新開始計時,直到達到時間後執行最後一次的回撥
語法:debounce(callback, wait)
常用場景: 登入、傳簡訊等按鈕避免使用者點選太快,以致於傳送了多次請求,需要防抖。

function debounce(callback,delay){
	let timer = null;
	//這裡的this是window
	return function(){
		if(timer){//說明已經觸發了
			clearTimeout(timer);
		}
		timer = setTimeout(()=>{
			callback.apply(this,arguments);
			timer = null;
		},delay)
	}
}

//立即執行
function debounce(func,delay) {
  let timeout;
  return function (...args) {
      if (timeout) clearTimeout(timeout);
      const callNow = !timeout;
      timeout = setTimeout(() => {
          timeout = null;
      }, delay)
      if (callNow) func.apply(this, args)
  }
}
登入後複製

原型和原型鏈

  • 原型和原型鏈
  • 繼承

JavaScript 執行緒機制與事件迴圈機制

內容

  • 程序和執行緒
  • 程序的通訊方式
  • 瀏覽器多程序架構
  • 如何實現瀏覽器多標籤之間的通訊
  • H5 Web Workers JS多執行緒執行
  • 瀏覽器的事件迴圈機制
  • Node的事件迴圈機制
  • node事件迴圈程式碼輸出題 用於理解
  • 程式碼輸出題

DOM渲染

內容

  • DOM的渲染過程
  • DOM渲染的時機與渲染程序的概述
    -瀏覽器的渲染流程
  • CSS、JS、DOM解析和渲染阻塞問題
  • JS載入阻塞DOM渲染問題,怎麼解決? - 非同步JS,JS三種非同步載入的方式
    • script 標籤中的 async 和 defer 屬性
    • DOMContentLoaded和Load
  • DOM渲染優化

重繪和迴流

內容

  • 什麼是重繪和迴流
    • 迴流和重繪觸發的時機
  • 優化方案
    • GPU加速,如何開啟GPU加速
    • JS優化減少重繪和迴流的觸發

setTimeout 、setInterval、requestAnimationFrame

內容

  • setTimeout(cb, 0)會立刻執行嗎?
  • settimeout定時的時間準確嗎? 為什麼不準確? 怎麼解決?
  • setTimeout和requestAnimation的區別
  • requestAnimationFrame講一下你的理解
  • setTimeout實際延遲時間
  • 用setTimeout實現setInterval,實現一個隨時停止的版本
  • setTimeout 和 setInterval區別
  • JS實現動畫的方式
  • requestAnimationFrame與requestIdleCallback分別是什麼?
  • requestAnimationFrame的執行時機?
  • requestanimationframe回撥函數中進行大量計算,會阻塞頁面的渲染嗎
  • 每隔一秒輸出一個數位

觀察者模式和釋出訂閱機制

觀察者是軟體設計模式中的一種,但釋出訂閱只是軟體架構中的一種訊息正規化

觀察者模式

觀察者模式定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。

被依賴的物件叫做subject,依賴的物件叫做觀察者Observer 。被依賴的物件首先需要提供一個新增觀察者方法,以供觀察者呼叫。所以還需要維護一個觀察者列表,自身發生變化後,依次通知觀察者。

//subject 被觀察者
class Subject {
  constructor() {
    this.observerList = [];
  }
  addObserver(observer) {
    this.observerList.push(observer);
  }
  removeObserver(observer) {
    const index = this.observerList.findIndex(o => o.name === observer.name);
    this.observerList.splice(index, 1);
  }
  notifyObservers(message) {
    const observers = this.observeList;
    observers.forEach(observer => observer.notified(message));
  }
}

//Observer 觀察者
class Observer {
  constructor(name, subject) {
    this.name = name;
    if (subject) {
      subject.addObserver(this);
    }
  }
  notified(message) {
    console.log(this.name, 'got message', message);
  }
}

//使用
const subject = new Subject();
const observerA = new Observer('observerA', subject);
const observerB = new Observer('observerB');
subject.addObserver(observerB);
subject.notifyObservers('Hello from subject');
subject.removeObserver(observerA);
subject.notifyObservers('Hello again');
登入後複製

釋出訂閱機制
釋出者和訂閱者不直接進行通訊,通過事件排程中心進行管理。釋出者將要釋出的訊息交由事件排程中心管理,訂閱者也是根據自己的情況,按需訂閱事件排程中心的訊息。

//事件排程中心
class PubSub {
	  constructor() {
        // 儲存格式: warTask: [], routeTask: []
        // {訂閱事件:[回撥1,回撥2...],訂閱事件2:[回撥1,回撥2..]}
        this.events = {}
   	  }
   	  // 訂閱方法 訂閱哪個型別type就把對應的回撥函數放入
      subscribe(type, cb) { 
       	 if (!this.events[type]) {
            this.events[type] = [];
        }
        this.events[type].push(cb);
     }
     // 釋出方法
    publish(type, ...args) {
        if (this.events[type]) {
            this.events[type].forEach(cb => cb(...args))
        }
    }
	// 取消訂閱方法 的某一個型別的某一個回撥
    unsubscribe(type, cb) {
        if (this.events[type]) {
            const cbIndex = this.events[type].findIndex(e=> e === cb)
            if (cbIndex != -1) {
                this.events[type].splice(cbIndex, 1);
            }
        }
        if (this.events[type].length === 0) {
            delete this.events[type];
        }
    }
}

//測試
let pubsub = new PubSub();
//訂閱
pubsub.subscribe('warTask', function (taskInfo){
    console.log("宗門殿釋出戰鬥任務,任務資訊:" + taskInfo);
})
pubsub.subscribe('routeTask', function (taskInfo) {
    console.log("宗門殿釋出日常任務,任務資訊:" + taskInfo);
});
pubsub.subscribe('allTask', function (taskInfo) {
    console.log("宗門殿釋出五星任務,任務資訊:" + taskInfo);
});
//釋出
pubsub.publish('warTask', "獵殺時刻");
pubsub.publish('allTask', "獵殺時刻");
pubsub.publish('routeTask', "種樹澆水");
pubsub.publish('allTask', "種樹澆水");
登入後複製

區別

型別描述特點
觀察者模式觀察者和被觀察者互相知道身份,目標直接將通知分發到觀察者身上高耦合
釋出訂閱機制釋出訂閱機制通過事件排程中心來協調,訂閱者和釋出者互相不知道身份低耦合

非同步程式設計解決方案 Generator生成器函數 async/await、Promise

筆記內容

  • Generator生成器函數
  • Generator生成器函數使用上的補充 瞭解
  • 基於Promise物件的簡單自動執行器
  • iterator迭代器
  • async/await是什麼? 使用場景是什麼?
  • await/async與generator函數的區別
  • await/async內部實現原理 Generator函數和自動執行器
  • async錯誤捕獲方式
  • promise概述
  • promise知識點 瞭解
  • promise.then、catch、finally的原理與實現
  • Promise.all/Promise.race/Promise.allSettled的原理和實現
  • 手寫題:請求五秒未完成則終止
  • promise實現並行的非同步任務排程器

擴充套件運運算元的原理和應用

淺拷貝和深拷貝

深淺拷貝只是針對參照資料型別
區分點: 複製之後的副本進行修改會不會影響到原來的

  • 淺拷貝:修改拷貝以後的資料會影響原資料。使得原資料不安全。(只拷貝一層)
  • 深拷貝:修改拷貝以後的資料不會影響原資料,拷貝的時候生成新資料。

淺拷貝

  • 擴充套件運運算元,適用於陣列/物件

  • Aarry.prototype.concat(拷貝物件1,拷貝物件2...) 陣列的合併方法,將多個陣列或物件拷貝進目標陣列,返回新陣列。

  • Object.assign(目標物件1,拷貝物件1,拷貝物件2.....)物件的合併方法,將拷貝物件拷貝進目標物件

深拷貝

方式一: JSON.parse(JSON.stringify())

  • JSON.stringify():將JavaScript物件轉換為JSON字串
  • JSON.parse():可以將JSON字串轉為一個物件。

問題1: 函數屬性會丟失,不能克隆方法
問題2: 迴圈參照會出錯

//迴圈參照:b中參照了c,c中又有b
obj = {
b:['a','f'],
c:{h:20}
}
obj.b.push(obj.c);
obj.c.j = obj.b;
b:['a','f',{h:20,j:[]}],
c:{h:20,j:['a','f',[]]}
function deepClone1(target) {
    //通過陣列建立JSON格式的字串
    let str = JSON.stringify(target);
    //將JSON格式的字串轉換為JS資料
    let data = JSON.parse(str);
    return data;
}
登入後複製

方式二:遞迴+map
遞迴:實現深拷貝,不丟失屬性
map:儲存已經拷貝過的物件,解決迴圈參照問題

//map存放已經拷貝過的物件,key為需要拷貝的物件,value為拷貝後的物件
function deepClone(target,map=new Map()){
	//1.判斷是否是參照型別
	if(typeof target === 'object' && target !==null ){
		if(map.has(target))return map.get(target); //說明已經拷貝過了
		let isArr = Array.isArray(target);
		let res = isArr?[]:{};
		map.set(target,res)
		if(isArr){//拷貝的是陣列
			target.forEach((item,index) => {
				res[index] = deepClone(item,map);
			});	
		}else{//拷貝的是物件
			Object.keys(target).forEach(key=>{
				res[key]=deepClone(target[key],map);
			})
		} 
		return res; //返回的是一個陣列或物件
	}else{
		return target;
	}
}


//測試
console.log(deepClone([1,[1,2,[3,4]]]))
登入後複製

commonJS和ES6模組化

內容

  • commonJs實現原理
  • 模組執行的原理
  • require模組載入原理
  • ES6 module的特性
  • import() 函數 動態引入
  • commonJs和es module的區別

【推薦學習:】

以上就是看看這些前端面試題,帶你搞定高頻知識點(八)的詳細內容,更多請關注TW511.COM其它相關文章!