20道JavaScript經典面試題

南玖發表於2022-02-15

該篇文章整理了一些前端經典面試題,附帶詳解,涉及到JavaScript多方面知識點,滿滿都是乾貨~建議收藏閱讀

前言

如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新的文章~

1.說一說JavaScript的資料型別以及儲存方式

JavaScript一共有8種資料型別

其中有7種基本資料型別:

ES5的5種:NullundefinedBooleanNumberString

ES6新增:Symbol 表示獨一無二的值

ES10新增:BigInt 表示任意大的整數

一種引用資料型別:

Object(本質上是由一組無序的鍵值對組成)

包含function,Array,Date等。JavaScript不支援建立任何自定義型別的資料,也就是說JavaScript中所有值的型別都是上面8中之一。

儲存方式

  • 基本資料型別:直接儲存在記憶體中,佔據空間小,大小固定,屬於被頻繁使用的資料。
  • 引用資料型別:同時儲存在記憶體與記憶體中,佔據空間大,大小不固定。引用資料型別將指標存在中,將值存在中。當我們把物件值賦值給另外一個變數時,複製的是物件的指標,指向同一塊記憶體地址。

null 與 undefined的異同

相同點:

  • Undefined 和 Null 都是基本資料型別,這兩個基本資料型別分別都只有一個值,就是 undefined 和 null

不同點:

  • undefined 代表的含義是未定義, null 代表的含義是空物件。

  • typeof null 返回'object',typeof undefined 返回'undefined'

  • null == undefined  // true
    null === undefined // false
    
  • 其實 null 不是物件,雖然 typeof null 會輸出 object,但是這只是 JS 存在的一個悠久 Bug。在 JS 的最初版本中使用的是 32 位系統,為了效能考慮使用低位儲存變數的型別資訊,000 開頭代表是物件,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部型別判斷程式碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。

2.說說JavaScript中判斷資料型別的幾種方法

typeof

  • typeof一般用來判斷基本資料型別,除了判斷null會輸出"object",其它都是正確的
  • typeof判斷引用資料型別時,除了判斷函式會輸出"function",其它都是輸出"object"
console.log(typeof 6);               // 'number'
console.log(typeof true);            // 'boolean'
console.log(typeof 'nanjiu');        // 'string'
console.log(typeof []);              // 'object'     []陣列的資料型別在 typeof 中被解釋為 object
console.log(typeof function(){});    // 'function'
console.log(typeof {});              // 'object'
console.log(typeof undefined);       // 'undefined'
console.log(typeof null);            // 'object'     null 的資料型別被 typeof 解釋為 object

對於引用資料型別的判斷,使用typeof並不準確,所以可以使用instanceof來判斷引用資料型別

instanceof

Instanceof 可以準確的判斷引用資料型別,它的原理是檢測建構函式的prototype屬性是否在某個例項物件的原型鏈上

原型知識點具體可以看我之前的文章:你一定要懂的JavaScript之原型與原型鏈

語法:

object instanceof constructor


console.log(6 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('nanjiu' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true    

constructor(建構函式)

當一個函式被定義時,JS引擎會為函式新增prototype屬性,然後在prototype屬性上新增一個constructor屬性,並讓其指向該函式。

constructor1.png
當執行 let f = new F()時,F被當成了建構函式,f是F的例項物件,此時F原型上的constructor屬性傳遞到了f上,所以f.constructor===F

function F(){}
let f = new F()

f.constructor === F // true
new Number(1).constructor === Number //true
new Function().constructor === Function // true
true.constructor === Boolean //true
''.constructor === String // true
new Date().constructor === Date // true
[].constructor === Array

⚠️注意:

  • null和undefined是無效的物件,所以他們不會有constructor屬性
  • 函式的construct是不穩定的,主要是因為開發者可以重寫prototype,原有的construction引用會丟失,constructor會預設為Object
function F(){}
F.prototype = {}

let f = new F()
f.constructor === F // false

console.log(f.constructor) //function Object(){..}

為什麼會變成Object?

因為prototype被重新賦值的是一個{}{}new Object()的字面量,因此 new Object() 會將 Object 原型上的 constructor 傳遞給 { },也就是 Object 本身。

因此,為了規範開發,在重寫物件原型時一般都需要重新給 constructor 賦值,以保證物件例項的型別不被篡改。

Object.prototype.toString.call()

toString() 是 Object 的原型方法,呼叫該方法,預設返回當前物件的 [[Class]] 。這是一個內部屬性,其格式為 [object Xxx] ,其中 Xxx 就是物件的型別。

對於 Object 物件,直接呼叫 toString() 就能返回 [object Object] 。而對於其他物件,則需要通過 call / apply 來呼叫才能返回正確的型別資訊。

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全域性物件 global 的引用

3.js資料型別轉換

在JavaScript中型別轉換有三種情況:

  • 轉換為數字(呼叫Number(),parseInt(),parseFloat()方法)
  • 轉換為字串(呼叫.toString()或String()方法)
  • 轉換為布林值(呼叫Boolean()方法)

null、undefined沒有.toString方法

轉換為數字

  • Number():可以把任意值轉換成數字,如果要轉換的字串中有不是數字的值,則會返回NaN
Number('1')   // 1
Number(true)  // 1
Number('123s') // NaN
Number({})  //NaN
  • parseInt(string,radix):解析一個字串並返回指定基數的十進位制整數,radix是2-36之間的整數,表示被解析字串的基數。
parseInt('2') //2
parseInt('2',10) // 2
parseInt('2',2)  // NaN
parseInt('a123')  // NaN  如果第一個字元不是數字或者符號就返回NaN
parseInt('123a')  // 123
  • parseFloat(string):解析一個引數並返回一個浮點數
parseFloat('123a')
//123
parseFloat('123a.01')
//123
parseFloat('123.01')
//123.01
parseFloat('123.01.1')
//123.01
  • 隱式轉換
let str = '123'
let res = str - 1 //122

str+1 // '1231'
+str+1 // 124

轉換為字串

  • .toString() ⚠️注意:null,undefined不能呼叫
Number(123).toString()
//'123'
[].toString()
//''
true.toString()
//'true'
  • String() 都能轉
String(123)
//'123'
String(true)
//'true'
String([])
//''
String(null)
//'null'
String(undefined)
//'undefined'
String({})
//'[object Object]'
  • 隱式轉換:當+兩邊有一個是字串,另一個是其它型別時,會先把其它型別轉換為字串再進行字串拼接,返回字串
let a = 1
a+'' // '1'

轉換為布林值

0, ''(空字串), null, undefined, NaN會轉成false,其它都是true

  • Boolean()
Boolean('') //false
Boolean(0) //false
Boolean(1) //true
Boolean(null) //false
Boolean(undefined) //false
Boolean(NaN) //false
Boolean({}) //true
Boolean([]) //true
  • 條件語句
let a
if(a) {
  //...   //這裡a為undefined,會轉為false,所以該條件語句內部不會執行
}
  • 隱式轉換 !!
let str = '111'
console.log(!!str) // true

4.{}和[]的valueOf和toString的返回結果?

  • valueOf:返回指定物件的原始值
物件 返回值
Array 返回陣列物件本身。
Boolean 布林值。
Date 儲存的時間是從 1970 年 1 月 1 日午夜開始計的毫秒數 UTC。
Function 函式本身。
Number 數字值。
Object 物件本身。這是預設情況。
String 字串值。
Math 和 Error 物件沒有 valueOf 方法。
  • toString:返回一個表示物件的字串。預設情況下,toString() 方法被每個 Object 物件繼承。如果此方法在自定義物件中未被覆蓋,toString() 返回 "[object type]",其中 type 是物件的型別。
({}).valueOf()   //{}
({}).toString()  //'[object Object]'
[].valueOf()    //[]
[].toString()   //''

5.let,const,var的區別?

  • 變數提升:let,const定義的變數不會出現變數提升,而var會
  • 塊級作用域:let,const 是塊作用域,即其在整個大括號 {} 之內可見,var:只有全域性作用域和函式作用域概念,沒有塊級作用域的概念。
  • 重複宣告:同一作用域下let,const宣告的變數不允許重複宣告,而var可以
  • 暫時性死區:let,const宣告的變數不能在宣告之前使用,而var可以
  • const 宣告的是一個只讀的常量,不允許修改

6.JavaScript作用域與作用域鏈

作用域:

簡單來說,作用域是指程式中定義變數的區域,它決定了當前執行程式碼對變數的訪問許可權

作用域鏈:

當可執行程式碼內部訪問變數時,會先查詢當前作用域下有無該變數,有則立即返回,沒有的話則會去父級作用域中查詢...一直找到全域性作用域。我們把這種作用域的巢狀機制稱為作用域鏈

詳細知識可以看我之前的文章:JavaScript深入之作用域與閉包

7.如何正確的判斷this指向?

this的繫結規則有四種:預設繫結,隱式繫結,顯式繫結,new繫結.

  1. 函式是否在 new 中呼叫(new繫結),如果是,那麼 this 繫結的是new中新建立的物件。
  2. 函式是否通過 call,apply 呼叫,或者使用了 bind (即硬繫結),如果是,那麼this繫結的就是指定的物件。
  3. 函式是否在某個上下文物件中呼叫(隱式繫結),如果是的話,this 繫結的是那個上下文物件。一般是 obj.foo()
  4. 如果以上都不是,那麼使用預設繫結。如果在嚴格模式下,則繫結到 undefined,否則繫結到全域性物件。
  5. 如果把 null 或者 undefined 作為 this 的繫結物件傳入 call、apply 或者 bind, 這些值在呼叫時會被忽略,實際應用的是預設繫結規則。
  6. 箭頭函式沒有自己的 this, 它的this繼承於上一層程式碼塊的this。

詳細知識可以看我之前的文章:this指向與call,apply,bind

8.for...of,for..in,forEach,map的區別?

for...of(不能遍歷物件)

在可迭代物件(具有 iterator 介面)(Array,Map,Set,String,arguments)上建立一個迭代迴圈,呼叫自定義迭代鉤子,併為每個不同屬性的值執行語句,不能遍歷物件

let arr=["前端","南玖","ssss"];
    for (let item of arr){
        console.log(item)
    }
//前端 南玖 ssss

//遍歷物件
let person={name:"南玖",age:18,city:"上海"}
for (let item of person){
  console.log(item)
}
// 我們發現它是不可以的 我們可以搭配Object.keys使用
for(let item of Object.keys(person)){
    console.log(person[item])
}
// 南玖 18 上海

for...in

for...in迴圈:遍歷物件自身的和繼承的可列舉的屬性, 不能直接獲取屬性值。可以中斷迴圈。

let person={name:"南玖",age:18,city:"上海"}
   let text=""
   for (let i in person){
      text+=person[i]
   }

   // 輸出:南玖18上海

//其次在嘗試一些陣列
   let arry=[1,2,3,4,5]
   for (let i in arry){
        console.log(arry[i])
    }
//1 2 3 4 5

forEach

forEach: 只能遍歷陣列,不能中斷,沒有返回值(或認為返回值是undefined)。

let arr=[1,2,3];
const res = arr.forEach(item=>{
  console.log(item*3)
})
// 3 6 9
console.log(res) //undefined
console.log(arr) // [1,2,3]

map

map: 只能遍歷陣列,不能中斷,返回值是修改後的陣列。

let arr=[1,2,3];
const res = arr.map(item=>{
  return res+1
})
console.log(res) //[2,3,4]
console.log(arr) // [1,2,3]

總結

  • forEach 遍歷列表值,不能使用 break 語句或使用 return 語句
  • for in 遍歷物件鍵值(key),或者陣列下標,不推薦迴圈一個陣列
  • for of 遍歷列表值,允許遍歷 Arrays(陣列), Strings(字串), Maps(對映), Sets(集合)等可迭代的資料結構等.在 ES6 中引入的 for of 迴圈,以替代 for in 和 forEach() ,並支援新的迭代協議。
  • for in迴圈出的是key,for of迴圈出的是value;
  • for of是ES6新引入的特性。修復了ES5的for in的不足;
  • for of不能迴圈普通的物件,需要通過和Object.keys()搭配使用。

9.說說你對原型鏈的理解?

每個函式(類)天生自帶一個屬性prototype,屬性值是一個物件,裡面儲存了當前類供例項使用的屬性和方法 「(顯示原型)」

在瀏覽器預設給原型開闢的堆記憶體中有一個constructor屬性:儲存的是當前類本身(⚠️注意:自己開闢的堆記憶體中預設沒有constructor屬性,需要自己手動新增)「(建構函式)」

每個物件都有一個__proto__屬性,這個屬性指向當前例項所屬類的原型(不確定所屬類,都指向Object.prototype「(隱式原型)」

當你試圖獲取一個物件的某個屬性時,如果這個物件本身沒有這個屬性,那麼它會去它的隱式原型__proto__(也就是它的建構函式的顯示原型prototype)中查詢。「(原型鏈)」

詳細知識可以看我之前的文章:你一定要懂的JavaScript之原型與原型鏈

10.說一說三種事件模型?

事件模型

DOM0級模型: ,這種模型不會傳播,所以沒有事件流的概念,但是現在有的瀏覽器支援以冒泡的方式實現,它可以在網頁中直接定義監聽函式,也可以通過 js屬性來指定監聽函式。這種方式是所有瀏覽器都相容的。

IE 事件模型: 在該事件模型中,一次事件共有兩個過程,事件處理階段,和事件冒泡階段。事件處理階段會首先執行目標元素繫結的監聽事件。然後是事件冒泡階段,冒泡指的是事件從目標元素冒泡到 document,依次檢查經過的節點是否繫結了事件監聽函式,如果有則執行。這種模型通過 attachEvent 來新增監聽函式,可以新增多個監聽函式,會按順序依次執行。

DOM2 級事件模型: 在該事件模型中,一次事件共有三個過程,第一個過程是事件捕獲階段。捕獲指的是事件從 document 一直向下傳播到目標元素,依次檢查經過的節點是否繫結了事件監聽函式,如果有則執行。後面兩個階段和 IE 事件模型的兩個階段相同。這種事件模型,事件繫結的函式是 addEventListener,其中第三個引數可以指定事件是否在捕獲階段執行。

事件委託

事件委託指的是把一個元素的事件委託到另外一個元素上。一般來講,會把一個或者一組元素的事件委託到它的父層或者更外層元素上,真正繫結事件的是外層元素,當事件響應到需要繫結的元素上時,會通過事件冒泡機制從而觸發它的外層元素的繫結事件上,然後在外層元素上去執行函式。

事件傳播(三個階段)

  1. 捕獲階段–事件從 window 開始,然後向下到每個元素,直到到達目標元素事件或event.target。
  2. 目標階段–事件已達到目標元素。
  3. 冒泡階段–事件從目標元素冒泡,然後上升到每個元素,直到到達 window。

事件捕獲

當事件發生在 DOM 元素上時,該事件並不完全發生在那個元素上。在捕獲階段,事件從window開始,一直到觸發事件的元素。window----> document----> html----> body ---->目標元素

事件冒泡

事件冒泡剛好與事件捕獲相反,當前元素---->body ----> html---->document ---->window。當事件發生在DOM元素上時,該事件並不完全發生在那個元素上。在冒泡階段,事件冒泡,或者事件發生在它的父代,祖父母,祖父母的父代,直到到達window為止。
事件傳播.jpeg

如何阻止事件冒泡

w3c的方法是e.stopPropagation(),IE則是使用e.cancelBubble = true。例如:

window.event?window.event.cancelBubble = true : e.stopPropagation();

return false也可以阻止冒泡。

11.JS延遲載入的方式

JavaScript會阻塞DOM的解析,因此也就會阻塞DOM的載入。所以有時候我們希望延遲JS的載入來提高頁面的載入速度。

  • 把JS放在頁面的最底部
  • script標籤的defer屬性:指令碼會立即下載但延遲到整個頁面載入完畢再執行。該屬性對於內聯指令碼無作用 (即沒有 「src」 屬性的指令碼)。
  • Async是在外部JS載入完成後,瀏覽器空閒時,Load事件觸發前執行,標記為async的指令碼並不保證按照指定他們的先後順序執行,該屬性對於內聯指令碼無作用 (即沒有 「src」 屬性的指令碼)。
  • 動態建立script標籤,監聽dom載入完畢再引入js檔案

12.說說什麼是模組化開發?

模組化的開發方式可以提高程式碼複用率,方便進行程式碼的管理。通常一個檔案就是一個模組,有自己的作用域,只向外暴露特定的變數和函式。

幾種模組化方案

  • 第一種是 CommonJS 方案,它通過 require 來引入模組,通過 module.exports 定義模組的輸出介面。

  • 第二種是 AMD 方案,這種方案採用非同步載入的方式來載入模組,模組的載入不影響後面語句的執行,所有依賴這個模組的語句都定義在一個回撥函式裡,等到載入完成後再執行回撥函式。require.js 實現了 AMD 規範。

  • 第三種是 CMD 方案,這種方案和 AMD 方案都是為了解決非同步模組載入的問題,sea.js 實現了 CMD 規範。它和require.js的區別在於模組定義時對依賴的處理不同和對依賴模組的執行時機的處理不同。

  • 第四種方案是 ES6 提出的方案,使用 importexport 的形式來匯入匯出模組。

CommonJS

Node.jscommonJS規範的主要踐行者。這種模組載入方案是伺服器端的解決方案,它是以同步的方式來引入模組的,因為在服務端檔案都儲存在本地磁碟,所以讀取非常快,所以以同步的方式載入沒有問題。但如果是在瀏覽器端,由於模組的載入是使用網路請求,因此使用非同步載入的方式更加合適。

// 定義模組a.js
var title = '前端';
function say(name, age) {
  console.log(`我是${name},今年${age}歲,歡迎關注我~`);
}
module.exports = { //在這裡寫上需要向外暴露的函式、變數
  say: say,
  title: title
}

// 引用自定義的模組時,引數包含路徑,可省略.js
var a = require('./a');
a.say('南玖', 18); //我是南玖,今年18歲,歡迎關注我~

AMD與require.js

AMD規範採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。這裡介紹用require.js實現AMD規範的模組化:用require.config()指定引用路徑等,用define()定義模組,用require()載入模組。

CMD與sea.js

CMD是另一種js模組化方案,它與AMD很類似,不同點在於:AMD 推崇依賴前置、提前執行,CMD推崇依賴就近、延遲執行。此規範其實是在sea.js推廣過程中產生的。

ES6 Module

ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,旨在成為瀏覽器和伺服器通用的模組解決方案。其模組功能主要由兩個命令構成:exportimportexport命令用於規定模組的對外介面,import命令用於輸入其他模組提供的功能。

// 定義模組a.js
var title = '前端';
function say(name, age) {
  console.log(`我是${name},今年${age}歲,歡迎關注我~`);
}
export { //在這裡寫上需要向外暴露的函式、變數
  say,
  title
}

// 引用自定義的模組時,引數包含路徑,可省略.js
import {say,title} from "./a"
say('南玖', 18); //我是南玖,今年18歲,歡迎關注我~

CommonJS 與 ES6 Module 的差異

CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。

  • CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。
  • ES6 模組的執行機制與 CommonJS 不一樣。JS 引擎對指令碼靜態分析的時候,遇到模組載入命令import,就會生成一個只讀引用。等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。換句話說,ES6 的import有點像 Unix 系統的“符號連線”,原始值變了,import載入的值也會跟著變。因此,ES6 模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。

CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。

  • 執行時載入: CommonJS 模組就是物件;即在輸入時是先載入整個模組,生成一個物件,然後再從這個物件上面讀取方法,這種載入稱為“執行時載入”。
  • 編譯時載入: ES6 模組不是物件,而是通過 export 命令顯式指定輸出的程式碼,import時採用靜態命令的形式。即在import時可以指定載入某個輸出值,而不是載入整個模組,這種載入稱為“編譯時載入”。

CommonJS 載入的是一個物件(即module.exports屬性),該物件只有在指令碼執行完才會生成。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。

推薦閱讀前端模組化理解

13.說說JS的執行機制

推薦閱讀探索JavaScript執行機制

14.如何在JavaScript中比較兩個物件?

對於兩個非基本型別的資料,我們用=====都指示檢查他們的引用是否相等,並不會檢查實際引用指向的值是否相等。

例如,預設情況下,陣列將被強制轉換成字串,並使用逗號連線所有元素

let a = [1,2,3]
let b = [1,2,3]
let c = "1,2,3"
a == b // false
a == c // true
b == c // true

一般比較兩個物件會採用遞迴來比較

15.說說你對閉包的理解,以及它的原理和應用場景?

一個函式和對其周圍(詞法環境)的引用捆綁在一起(或者說函式被引用包圍),這樣一個組合就是閉包(「closure」

閉包原理

函式執行分成兩個階段(預編譯階段和執行階段)。

  • 在預編譯階段,如果發現內部函式使用了外部函式的變數,則會在記憶體中建立一個“閉包”物件並儲存對應變數值,如果已存在“閉包”,則只需要增加對應屬性值即可。
  • 執行完後,函式執行上下文會被銷燬,函式對“閉包”物件的引用也會被銷燬,但其內部函式還持用該“閉包”的引用,所以內部函式可以繼續使用“外部函式”中的變數

利用了函式作用域鏈的特性,一個函式內部定義的函式會將包含外部函式的活動物件新增到它的作用域鏈中,函式執行完畢,其執行作用域鏈銷燬,但因內部函式的作用域鏈仍然在引用這個活動物件,所以其活動物件不會被銷燬,直到內部函式被燒燬後才被銷燬。

優點

  1. 可以從內部函式訪問外部函式的作用域中的變數,且訪問到的變數長期駐紮在記憶體中,可供之後使用
  2. 避免變數汙染全域性
  3. 把變數存到獨立的作用域,作為私有成員存在

缺點

  1. 對記憶體消耗有負面影響。因內部函式儲存了對外部變數的引用,導致無法被垃圾回收,增大記憶體使用量,所以使用不當會導致記憶體洩漏
  2. 對處理速度具有負面影響。閉包的層級決定了引用的外部變數在查詢時經過的作用域鏈長度
  3. 可能獲取到意外的值(captured value)

應用場景

  • 模組封裝,防止變數汙染全域性
var Person = (function(){
  	var name = '南玖'
    function Person() {
      console.log('work for qtt')
    }
  Person.prototype.work = function() {}
   return Person
})()
  • 迴圈體中建立閉包,儲存變數
for(var i=0;i<5;i++){
  (function(j){
    	setTimeOut(() => {
        console.log(j)
      },1000)
  })(i)
}

推薦閱讀:JavaScript深入之作用域與閉包

16.Object.is()與比較操作符=====的區別?

  • ==會先進行型別轉換再比較
  • ===比較時不會進行型別轉換,型別不同則直接返回false
  • Object.is()===基礎上特別處理了NaN,-0,+0,保證-0與+0不相等,但NaN與NaN相等

==操作符的強制型別轉換規則

  • 字串和數字之間的相等比較,將字串轉換為數字之後再進行比較。
  • 其他型別和布林型別之間的相等比較,先將布林值轉換為數字後,再應用其他規則進行比較。
  • null 和 undefined 之間的相等比較,結果為真。其他值和它們進行比較都返回假值。
  • 物件和非物件之間的相等比較,物件先呼叫 ToPrimitive 抽象操作後,再進行比較。
  • 如果一個操作值為 NaN ,則相等比較返回 false( NaN 本身也不等於 NaN )。
  • 如果兩個操作值都是物件,則比較它們是不是指向同一個物件。如果兩個運算元都指向同一個物件,則相等操作符返回true,否則,返回 false。
'1' == 1 // true
'1' === 1 // false
NaN == NaN //false
+0 == -0 //true
+0 === -0 // true
Object.is(+0,-0) //false
Object.is(NaN,NaN) //true

17.call與apply、bind的區別?

實際上call與apply的功能是相同的,只是兩者的傳參方式不一樣,而bind傳參方式與call相同,但它不會立即執行,而是返回這個改變了this指向的函式。

推薦閱讀:this指向與call,apply,bind

18.說說你瞭解哪些前端本地儲存?

推薦閱讀:這一次帶你徹底瞭解前端本地儲存

19.說說JavaScript陣列常用方法

向陣列新增元素的方法:
  1. push:向陣列的末尾追加 返回值是新增資料後陣列的新長度,改變原有陣列
  2. unshift:向陣列的開頭新增 返回值是新增資料後陣列的新長度,改變原有陣列
  3. splice:向陣列的指定index處插入 返回的是被刪除掉的元素的集合,會改變原有陣列
向陣列刪除元素的方法:
  1. pop():從尾部刪除一個元素 返回被刪除掉的元素,改變原有陣列
  2. shift():從頭部刪除一個元素 返回被刪除掉的元素,改變原有陣列
  3. splice:在index處刪除howmany個元素 返回的是被刪除掉的元素的集合,會改變原有陣列
陣列排序的方法:
  1. reverse():反轉,倒置 改變原有陣列
  2. sort():按指定規則排序 改變原有陣列
陣列迭代方法

引數: 每一項上執行的函式, 執行該函式的作用域物件(可選)

every()

對陣列中的每一執行給定的函式,如果該函式對每一項都返回true,則該函式返回true

var arr = [10,30,25,64,18,3,9]
var result = arr.every((item,index,arr)=>{
      return item>3
})
console.log(result)  //false

some()
對陣列中的每一執行給定的函式,如果該函式有一項返回true,就返回true,所有項返回false才返回false

var arr2 = [10,20,32,45,36,94,75]
var result2 = arr2.some((item,index,arr)=>{
    return item<10
})
console.log(result2)  //false

filter()

對陣列中的每一執行給定的函式,會返回滿足該函式的項組成的陣列

// filter  返回滿足要求的陣列項組成的新陣列
var arr3 = [3,6,7,12,20,64,35]
var result3 = arr3.filter((item,index,arr)=>{
    return item > 3
})
console.log(result3)  //[6,7,12,20,64,35]

map()

對陣列中的每一元素執行給定的函式,返回每次函式呼叫的結果組成的陣列

// map  返回每次函式呼叫的結果組成的陣列
var arr4 = [1,2,3,4,5,6]
var result4 = arr4.map((item,index,arr)=>{
    return `<span>${item}</span>`
})
console.log(result4)  
/*[ '<span>1</span>',
  '<span>2</span>',
  '<span>3</span>',
  '<span>4</span>',
  '<span>5</span>',
  '<span>6</span>' ]*/

forEach()

對陣列中的每一元素執行給定的函式,沒有返回值,常用來遍歷元素

// forEach 

var arr5 = [10,20,30]
var result5 = arr5.forEach((item,index,arr)=>{
    console.log(item)
})
console.log(result5)
/*
10
20
30
undefined   該方法沒有返回值
*/

reduce()

reduce()方法對陣列中的每個元素執行一個由你提供的reducer函式(升序執行),將其結果彙總為單個返回值

const array = [1,2,3,4]
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));

20.JavaScript為什麼要進行變數提升,它導致了什麼問題?

變數提升的表現是,在變數或函式宣告之前訪問變數或呼叫函式而不會報錯。

原因

JavaScript引擎在程式碼執行前有一個解析的過程(預編譯),建立執行上線文,初始化一些程式碼執行時需要用到的物件。

當訪問一個變數時,會到當前執行上下文中的作用域鏈中去查詢,而作用域鏈的首端指向的是當前執行上下文的變數物件,這個變數物件是執行上下文的一個屬性,它包含了函式的形參、所有的函式和變數宣告,這個物件的是在程式碼解析的時候建立的。

首先要知道,JS在拿到一個變數或者一個函式的時候,會有兩步操作,即解析和執行。

  • 在解析階段

    JS會檢查語法,並對函式進行預編譯。解析的時候會先建立一個全域性執行上下文環境,先把程式碼中即將執行的變數、函式宣告都拿出來,變數先賦值為undefined,函式先宣告好可使用。在一個函式執行之前,也會建立一個函式執行上下文環境,跟全域性執行上下文類似,不過函式執行上下文會多出this、arguments和函式的引數。

    • 全域性上下文:變數定義,函式宣告
    • 函式上下文:變數定義,函式宣告,this,arguments
  • 在執行階段,就是按照程式碼的順序依次執行。

那為什麼會進行變數提升呢?主要有以下兩個原因:

  • 提高效能
  • 容錯性更好

(1)提高效能 在JS程式碼執行之前,會進行語法檢查和預編譯,並且這一操作只進行一次。這麼做就是為了提高效能,如果沒有這一步,那麼每次執行程式碼前都必須重新解析一遍該變數(函式),而這是沒有必要的,因為變數(函式)的程式碼並不會改變,解析一遍就夠了。

在解析的過程中,還會為函式生成預編譯程式碼。在預編譯時,會統計宣告瞭哪些變數、建立了哪些函式,並對函式的程式碼進行壓縮,去除註釋、不必要的空白等。這樣做的好處就是每次執行函式時都可以直接為該函式分配棧空間(不需要再解析一遍去獲取程式碼中宣告瞭哪些變數,建立了哪些函式),並且因為程式碼壓縮的原因,程式碼執行也更快了。

(2)容錯性更好 變數提升可以在一定程度上提高JS的容錯性,看下面的程式碼:

a = 1
var a
console.log(a) //1

如果沒有變數提升,這段程式碼就會報錯

導致的問題

var tmp = new Date();

function fn(){
	console.log(tmp);
	if(false){
		var tmp = 'hello nanjiu';
	}
}
fn();  // undefined

在這個函式中,原本是要列印出外層的tmp變數,但是因為變數提升的問題,內層定義的tmp被提到函式內部的最頂部,相當於覆蓋了外層的tmp,所以列印結果為undefined。

var tmp = 'hello nanjiu';

for (var i = 0; i < tmp.length; i++) {
	console.log(tmp[i]);
}
console.log(i); // 13

由於遍歷時定義的i會變數提升成為一個全域性變數,在函式結束之後不會被銷燬,所以列印出來13。

總結

  • 解析和預編譯過程中的宣告提升可以提高效能,讓函式可以在執行時預先為變數分配棧空間
  • 宣告提升還可以提高JS程式碼的容錯性,使一些不規範的程式碼也可以正常執行
  • 函式是一等公民,當函式宣告與變數宣告衝突時,變數提升時函式優先順序更高,會忽略同名的變數宣告

覺得文章不錯,可以點個贊呀_ 歡迎關注南玖~

相關文章