該篇文章整理了一些前端經典面試題,附帶詳解,涉及到JavaScript多方面知識點,滿滿都是干貨~建議收藏閱讀
前言
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新的文章~
1.說一說JavaScript的數據類型以及存儲方式
JavaScript一共有8種數據類型
其中有7種基本數據類型:
ES5的5種:Null
,undefined
,Boolean
,Number
,String
,
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
屬性,並讓其指向該函數。
當執行 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綁定.
- 函數是否在 new 中調用(new綁定),如果是,那么 this 綁定的是new中新創建的對象。
- 函數是否通過 call,apply 調用,或者使用了 bind (即硬綁定),如果是,那么this綁定的就是指定的對象。
- 函數是否在某個上下文對象中調用(隱式綁定),如果是的話,this 綁定的是那個上下文對象。一般是 obj.foo()
- 如果以上都不是,那么使用默認綁定。如果在嚴格模式下,則綁定到 undefined,否則綁定到全局對象。
- 如果把 null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind, 這些值在調用時會被忽略,實際應用的是默認綁定規則。
- 箭頭函數沒有自己的 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,其中第三個參數可以指定事件是否在捕獲階段執行。
事件委托
事件委托指的是把一個元素的事件委托到另外一個元素上。一般來講,會把一個或者一組元素的事件委托到它的父層或者更外層元素上,真正綁定事件的是外層元素,當事件響應到需要綁定的元素上時,會通過事件冒泡機制從而觸發它的外層元素的綁定事件上,然后在外層元素上去執行函數。
事件傳播(三個階段)
- 捕獲階段–事件從 window 開始,然后向下到每個元素,直到到達目標元素事件或event.target。
- 目標階段–事件已達到目標元素。
- 冒泡階段–事件從目標元素冒泡,然后上升到每個元素,直到到達 window。
事件捕獲
當事件發生在 DOM 元素上時,該事件並不完全發生在那個元素上。在捕獲階段,事件從window開始,一直到觸發事件的元素。window----> document----> html----> body ---->目標元素
事件冒泡
事件冒泡剛好與事件捕獲相反,當前元素---->body ----> html---->document ---->window
。當事件發生在DOM元素上時,該事件並不完全發生在那個元素上。在冒泡階段,事件冒泡,或者事件發生在它的父代,祖父母,祖父母的父代,直到到達window為止。
如何阻止事件冒泡
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
提出的方案,使用import
和export
的形式來導入導出模塊。
CommonJS
Node.js
是commonJS
規范的主要踐行者。這種模塊加載方案是服務器端的解決方案,它是以同步的方式來引入模塊的,因為在服務端文件都存儲在本地磁盤,所以讀取非常快,所以以同步的方式加載沒有問題。但如果是在瀏覽器端,由於模塊的加載是使用網絡請求,因此使用異步加載的方式更加合適。
// 定義模塊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 在語言標准的層面上,實現了模塊功能,而且實現得相當簡單,旨在成為瀏覽器和服務器通用的模塊解決方案。其模塊功能主要由兩個命令構成:export
和import
。export
命令用於規定模塊的對外接口,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」)
閉包原理
函數執行分成兩個階段(預編譯階段和執行階段)。
- 在預編譯階段,如果發現內部函數使用了外部函數的變量,則會在內存中創建一個“閉包”對象並保存對應變量值,如果已存在“閉包”,則只需要增加對應屬性值即可。
- 執行完后,函數執行上下文會被銷毀,函數對“閉包”對象的引用也會被銷毀,但其內部函數還持用該“閉包”的引用,所以內部函數可以繼續使用“外部函數”中的變量
利用了函數作用域鏈的特性,一個函數內部定義的函數會將包含外部函數的活動對象添加到它的作用域鏈中,函數執行完畢,其執行作用域鏈銷毀,但因內部函數的作用域鏈仍然在引用這個活動對象,所以其活動對象不會被銷毀,直到內部函數被燒毀后才被銷毀。
優點
- 可以從內部函數訪問外部函數的作用域中的變量,且訪問到的變量長期駐扎在內存中,可供之后使用
- 避免變量污染全局
- 把變量存到獨立的作用域,作為私有成員存在
缺點
- 對內存消耗有負面影響。因內部函數保存了對外部變量的引用,導致無法被垃圾回收,增大內存使用量,所以使用不當會導致內存泄漏
- 對處理速度具有負面影響。閉包的層級決定了引用的外部變量在查找時經過的作用域鏈長度
- 可能獲取到意外的值(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()與比較操作符==
、===
的區別?
==
會先進行類型轉換再比較===
比較時不會進行類型轉換,類型不同則直接返回falseObject.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指向的函數。
18.說說你了解哪些前端本地存儲?
推薦閱讀:這一次帶你徹底了解前端本地存儲
19.說說JavaScript數組常用方法
向數組添加元素的方法:
- push:向數組的末尾追加 返回值是添加數據后數組的新長度,改變原有數組
- unshift:向數組的開頭添加 返回值是添加數據后數組的新長度,改變原有數組
- splice:向數組的指定index處插入 返回的是被刪除掉的元素的集合,會改變原有數組
向數組刪除元素的方法:
- pop():從尾部刪除一個元素 返回被刪除掉的元素,改變原有數組
- shift():從頭部刪除一個元素 返回被刪除掉的元素,改變原有數組
- splice:在index處刪除howmany個元素 返回的是被刪除掉的元素的集合,會改變原有數組
數組排序的方法:
- reverse():反轉,倒置 改變原有數組
- 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代碼的容錯性,使一些不規范的代碼也可以正常執行
- 函數是一等公民,當函數聲明與變量聲明沖突時,變量提升時函數優先級更高,會忽略同名的變量聲明
覺得文章不錯,可以點個贊呀_ 歡迎關注南玖~