前言
2020年已經到來,是不是該為了更好的2020年再戰一回呢? ‘勝敗兵家事不期,包羞忍恥是男兒。江東子弟多才俊,捲土重來未可知’,那些在秋招失利的人,難道就心甘情願放棄嗎!
此文總結2019年以來本人經歷以及瀏覽文章中,較熱門的一些面試題,涵蓋從CSS到JS再到Vue再到網路等前端基礎到進階的一些知識。
總結面試題涉及的知識點是對自己的一個提升,也希望可以幫助到同學們,在2020年會有一個更好的競爭能力。
css篇
- juejin.im/post/5e040e…Es6篇
- juejin.im/post/5e2962…Vue篇
- juejin.im/post/5e5485…
Module Two - JavaScript
1 - 基本型別
- 簡單資料型別(原始型別): String、Number、Boolean、Null、Undefined、Symbol
- 複雜資料型別(物件型別): Object
Null是物件嗎?
雖然typeof null
返回的是object
,但這其實是JavaScript長久以來遺留的一個bug,null
實質上是基本資料型別的一種
簡單資料型別與複雜資料型別在資料儲存上有什麼區別?
簡單資料型別以棧的形式儲存,儲存的是值
複雜資料型別以堆的形式儲存,地址(指向堆中的值)儲存在棧中。
❗ 小知識: 當我們把物件賦給另外一個變數時,複製的是地址,指向同一塊記憶體空間,所以當其中一個物件改變時,另外一個物件也會隨之改變
棧記憶體與對堆記憶體的區別
- 棧記憶體
JavaScript中原始型別的值被直接儲存在棧中,在定義變數時,棧就為其分配好記憶體空間
- 儲存的值大小固定
- 空間較小
- 可以直接操作其儲存的變數,執行效率高
- 由系統自動分配儲存空間
複製程式碼
- 堆記憶體
JavaScript中引用型別(物件型別)的值實際儲存在堆記憶體中,
它在棧中只儲存了一個固定長度的地址,這個地址指向堆記憶體中的值
- 儲存的值大小不定,可動態調整
- 空間較大,執行效率低
- 無法直接操作其內部儲存,使用其地址讀取值
- 通過程式碼分配空間
複製程式碼
2 - typeof與instanceof的作用和區別是什麼?
- typeof
- 能夠正確判斷簡單資料型別(原始型別),除了null,typeof null結果為object
- 對於物件而言,typeof不能正確判斷物件型別,typeof僅可以區分開function,除此之外,結果均為object
複製程式碼
- instanceof
- 能夠準確判斷複雜資料型別,但是不能正確判斷簡單資料型別
複製程式碼
instanceof的原理
instanceof是通過原型鏈進行判斷的,A instanceof B
,在A的原型鏈中層層查詢,查詢是否有原型等於B.prototype
,如果一直找到A的原型鏈的頂端,即Object.prototype.__proto__
,仍然不等於B.prototype
,那麼返回false,否則返回true
❗ 小知識:
var str = 'hello world' str instanceof String → false
var str = new String('hello world') str instanceof String → true
複製程式碼
3 - 函式引數為物件的情況
function test(person) {
person.age = 26;
person = {
name: 'foo',
age: 30
}
return person
}
const p1 = {
name: 'bar',
age: 25
}
const p2 = test(p1)
console.log(p1) // name:bar age:26
console.log(p2) // name:foo age:30
複製程式碼
解析 首先函式傳參是按值傳遞的,即傳遞的是物件指標的副本,到函式內部修改引數這一步,p1的值也被修改,但是當我們重新為person分配一個物件時,是建立了一個新的地址(指標),也就和p1沒有關係了,所以最終2個變數的值不同
4 - 型別轉換
- 轉Boolean
在條件判斷中,除了undefined、null、''、false、0、-0、NaN,其他所有值都轉為true,包括空資料和物件
複製程式碼
- 物件轉原始型別
物件在進行型別轉換時,會呼叫內部的[[ToPrimitive]]函式
- 如果已經是原始型別,則不需要進行轉換
- 呼叫x.value(),如果轉換為基礎型別,則返回轉換的值
- 呼叫x.toString(),如果轉換為基礎型別,則返回轉換的值
- 如果都不返回原始型別值,則報錯
重寫:
let a = {
valueOf(){
// toDo
},
toString(){
// toDo
},
[Symbol.toPrimitive](){
// toDo
}
}
複製程式碼
❗ 小知識:
引用型別 → Number
先進行valueOf,再進行toString
引用型別 → String
先進行toString,再進行valueOf
若valueOf和toString都不存在,則返回基本型別錯誤,丟擲TypeError
例子:
const Obj1 = {
valueOf:() => {
console.log('valueOf')
return 123
},
toString:() => {
console.log('toString')
return 'Chicago'
}
}
const Obj2 = {
valueOf:() => {
console.log('valueOf')
return {}
},
toString:() => {
console.log('toString')
return '{}'
}
}
console.log(Obj1 - 1) → valueOf 122
console.log(Obj2 - 1) → valueOf toString TypeError
複製程式碼
- 加法運算
加法運算與其他有所區別
- 當運算子其中一方為字串時,那麼另一方也轉換為字串
- 當一側為Number型別,另一側為原始型別,則將原始型別轉換為Number
- 當一側為Number型別,另一側為引用型別,則將引用型別和Number型別轉換為字串後拼接
例子:
1 + '1' → '11'
true + true → 2
4 + [1,2,3] → '41,2,3'
Ps: 特別注意 'a'+ +'b',因為 +'b' 會等於NaN,所以結果為 'aNaN'
複製程式碼
- 除加法外,只要其中一方為數字,那麼另一方就會轉換為數字
- 比較運算子的轉換規則( == )
- NaN:和其他型別比較永遠返回false(包括自己)
- Boolean:和其他型別比較,Boolean首先被轉化為Number(true → 1、false → 0)
- String和Number:String先轉化為Number型別,再進行比較
- Null和undefined:null == undefined → true,除此之外,null、undefined和其他任何值比較均為false
- 原始型別和引用型別:引用型別轉換為原始型別
複製程式碼
5 - 常見面試點:[] == ![] 為何為true、[undefined]為何為false?
1)由於 ! 的優先順序高於 == ,![] 首先會被轉換為false,然後根據Boolean轉換原則,false將會轉換為Number型別,即轉換為 0,然後左側的 [] 轉換為原始型別,也為 0 ,所以最終結果為 true
2)陣列元素為null、undefined時,該元素被當作空字串,所以 [undefined]、[null] 都會變為 0 , 最終 0 == false → true
6 - 什麼是包裝型別,與原始型別有什麼區別??
包裝型別即 Boolean、Number、String
與原始型別的區別:
true === new Boolean → false
123 === new Number('123') → false
'Chicago' === new String('Chicago') → false
typeof new String('Chicago') → Object
typeof 'Chicago' → string
複製程式碼
什麼是裝箱和拆箱? 裝箱即原始型別轉換為包裝型別、拆箱即包裝型別轉換為原始型別
如何使用原始型別來呼叫方法?
原始型別呼叫方法,實際上自動進行了裝箱和拆箱操作
var name1 = 'Chicago'
var name2 = name1.substring(2)
以上2行程式碼,實際上發生了3個事情
- 建立一個String的包裝類例項
- 在例項上呼叫substring方法
- 銷燬例項
複製程式碼
手動裝箱、拆箱
var obj1 = new Number(123)
var obj2 = new String('chicago')
console.log(typeof obj1.valueOf()) → number
console.log(typeof obj2.toString()) → string
複製程式碼
7 - 如何讓 a == 1 && a == 2 && a == 3 為 true?
依據拆箱:
const a = {
value:[3,2,1],
valueOf:function(){
return this.value.pop()
} // 每次呼叫,刪除一個元素
}
console.log(a == 1 && a == 2 && a == 3) // true (注意僅能判斷一次)
複製程式碼
★ 8 - 如何正確判斷this? 箭頭函式的this又是什麼?
誰呼叫它,this就指向誰這句話可以說即精準又帶坑
(繫結方式) 影響this的指向實際有4種:
- 預設繫結:全域性呼叫
- 隱式呼叫:物件呼叫
- 顯示呼叫:call()、apply()、bind()
- new繫結
複製程式碼
- 預設
function foo(){
console.log(this.a)
}
var a = 2
foo() // 2 → this指向全域性
複製程式碼
- 隱式
function foo(){
console.log(this.a)
}
var obj1 = {
a = 1,
foo
}
var obj2 = {
a = 2,
foo
}
obj1.foo() // 1 → this 指向 obj1
obj2.foo() // 2 → this 指向 obj2
複製程式碼
- 顯式
function foo(){
console.log(this.a)
bar.apply( {a:2},arguments )
}
function bar(b){
console.log(this.a + b)
}
var a = 1 // 全域性 a 變數
foo(3) // 1 5 → 1 說明第一個列印種 this 指向全域性,5 說明第二個列印中 this 指向 {a:2}
複製程式碼
❗ 小知識:
call()、apply()、bind()三者區別:
call()、apply()屬於立即執行函式,區別在於接收的引數形式不同,前者是依次傳入引數,後者引數可以是陣列
bind()則是建立一個新的包裝函式,並且返回,它不會立即執行bind(this,arg1,arg2···)
複製程式碼
▲ 當call、apply、bind傳入的第一個引數為 undefined/null 時,嚴格模式下this值為傳入的undefined/null,非嚴格模式下,實際應用預設繫結,即指向全域性(node環境下指向global、瀏覽器環境下指向window)
function info(){
console.log(this);
console.log(this.age);
}
var person = {
age:20,
info
}
var age = 28;
var info = person.info;
info.call(null); // window 、 28
複製程式碼
- new繫結
- 建構函式返回值不是function/object
function Super(age){
this.age = age
}
let instance = new Super('26')
console.log(instance.age) // '26'
- 建構函式返回function/object
function Super(age){
this.age = age
let obj = {
a:2
}
return obj
}
let instance = new Super('26')
console.log(instance.age) // undefined → 返回的新obj中沒有age
複製程式碼
靈魂拷問:new 的實現原理
1 - 建立一個新物件
2 - 這個新物件會被執行[[原型]]連結
3 - 屬性和方法被加入到this引用的物件裡,並執行建構函式中的方法
4 - 如果函式沒有返回其他物件,那麼this指向這個新物件,否則this指向建構函式返回的物件
複製程式碼
❗ 小知識:
對於this的繫結問題,優先順序如下
New > 顯式繫結 > 隱式繫結 > 預設繫結
複製程式碼
箭頭函式的this
1) 箭頭函式沒有自己的this
當我們使用箭頭函式的時候,箭頭函式會預設幫我們繫結外層this的值,所以在箭頭函式中this的值與外層的this是一樣的
例子1:
const obj = {
a: () => {
console.log(this)
}
}
obj.a() //打出來的是window
因為箭頭函式預設不會使用自己的this,而是會和外層的this保持一致,最外層的this就是window物件
複製程式碼
例子2:
let obj = {
age:20,
info:function(){
return () => {
console.log(this.age)
}
}
}
let person = { age:28 }
let info1 = obj.info()
info1() // 20
let info2 = obj.info.call(person)
info2() // 28
複製程式碼
2) 箭頭函式不能在call方法修改裡面的this
函式的this可以通過call等顯式繫結的方式修改,而為了減少this的複雜性,箭頭函式無法用call()來指定this
const obj = {
a: () => {
console.log(this)
}
}
obj.a.call('123') //打出來的結果依然是window物件
複製程式碼
9 - 如果對一個函式進行多次bind,會出現什麼情況?
不管我們給函式進行幾次bind顯式繫結,函式中的this永遠由 第一次bind 決定
let a = {}
let fn = function(){
console.log(this)
}
fn.bind().bind(a)() // => Window
複製程式碼
10 - == 與 === 有什麼區別?
- == 如果雙方型別不同,會自動進行型別轉換
- === 判斷兩者型別與值是否相同,不會進行型別轉換
★ 11 - 何為閉包?
紅寶書上對於閉包的定義:閉包是指有權訪問另外一個函式作用域中的變數的函式
簡單來說,閉包就是一個函式A內部有另一個函式B,函式B可以訪問到函式A的變數和方法,此時函式B就是閉包
例子:
function A(){
let a = 1
window.B = function(){
console.log(a)
}
}
A() // 定義a,賦值window.B
B() // 1 → 訪問到函式A內部的變數
複製程式碼
閉包存在意義
在Js中,閉包存在的意義就是讓我們可以間接訪問到函式內部的變數 (函式內部變數的引用也會在內部函式中,不會因為執行完函式就被銷燬,但過多的閉包會造成記憶體洩漏)
閉包的三個特性
- 閉包可以訪問當前函式以外的變數
function getOuter(){
var data = 'outer'
function getDate(str){
console.log(str + data) // 訪問外部變數 'data'
}
return getDate('I am')
}
getOuter() // I am outer
複製程式碼
- 即使外部函式已經返回,閉包仍能訪問外部函式定義的變數
function getOuter(){
var date = '815';
function getDate(str){
console.log(str + date); //訪問外部的date
}
return getDate; //外部函式返回
}
var today = getOuter();
today('今天是:'); //"今天是:815"
today('明天不是:'); //"明天不是:815"
複製程式碼
- 閉包可以更新外部變數的值
function updateCount(){
var count = 0;
function getCount(val){
count = val; // 更新外部函式變數
console.log(count);
}
return getCount; //外部函式返回
}
var count = updateCount();
count(815); //815
count(816); //816
複製程式碼
經典面試題:迴圈中使用閉包解決var定義的問題(迴圈輸出0-5,結果卻是一堆6?)
for (var i = 0; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, 1000 * i)
} // 6,6,6,6,6,6
for(var i = 1;i <= 5; i++){
(function(j){
setTimeout( () => {
console.log(j)
},j * 1000)
})(i)
} // 0,1,2,3,4,5
// Tips:通過let定義i也能夠解決,因為let具有塊級作用域
複製程式碼
12 - 淺拷貝?深拷貝?如何實現?
- 淺拷貝
1、首先可以通過Object.assign
來實現,Object.assgin
只會拷貝所有的屬性值到新的物件中,但如果屬性值是一個物件的話,拷貝的是地址,所以並不是深拷貝
let obj = {
a: 1,
b:{
foo:'foo',
bar:'bar'
}
}
let objCopy = Object.assign({},obj)
console.log(objCopy) // {a:1,b:{foo:'foo',bar:'bar'}}
obj.a = 2
console.log(objCopy.a) // 1 → 不會隨著obj修改而修改
obj.b.foo = 'FOO'
console.log(objCopy.b) // {foo:'FOO',bar:'bar'} → 拷貝的是地址,指向同一個值,所以修改obj.b會影響到objCopy
複製程式碼
2、也可以通過展開運算子...來實現淺拷貝
let obj = {
a: 1
b:{
foo:'foo',
bar:'bar'
}
}
let objCopy = { ...obj }
console.log(objCopy) // {a:1,b:{foo:'foo',bar:'bar'}}
obj.a = 2
console.log(objCopy.a) // 1
obj.b.foo = 'FOO'
console.log(objCopy.b) // {foo:'FOO',bar:'bar'}
與Object.assign一樣,屬性值為物件的拷貝,拷貝的是地址
複製程式碼
- 深拷貝
通常來說淺拷貝可以解決大部分的問題,但如果遇到下面這種情況,就需要深拷貝來解決
let a = {
age:1,
jobs:{
first:'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
複製程式碼
淺拷貝只能解決第一層的問題,如果物件的屬性還是物件的話,該屬性兩者會共享相同的地址,假如我們不想b的物件屬性隨a改變而改變,就需要通過深拷貝
1 - JSON.parse(JSON.stringify(object))
let a = {
age: 1,
jobs: { first: 'FE' }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
複製程式碼
2 - lodash庫中的cloneDeep()
複製程式碼
13 - var、let、const的區別是什麼?什麼是變數提升?暫時性死區又是什麼?
三者區別
1 - var【宣告變數】
var 沒有塊的概念,可以跨塊訪問,無法跨函式訪問
2 - let【宣告塊中的變數】
let 只能在塊作用域裡訪問,不能跨塊訪問,更不能跨函式訪問
3 - const【宣告常量,一旦賦值便不可修改】
const 只能在塊級作用域裡訪問,而且不能修改值
Tips: 這裡的不能修改,並不是變數的值不能改動,而是變數所指向的那個記憶體地址儲存的指標不能改動
複製程式碼
❗ 小知識:
var a = 1
let b = 1
const c = 1
console.log(window.a) // 1
console.log(window.b) // undefined
console.log(window.c) // undefined
在全域性作用域下使用let和const宣告變數,變數並不會被掛載到window上,這一點與var不同
關於const,還有兩個注意點:
- const宣告之後必須馬上賦值,否則報錯
- const簡單型別一旦宣告就不能修改,而複雜型別(陣列,物件)指標指向的地址不能修改,但內部資料可以修改
複製程式碼
何為提升?
console.log(a) // undefined
var a = 1
複製程式碼
上面兩行程式碼,雖然在列印a前變數並沒有被宣告,但是卻可以使用這個未宣告的變數,不報錯,這一種情況就叫做提升,而且提升的是宣告
實際上,提升不僅僅只能作用於變數的宣告,函式的宣告也會被提升
console.log(a) // f a(){}
function a(){}
var a = 1
複製程式碼
函式的宣告優先順序高於變數的宣告
何為暫時性死區?
console.log(a) // ReferenceError: Cannot access 'a' before initialization
let a
複製程式碼
為何這次就會報錯呢? 只要一進入當前作用域,所要使用得變數就已經存在了,但是不可獲取,只有等到宣告變數的那一行程式碼出現,才可以獲取和使用該變數,這就是暫時性死區
var a = 123; // 宣告
if (true) {
a = 'A'; // 報錯 因為本塊級作用域有a宣告變數
let a; // 繫結if這個塊級的作用域 不能出現a變數
}
複製程式碼
對於暫時性死區,我的理解是宣告提升了,但初始化沒有被提升,而提升是宣告提升,並初始化為undefined
總結
函式提升優於變數提升,函式提升會把整個函式提升到作用域頂部,變數提升只會把宣告提升到作用域頂部
- var存在提升,我們能在宣告之前使用。let和const由於暫時性死區的原因,不能在宣告前使用
- var 在全域性作用域下宣告變數會導致變數被掛載到window上,其他兩者不會
- let / const 作用基本一致,但後者不允許再次賦值
- let、const不允許在相同作用域內,重複宣告同一個變數
★ 14 - 原型 / 建構函式 / 例項
- 原型 一個簡單的物件,用於實現物件的屬性繼承
每一個JavaScript物件(null除外)在建立的時候就會有一個與之關聯的物件,這個物件就是原型物件
每一個物件都會從原型上繼承屬性
複製程式碼
- 建構函式 可以通過
new
來建立一個物件的函式 - 例項 通過建構函式和
new
建立出來的物件,便是例項
例項通過__proto__指向原型,通過constructor指向建構函式
複製程式碼
以Object
為例子,Object
便是一個建構函式,我們通過它來構建例項
const instance = new Object()
複製程式碼
這裡,instance
是Object
的例項,Object
是instance
的建構函式,建構函式擁有一個prototype
的屬性來指向原型,因此有
const prototype = Object.prototype
複製程式碼
原型、建構函式、例項 三者關係
例項.__proto__ === 原型
原型.constructor === 建構函式
建構函式.prototype === 原型
Tips:
const instance = new Object()
instance.constructor === Object // true
當獲取 例項.constructor 時,其實例項上並沒有constructor屬性,當不能讀到constructor屬性時,會從例項的原型中讀取
則有 instance.constructor === instance.__proto__.constructor
如果修改了instance.__proto__,instance.constructor將不再為Object
instance.__proto__ = null
instance.constructor === Object // false
複製程式碼
★ 15 - 原型鏈
原型鏈是由相互關聯的原型物件組成的鏈狀結構
每個物件都有__proto__
屬性(建構函式也有),指向建立該物件的建構函式的原型 ,__proto__
將物件連結起來組成了原型鏈。是一個可以用來實現繼承和共享屬性的有限鏈
原型鏈中的機制
- 屬性查詢機制 當查詢物件的屬性時,如果例項物件自身不存在該屬性,則沿著原型鏈往上一級查詢,找到時則輸出,不存在時,則繼續沿著原型鏈往上一級查詢,直至最頂級的原型物件
Object.prototype
(Object.prototype.__proto__ === null
),假如還是沒有找到,則輸出undefined - 屬性修改機制 只會修改例項物件本身的屬性,如果不存在,則進行新增該屬性,如果需要修改原型的屬性,則需要通過
prototype
屬性(b.prototype.B = 1
),但這樣修改會導致所有繼承於這個物件的例項的屬性發生改變
16 - 原型如何實現繼承?Class如何實現繼承?Class本質是什麼?
- 組合繼承 使用原型鏈實現對原型屬性和方法的繼承,使用借用建構函式來實現對例項屬性的繼承
function Parent(value){
this.val = value // 例項屬性
}
Parent.prototype.getValue = function(){ // 原型屬性
console.log(this.val)
}
function Child(value){
Parent.call(this,value) // 借用建構函式來繼承例項屬性
}
Child.prototype = new Parent() // 原型鏈繼承
const child = new Child(1)
child.getValue() // 1
child instancof Parent // true
複製程式碼
這種方式優點在於建構函式可以傳參,不會與父類共享引用屬性,可以複用父類的函式,但缺點就是在繼承父類函式的時候呼叫父類建構函式,導致子類的原型上會多了不需要的父類屬性,存在記憶體浪費
- 寄生組合繼承
function Parent(value){
this.val = value
}
Parent.prototype.getValue = function(){
console.log(this.val)
}
function Child(value){
Parent.call(this,value)
}
Child.prototype = Object.create(Parent.prototype,{
constructor:{
value:Child,
enumerable:false.
writable:true,
configurable:true
}
}) // 將父類的原型賦值給子類,並將原型的constructor修改為子類
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
複製程式碼
這種寄生組合繼承是對組合繼承進行優化的,核心就是將父類的原型賦值給子類,並且將建構函式設定為子類,這樣解決了無用的父類屬性問題,還能正確的找到子類的建構函式
Class本質及繼承實現
其實JavaScript中並不存在類的概念,class只是一種語法糖,本質來說還是函式
class Person{}
Person instanceof Function // true
複製程式碼
Class繼承
在ES6中,我們可以通過class
實現繼承
class Parent{
constructor(value){
this.val = value
}
getValue(){
console.log(this.val)
}
}
class Child extends Parent{
constructor(value){
super(value)
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
複製程式碼
Class實現繼承,核心在於使用extends
關鍵字來表明繼承自哪個父類,並且在子類建構函式中必須呼叫super
★ 17 - 什麼是作用域和作用域鏈
說到作用域,我們要先理解什麼是執行上下文
執行上下文 可以簡單理解為一個物件,它具有三個型別:
- 全域性執行上下文(caller)
- 函式執行上下文(callee)
eval()
執行上下文
通過程式碼執行過程來理解執行上下文
- 建立 全域性上下文(global EC)
- 全域性執行上下文(caller)逐行以自上而下的順序執行,遇到函式時,函式執行上下文(callee) 被
push
到執行棧頂層 - 函式執行上下文被啟用,成為
active EC
,然後開始執行函式中的程式碼,全域性執行上下文(caller)被掛起 - 函式執行完後,函式執行上下文(callee) 被
pop
移除出執行棧,控制權交回給全域性執行上下文(caller),繼續按照自上而下的順序執行程式碼
❗ 小知識:
變數物件,是執行上下文中的一部分,可以抽象為一種資料作用域
其實也可以理解為一個簡單的物件,儲存著該執行上下文中的所有變數和函式宣告(不包括函式表示式)
活動物件(AO)- 當變數物件所處的上下文被啟用時(active EC)時,稱為活動物件
複製程式碼
作用域
作用域可以理解為當前上下文中宣告的變數和函式的作用範圍,它規定了如何查詢變數,也就是當前執行程式碼對變數的訪問許可權
作用域可以分為 塊級作用域 和 函式作用域
作用域特性:
- 變數提升:一個宣告在函式體內部都是可見的(僅var),函式宣告由於變數宣告
- 非匿名自執行函式 ,函式變數為 只讀 狀態,不能修改
let a = function(){
console.log(1)
};
(function a(){
a = 'a'
console.log(a)
})() // ƒ a() { a = 'a' ; console.log(a) }
複製程式碼
作用域鏈
作用域鏈可以理解為一組物件列表,由當前環境與上層環境的一系列變數物件組成,因此我們可以通過作用域鏈訪問到父級裡面宣告的變數或者函式
作用域鏈由兩部分組成
[[scope]]
屬性:指向父級變數物件和作用域鏈,也就是包含了父級的[[scope]]
和活動變數(AO)
- 活動變數:自身活動變數
由於[[scope]]
包含父級[[scope]]
形成鏈狀關係,便自上而下形成鏈式作用域
作用域鏈作用
保證當前執行環境裡,有權訪問的變數和函式是有序的(作用域鏈的變數只能向上訪問變數,訪問到window物件時被終止)
Ps:作用域鏈不允許向下訪問
作用域鏈和原型繼承查詢時的區別 - 如果去查詢一個普通物件的屬性,但是在當前物件和其原型中都找不到時,會返回undefined
;但查詢的屬性在作用域鏈中不存在的話就會丟擲ReferenceError
18 - script的引入方式
- HTML頁面通過
<script>
標籤引入 - 非同步方式
- Js動態插入
<script>
標籤 <script defer>
延遲載入,元素解析完畢後執行<script async>
非同步載入,但執行時會阻塞元素渲染
- Js動態插入
19 - null 與 undefined 有什麼區別?
- null 表示一個物件被定義了,但值是空值(定義為空)
- undefined 表示不存在這個值
typeof null // 'Object'
null 是一個空物件,沒有任何的屬性和方法
typeof undefined // 'undefined'
undefined 是一個表示'無'的原始值或表示缺少值,例如變數被宣告瞭,但沒有任何賦值時
複製程式碼
從記憶體來看null和undefined,本質區別是什麼?
- 給一個全域性變數賦值為null,相當給這個屬性分配了一塊空的記憶體,然後值為null,Js會回收全域性變數為null的物件,一般用於主動釋放指向物件的引用
- 給一個全域性變數賦值為undefined,相當於將這個物件的值清空,但是這個物件依舊存在,表示變數宣告過但並未賦過值。 它是所有未賦值變數預設值
20 - 什麼是記憶體洩漏?如何解決記憶體洩漏?
記憶體洩漏 在使用一些記憶體之後,如果後面不再需要用到這些記憶體,但沒有將它們及時釋放掉,就稱為記憶體洩漏
如果出現嚴重的記憶體洩漏,那麼有可能使得記憶體越來越大,最終導致瀏覽器崩潰
四種常見的Js記憶體洩露
- 意外的全域性變數
未定義的變數會在全域性物件建立一個新變數
function foo(arg){
bar = 'I am belong to global' // 未定義,會建立在全域性中
}
解決方案:
- 在JavaScript頭部加上`use strict`,使用嚴格模式避免意外的全域性變數
複製程式碼
- 被遺忘的定時器或回撥函式
定義定時器(setTimeout / setInterval)後沒有移除(clearTimeout / clearInterval)
複製程式碼
- 脫離DOM的引用
- 閉包
閉包的關鍵是匿名函式可以訪問父級作用域的變數,讓變數不被回收
如果不及時清除,就會導致記憶體洩漏
複製程式碼
如何解決記憶體洩漏?
通過GC垃圾回收機制來解決記憶體洩漏
所謂垃圾回收機制,是指找到記憶體空間中的垃圾並回收,可以再次利用這部分記憶體空間
垃圾回收機制有兩種方式:
- 引用計數: 當宣告一個變數並將一個引用型別賦值給該變數時,則這個值引用就加1,相反,如果包含這個值的變數又取得另外一個值,那麼這個值的引用就減去1,當引用次數變為0,則說明沒有辦法訪問這個值,所以就可以把其所佔有的記憶體空間回收
- 標記清除: 當變數進入環境時,就標記這個變數為進入環境,當變數離開環境時就將其標記為離開環境
21 - Js中的迴圈方式有哪些?For in 與 For of 有什麼區別?
在JavaScript
中,我們可以採用多種方式實現迴圈
while
do...while
for
for...in
for...of
for in 與 for of 的區別
- for in
- 遍歷物件及其原型鏈上可列舉的屬性
- 如果用於遍歷陣列,除了遍歷其元素外,還會遍歷陣列物件自定義的可列舉屬性及其原型鏈上的可列舉屬性
- 遍歷物件返回的屬性名和遍歷陣列返回的索引都是字串索引
- 某些情況下,可能按隨機順序遍歷陣列元素
複製程式碼
- for of
- es6 中新增的迴圈遍歷語法
- 支援遍歷陣列,類陣列物件(DOM NodeList),字串,Map 物件,Set 物件
- 不支援遍歷普通物件
- 遍歷後輸出的結果為陣列元素的值
- 可搭配例項方法 entries(),同時輸出陣列的內容和索引
複製程式碼
- 補充:Object.keys
- 返回物件自身可列舉屬性組成的陣列
- 不會遍歷物件原型鏈上的屬性以及 Symbol 屬性
- 對陣列的遍歷順序和 for in 一致
複製程式碼
Tips: for in
更適合遍歷物件,儘量不用for in
來遍歷陣列
22 - 關於陣列(Array)API總結
迭代相關
- every()
對每一項執行給定函式,全true則返回true
複製程式碼
- filter()
對陣列中每一項執行函式,返回該函式會返回true項
複製程式碼
- forEach()
對陣列每一項執行函式,沒有返回值 (forEach無法中途跳出forEach迴圈,break、continue和return都不奏效。)
複製程式碼
- map()
對每一項執行函式,返回每次函式呼叫的結果組成的陣列
複製程式碼
- some()
對每一項執行函式,如果對任一項返回了true,則返回true
複製程式碼
其他
- join('連線符')
通過指定連線符生成字串
複製程式碼
- push/pop
陣列尾部推入和彈出,改變原陣列,返回操作項
複製程式碼
- shift/unshift
陣列頭部彈出和推入,改變原陣列,返回操作項
複製程式碼
- sort(fn)/reverse
陣列排序(fn定義排序規則)與反轉,改變原陣列
複製程式碼
- concat
連線陣列,不改變原陣列,返回新陣列(淺拷貝)
複製程式碼
- slice(start,end)
截斷陣列,返回截斷後的新陣列,不改變原陣列
複製程式碼
- splice(start,number,arg...)
從下標start開始,刪除number個元素,並插入arg,返回所刪除元素組成的陣列,改變原陣列
複製程式碼
- indexOf / lastIndexOf(value, fromIndex)
查詢陣列元素,返回下標索引
複製程式碼
- reduce / reduceRight(fn(prev, cur), defaultPrev)
歸併陣列,prev為累計值,cur為當前值,defaultPrev為初始值
複製程式碼
23 - 常用字串(String)API總結
- concat
連線字串
複製程式碼
- indexOf / lastIndexOf()
檢索字串、從後向前檢索字串
複製程式碼
- match / replace / search
找到一個或多個正規表示式的匹配
替換與正規表示式匹配的子串
檢索與正規表示式匹配的值
複製程式碼
- slice
擷取字串片段,並在新的字串中返回被擷取的片段
複製程式碼
- substr(start,length)
從起始索引號提取字串中指定數目的字元
複製程式碼
- substring(start,stop)
擷取字串中兩個指定的索引號之間的字元。
複製程式碼
- split
用於把一個字串通過指定的分隔符進行分隔成陣列
複製程式碼
- toString()
返回字串
複製程式碼
- valueOf()
返回某個字串物件的原始值
複製程式碼
24 - map、filter、reduce各有什麼作用
- map
作用:生成一個陣列,遍歷原陣列,將每一個元素拿出來做一些變化後存入新陣列
[1,2,3].map(item => item + 1) // [2,3,4]
另外map的回撥接收三個引數,分別是當前元素、索引,原陣列
常見題:['1','2','3'].map(parseInt) 結果是什麼?
['1','2','3'].map(parseInt) → [1,NaN,NaN]
解析:
第一輪遍歷 parseInt('1',0) // 1
第二輪遍歷 parseInt('2',1) // NaN
第三輪遍歷 parseInt('3',2) // NaN
複製程式碼
- filter
作用:生成一個新陣列,在遍歷陣列的時候將返回值為true的元素放在新陣列
場景:我們可以利用這個函式刪除一些不需要的元素(過濾)
let array = [1,2,3,4,5]
let newArray = array.filter(item => item !== 5)
console.log(newArray) // [1,2,3,4]
Tips:與map一致,也接收3個引數
複製程式碼
- reduce
作用:將陣列中的元素通過回撥函式最後轉換為一個值(歸併)
場景:實現一個將陣列裡的元素全部相加得到一個值的功能
const arr = [1,2,3]
const sum = arr.reduce((acc,current) => {
return acc + current
},0)
console.log(sum) // 6
對於reduce來說,它只接受2個引數,分別是回撥函式和初始值:
- 首先初始值為0,該值會在執行第一次回撥函式時作為第一個引數傳入
- 回撥函式接收四個引數,分別為累計值、當前元素、當前索引、原陣列
- 在第一次執行回撥時,當前值和初始值相加為1,該結果會作為第二次回撥時的累計值(第一個引數)傳入
- 第二次執行時,相加的值分別為1和2,以此類推,迴圈結果後得出最終值
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
- total 必選,初始值或計算結束後的返回值
- currentValue 必選,當前元素
- currentIndex 可選,當前索引
- arr 可選,當前元素所屬的陣列物件
- initialValue 可選,傳遞給函式的初始值
複製程式碼
經典面試題 - 實現物件陣列去重
複製程式碼
25 - 程式碼複用
- 函式封裝
- 繼承
- 混入
mixin
- 複製
extend
- 借用
call/apply
26 - 併發與並行有什麼區別?
- 併發 是巨集觀概念,指在一段時間內通過任務間的切換完成了這個兩個任務,這種情況就稱為併發
- 並行 是微觀概念,同時完成多個任務的情況就稱為並行
27 - require 與 import 有什麼區別?
兩者區別在於載入方式不同、規範不同
- 載入方式不同:require是在執行時載入,而import是在編譯時載入
require('./a')() // 假設a模組是一個函式,立即執行a模組函式
var data = reuqire('./a').data // 假設a模組匯出一個物件
Tips:require寫在程式碼哪一行都可以
import Jq from 'jquery'
import * as _ from '_'
import {a,b,c} from './a'
import {default as alias, a as a_a, b, c} from './a';
Tips:import應該用在程式碼行開頭
複製程式碼
- 規範不同:require是CommonJS/AMD規範,而import屬於ES6規範
❗ 小知識:
require特點:
- 提供了伺服器/瀏覽器的模組載入方案。非語言層面的標準。
- 只能在執行時確定模組的依賴關係及輸入/輸出的遍歷,無法進行靜態優化
import特點:
- 語言規格層面支援模組功能。
- 支援編譯時靜態分析,動態繫結
複製程式碼
28 - 如何判斷一個變數是NaN?
NaN與任務值比較都是false,包括他自己,判斷一個變數為NaN,可以通過isNaN()
isNaN(NaN) // true
複製程式碼
29 - 嚴格模式有什麼作用?表現在哪?
- 作用
- 消除JavaScript語法的一些不合理、不嚴謹之處,減少一些怪異行為
- 消除程式碼執行的一些不安全行為,保證程式碼執行的安全
- 提高編譯器效率,增加執行速度
- 為未來新版本的JavaScript做好鋪墊
複製程式碼
- 表現
- 嚴格模式下,delete運算子後跟隨非法識別符號,會丟擲語法錯誤
- 嚴格模式下,定義同名屬性會丟擲語法錯誤
- 嚴格模式下,函式形參存在同名,會丟擲語法錯誤
- 嚴格模式下,不允許八進位制整數直接量
- 嚴格模式下,arguments物件是傳入函式內實參列表的靜態副本
- 嚴格模式下,eval和arguments當做關鍵字,它們不能被賦值和用作變數宣告
- 嚴格模式下,變數必須先宣告,直接給變數賦值,不會隱式建立全域性變數,不能用with
- 嚴格模式下,call/apply第一個引數為null/undefined,不會被轉換為window
複製程式碼
30 - 說說對鬆散型別的理解
JavaScript
中的變數是鬆散型別,所謂鬆散型別,就是指當一個變數被宣告出來就可以儲存任何型別的值,一個變數所儲存值的型別也可以改變
★ 31 - 函式防抖(debounce) 與 函式節流(throttle)
如果事件處理函式(click)呼叫頻率無限制,會加重瀏覽器的負擔,導致使用者體驗非常糟糕,那麼我們可以採用debounce(防抖) 和 throttle(節流) 的方式來減少呼叫頻率,同時又不影響實際效果
- 函式防抖(debounce) 當持續觸發事件時,一定時間段內沒有再次觸發事件,事件處理函式才執行一次,如果設當的事件到來之前,又觸發了事件,就重新開始延時(即設定的時間內觸發事件將無效)
例如:
?當持續觸發scroll事件時,事件處理函式handle只在停止滾動1000毫秒之後才會呼叫一次,即持續觸發滾動事件的過程中,handle一直沒有執行
複製程式碼
function debounce(handle){
let timeout = null // 建立一個標記用來存放定時器
return function(){
clearTimeout(timeout) // 每當使用者呼叫的時候把前一個定時器清空
timeout = setTimeout(() => {
handle.apply(this,arguments)
},500) // 500ms後觸發,期間再次呼叫,則重新計算延時
}
}
function sayHi(){
console.log('防抖成功')
}
var btn = document.getElementById('button')
btn.addEventListener('click',debounce(sayHi))
複製程式碼
- 函式節流(throttle)
當持續觸發事件時,保證一定時間段內只呼叫一次事件處理函式 (通過判斷是否到達一定條件來觸發函式)
- 第一種方式:通過時間戳來判斷是否已到可執行時間,記錄上一次執行的時間戳,然後每次觸發事件執行回撥,回撥中判斷當前時間戳距離上次執行時間戳的間隔是否已經到達設定的時間差,如果是則執行,並更新上次執行的時間戳
- 第二種方式:使用定時器
function throttle(handle){
let canRun = true // 通過閉包儲存一個標記,不被回收
return function(){
if(!canRun) return // 在函式頭部判斷標記是否為true,為false時不允許呼叫handle
canRun = false // 設定標記為false
setTimeout(() => { // 將外部傳入的函式的執行放在setTimeout中
handle.apply(this, arguments)
// 最後在setTimeout執行完畢後再把標記設定為true(關鍵)表示可以執行下一次迴圈了。當定時器沒有執行的時候標記永遠是false,在開頭被return掉
canRun = true
}, 500);
}
}
function sayHi() {
console.log('節流成功');
}
var btn = document.getElementById('button');
btn.addEventListener('click', throttle(sayHi)); // 節流
複製程式碼
32 - 什麼是事件捕獲?什麼是事件冒泡?
- 事件捕獲:事件從最不精準的目標(document物件)開始觸發,然後到最精確的目標 (不精確 → 精確)
- 事件冒泡:事件按照從最特定的事件目標到最不特定的事件目標(document物件)的順序觸發 (特定 → 不特定)
哪些事件不支援冒泡?
- 滑鼠事件:mouseleave、mouseenter
- 焦點事件:blur、focus
- UI事件:scroll、resize
- ···
複製程式碼
事件如何先冒泡後捕獲?
對於同一個事件,監聽捕獲和冒泡,分別對應相應的處理函式,監聽到捕獲事件時,先暫停執行,直到冒泡事件被捕獲後再執行事件
33 - 如何阻止事件冒泡?又如何阻止預設行為?
- 阻止事件冒泡
非IE瀏覽器:event.stopPropagation()
IE瀏覽器:window.event.cancelBubble = true
function stopBubble(e){
// 如果提供了事件物件,則是非IE瀏覽器下
if(e && e.stopPropagation){
// 因此它支援W3C的stopPropagation()方法
e.stopPropagation()
}else{
// IE瀏覽器下,取消事件冒泡
window.event.cancelBubble = true
}
}
複製程式碼
- 阻止預設行為
非IE瀏覽器:event.preventDefault()
IE瀏覽器:window.event.returnValue = false
function stopDefault(e) {
//阻止預設瀏覽器動作(W3C)
if (e && e.preventDefault) e.preventDefault();
//IE中阻止函式器預設動作的方式
else window.event.returnValue = false;
return false;
}
複製程式碼
34 - 事件委託是什麼?
所謂事件委託,就是利用事件冒泡的原理,讓自己所觸發的事件,讓其父元素代替執行
即:不在事件(直接DOM)上設定監聽函式,而是在其父元素上設定監聽函式,通過事件冒泡,父元素可以監聽到子元素上事件的觸發,通過判斷事件發生在哪一個子元素上來做出不同的響應
為什麼要用事件委託?好處在哪?
- 提高效能
<ul>
<li>蘋果</li>
<li>香蕉</li>
<li>鳳梨</li>
</ul>
// 在ul上設定監聽函式(Good)
document.querySelector('ul').onclick = (event) => {
let target = event.target
if (target.nodeName === 'LI') {
console.log(target.innerHTML)
}
}
// 在每一個li上監聽函式(Bad)
document.querySelectorAll('li').forEach((e) => {
e.onclick = function() {
console.log(this.innerHTML)
}
})
複製程式碼
- 新新增的元素也能觸發繫結在父元素上的監聽事件
事件委託與事件冒泡的對比
- 事件冒泡:父元素下無論是什麼元素,點選後都會觸發 box 的點選事件
- 事件委託:可以對父元素下的元素進行篩選
複製程式碼
35 - Js中高階函式是什麼?
高階函式(Highter-order-function)的定義很簡單,就是至少滿足下列一個條件的函式:
- 接受一個或多個函式作為輸入
- 輸出一個函式
也就是說高階函式是對其他函式進行操作的函式,可以將它們作為引數傳遞,或者是返回它們。
- 函式作為引數傳遞
Javascript中內建了一些高階函式,比如
Array.prototype.map
、Array.prototype.filter
、Array.prototype.reduce
···,它們接受一個函式作為引數,並應用這個函式到列表的每一個元素
對比使用高階函式和不使用高階函式
例子:有一個陣列[1,2,3,4],我們想要生成一個新陣列,其元素是之前陣列的兩倍
- 不使用高階函式
const arr1 = [1,2,3,4]
const arr2 = []
for(let i = 0; i < arr1.length; i++){
arr2.push(arr1[i] * 2)
}
- 使用高階函式
const arr1 = [1,2,3,4]
const arr2 = []
arr2 = arr1.map( item => item * 2)
複製程式碼
- 函式作為返回值輸出
在判斷型別的時候可以同個Object.prototype.toString.call來獲取對應物件返回的字串,如:
let isString = obj => Object.prototype.toString.call( obj ) === '[object String]'
let isArray = obj => Object.prototype.toString.call( obj ) === '[object Array]'
let isNumber = obj => Object.prototype.toString.call( obj ) === '[object Number]'
可以發現這三行有許多重複程式碼,只需要把具體的型別抽離出來就可以封裝成一個判斷型別的方法,如:
let isType = (type, obj) => {
return Object.prototype.toString.call(obj) === '[Object ' + type + ']'
}
isType('String')('123'); // true
isType('Array')([1, 2, 3]); // true
isType('Number')(123); // true
複製程式碼
36 - 什麼是柯里化函式?
柯里化 - 簡單來說就是隻傳遞函式一部分引數來呼叫它,讓它返回一個新函式去處理剩下的引數。
通過add()函式來了解柯里化
const add = (...args) => args.reduce( (a, b) => a + b ) // a為初始值或計算結束的返回值,b為當前元素
// 傳入多個引數,執行add函式
add(1,2) // 3
// 假設我們實現了一個柯里化函式,支援一次傳入一個引數
let sum = currying(add)
// 封裝第一個引數,方便重用
let addCurryOne = sum(1)
addCurryOne(2) // 3
addCurryOne(3) // 4
複製程式碼
實現currying
函式
我們可以理解所謂的柯里化函式,就是封裝一系列的處理步驟,通過閉包將引數集中起來計算,最後再把需要處理的引數傳進去,那麼如何實現currying
函式呢?
實現原理就是用閉包把傳入的引數儲存起來,當傳入引數的數量足夠執行函式時,就開始執行函式
實現一個健壯的currying函式:
function currying(fn, length) {
length = length || fn.length // 第一次呼叫獲取函式 fn 引數的長度,後續呼叫獲取 fn 剩餘引數的長度
return function( ...args ) { // currying 包裹之後返回一個新函式,接收引數為 ...args
return args.length >= length // 新函式接收的引數長度是否大於等於 fn 剩餘引數需要接收的長度
? fn.apply(this, args) // 滿足要求,執行 fn 函式,傳入新函式的引數
: currying(fn.bind(this, ...agrs), length - args.length) // 不滿足要求,遞迴 currying 函式,新的 fn 為 bind 返回的新函式(bind 繫結了 ...args 引數,未執行),新的 length 為 fn 剩餘引數的長度
}
}
// Test
const fn = currying(function(a,b,c) {
console.log([a, b, c])
})
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
複製程式碼
實際應用
- 延遲計算:部分求和、bind函式
延遲計算:
const add = (...args) => args.reduce((a, b) => a + b);
// 簡化寫法
function currying(func) {
const args = [];
return function result(...rest) {
if (rest.length === 0) {
return func(...args);
} else {
args.push(...rest);
return result;
}
}
}
const sum = currying(add);
sum(1,2)(3); // 未真正求值
sum(4); // 未真正求值
sum(); // 輸出 10
複製程式碼
- 動態建立函式:新增監聽addEvent、惰性函式
- 引數複用
37 - 一行程式碼將陣列扁平化並去重,最終得到升序不重複的陣列
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b}
複製程式碼
解析:
array.from - 從一個類陣列或可迭代物件建立一個新的,淺拷貝的陣列例項
array.flat - 用於將巢狀的陣列'拉平'(扁平化)
[1,2,[3,4]].flat() // [1,2,3,4]
array.sort - 用於對陣列的元素進行排序
- 該函式要比較兩個值,然後返回一個用於說明這兩個值的相對順序的數字。
比較函式應該具有兩個引數 a 和 b,其返回值如下:
- 若 a 小於 b,在排序後的陣列中 a 應該出現在 b 之前,則返回一個小於 0 的值。
- 若 a 等於 b,則返回 0
- 若 a 大於 b,則返回一個大於 0 的值。
1 - arr.flat(Infinity) 先將陣列扁平化
2 - new Set(arr.flat(Infinity)) 去重扁平化後的陣列
3 - Array.from(new Set(arr.flat(Infinity))) 建立一個新的,淺拷貝的陣列例項
4 - Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b } 將該新陣列進行升序排序
複製程式碼
38 - JS如何動態新增、移除、移動、複製、建立和查詢節點?
- 建立新節點
createDocumentFragment() - 建立一個DOM片段
createElement() - 建立一個具體的元素
createTextNode() - 建立一個文字節點
複製程式碼
- 新增、移除、替換、插入
appendChild() - 新增子節點
removeChild() - 移除子節點
replaceChild() - 替換子節點
insertBefore() - 插入
複製程式碼
- 查詢
getElementsByTagName() - 通過標籤名稱
getElementsByName() - 通過元素的Name屬性
getElementById() - 通過元素id,具有唯一性
複製程式碼
39 - Javascript是一門怎樣的語言?有什麼特點?
- 指令碼語言:
Javascript
是一種解釋型語言,C、C++
等語言先編譯後執行,而Javascript
是在程式的執行過程中逐行進行解釋 - 基於物件:
Javascript
是一種基於物件的指令碼語言,它不僅可以建立物件,也能使用現有的物件 - 簡單:
Javascript
語言中採用的是弱型別的變數型別,對使用的資料型別未做出嚴格的要求,是基於Java
基本語句和控制的指令碼語言 - 動態性:
Javascript
是一種採用事件驅動的指令碼語言,它不需要經過Web伺服器就可以對使用者的輸入做出響應 - 跨平臺性:
Javascript
不依賴於作業系統,僅需要瀏覽器的支援
40 - 相容各種瀏覽器版本的事件繫結
/**
* 相容低版本IE,element為需要繫結事件的元素,
* eventName為事件名(保持addEventListener語法,去掉on),fun為事件響應函式
*/
function addEvent(element, eventName, fun){
if(element.addEventListener){
element.addEventListener(eventName, fun, false)
}else{
element.attachEvent('on' + eventName, fun)
}
}
複製程式碼
41 - sort()排序原理是什麼?
sort()內部是利用遞迴進行氣泡排序的
- 解析氣泡排序的原理
- 比較相鄰的元素,如果第一個比第二個大,就交換它們兩個
- 對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對,在這一點,最後的元素應該會是最大的數
- 針對所有的元素重複以上的步驟,除了最後一個
- 持續每次對越來越少的元素重複上面的步驟,知道沒有任何一對數字需要比較
複製程式碼
- 示例
var arr = [1,5,4,2]
sort()的比較邏輯為:
- 1和5比,1和4比,1和2比
- 5和4比,5和2比
- 4和2比
複製程式碼
- sort()排序規則
- return > 0 則交換陣列相鄰2個元素的位置
- arr.sort(function(a,b){ ... })
- a → 代表每一次執行匿名函式時,找到陣列中的當前項
- b → 代表當前項的後一項
複製程式碼
1 - 升序
var arr = [45, 42, 10, 147, 7, 65, -74]
console.log(arr.sort()) → 預設法 缺點:預設排序順序是根據字串UniCode碼。因為排序是按照字串UniCode碼的順序進行排序的(按首位排序)
// [-74, 10, 147, 42, 45, 65, 7]
console.log(
arr.sort(function(a, b) {
return a - b; // 若return返回值大於0(即a>b),則a,b交換位置
}) → 指定排序規則,return可返回任何值
)
// [-74, 7, 10, 42, 45, 65, 147]
2 - 降序
var arr = [45, 42, 10, 111, 7, 65, -74];
console.log(
arr.sort(function(a, b) {
return b - a; // 若return返回值大於零(即b>a),則a,b交換位置
}) → 指定排序規則,return可返回任何值
);
複製程式碼
42 - 如何判斷當前指令碼執行在瀏覽器還是node環境中?
通過判斷Global物件是否為window,如果不為window,則當前指令碼沒有執行在瀏覽器中
複製程式碼
43 - 一行程式碼求陣列最大值與最小值
var a = [1, 2, 3, 5];
alert(Math.max.apply(null, a)); //最大值
alert(Math.min.apply(null, a)); //最小值
之所以需要用到apply,是因為 Math.max / Math.min 不支援傳遞陣列過去
複製程式碼
44 - offsetWidth/offsetHeight,clientWidth/clientHeight 與 scrollWidth/scrollHeight 的區別
-
offsetWidth → 返回元素的寬度(包括元素寬度、內邊距和邊框,不包括外邊距)
-
offsetHeight → 返回元素的高度(包括元素高度、內邊距和邊框,不包括外邊距)
-
clientWidth → 返回元素的寬度(包括元素寬度、內邊距,不包括邊框和外邊距)
-
clientHeight → 返回元素的高度(包括元素高度、內邊距,不包括邊框和外邊距)
-
style.width → 返回元素的寬度(包括元素寬度,不包括內邊距、邊框和外邊距)
-
style.height → 返回元素的高度(包括元素高度,不包括內邊距、邊框和外邊距)
-
scrollWidth → 返回元素的寬度(包括元素寬度、內邊距和溢位尺寸,不包括邊框和外邊距),無溢位的情況,與clientWidth相同
-
scrollHeigh → 返回元素的高度(包括元素高度、內邊距和溢位尺寸,不包括邊框和外邊距),無溢位的情況,與clientHeight相同
45 - offsetTop / offsetLeft / scrollTop / scrollLeft 的區別
-
offsetTop → 返回元素的上外緣距離最近採用定位父元素內壁的距離,如果父元素中沒有采用定位的,則是獲取上外邊緣距離文件內壁的距離。
-
offsetLeft → 此屬性和offsetTop的原理是一樣的,只不過方位不同
-
scrollLeft → 此屬性可以獲取或者設定物件的最左邊到物件在當前視窗顯示的範圍內的左邊的距離,也就是元素被滾動條向左拉動的距離。
-
scrollTop → 此屬性可以獲取或者設定物件的最頂部到物件在當前視窗顯示的範圍內的頂邊的距離,也就是元素滾動條被向下拉動的距離。
46 - Javascript中的arguments物件是什麼?
在函式呼叫的時候,瀏覽器每次都會傳遞進兩個隱式引數,一個是函式的上下文物件this
,另外一個則是封裝實參的偽陣列物件arguments
關於arguments
arguments
定義是物件,但是因為物件的屬性是無序的,而arguments
是用來儲存實參的,是有順序的,它具有和陣列相同的訪問性質及方式,並擁有陣列長度屬性length
(類陣列物件、用來儲存實際傳遞給函式的引數)arguments
訪問單個引數的方式與訪問陣列元素的方式相同,例如arguments[0]
、arguments[1]
、arguments[n]
,在函式中不需要明確指出引數名,就能訪問它們。通過length
屬性可以知道實參的個數。arguments
有一個callee
屬性,返回正被執行的Function
物件
function fun() {
console.log(arguments.callee === fun); // true
}
fun();
複製程式碼
- 在正常模式下,
arguments
物件是允許在執行時進行修改
function fun() {
arguments[0] = 'sex';
console.log(arguments[0]); // sex
}
fun('name', 'age');
複製程式碼
一行程式碼實現偽陣列arguments轉換為陣列
var args = [].slice.call(arguments)
複製程式碼
★ 47 - Js的事件迴圈(Event Loop)機制
為什麼Js是單執行緒?
Javascript
作為主要執行在瀏覽器的指令碼語言,主要用途之一就是操作Dom
如果Javascript
同時有兩個執行緒,同時對同一個Dom
進行操作,這時瀏覽器應該聽哪個執行緒的,又如何判斷優先順序?
為了避免這種問題,Javascript
必須是一門單執行緒語言
執行棧與任務佇列
由於Javascript
是單執行緒語言,當遇到非同步任務(如Ajax
)時,不可能一直等到非同步執行完成後,再繼續往下執行,因為這段時間瀏覽器處於空閒狀態,會導致巨大的資源浪費
執行棧
當執行某個函式、事件(指定過回撥函式)時,就會進入執行棧中,等待主執行緒讀取
執行棧視覺化:
主執行緒
主執行緒與執行棧不同,主執行緒規定了現在執行執行棧中的哪個事件
主執行緒迴圈: 即主執行緒會不停的從執行棧中獲取事件,然後執行完所有棧中的同步程式碼
當遇到一個非同步事件後,並不會一直等待非同步事件返回結果,而是會將這個事件掛在與執行棧不同的佇列中,這個佇列稱為任務佇列TaskQueue
當主執行緒將執行棧中的所有程式碼都執行完後,主執行緒將會去檢視任務佇列中是否存在任務。 如果存在,那麼主執行緒會依次執行那些任務佇列中的回撥函式
Javascript
非同步執行的執行機制
- 所有任務都在主執行緒上執行,形成一個執行棧
- 主執行緒之外,還存在一個任務佇列(
TaskQueue
)。只要非同步任務有了返回結果,就在任務佇列之中放置一個事件 - 當執行棧中的所有同步任務執行完畢,就會去檢視任務佇列,那些對應的非同步任務,結束等待狀態,進入執行棧並開始執行
- 主執行緒會不斷的重複第三點
巨集任務與微任務
非同步任務可以分為兩類,不同型別的API註冊的任務會依次進入到各自對應的佇列中,然後等待事件迴圈(EventLoop
)將它們依次壓入執行棧中執行
- 巨集任務
MacroTask
script(整體程式碼),setTimeout,setInterval,setImmediate,UI渲染,I/O流操作,postMessage,MessageChannel
複製程式碼
- 微任務
MicroTask
Promise,MutaionObserver,process.nextTick
複製程式碼
事件迴圈(EventLoop)
Event Loop(事件迴圈)中,每一次迴圈稱為 tick, 每一次tick的任務如下:
- 執行棧選擇最先進入佇列的巨集任務(通常是script整體程式碼),如果有則執行
- 檢查是否存在 Microtask,如果存在則不停的執行,直至清空 microtask 佇列
- 更新render(每一次事件迴圈,瀏覽器都可能會去更新渲染)
- 重複以上步驟
綜上所述
巨集任務 → 所有微任務 → 下一個巨集任務
複製程式碼
兩道題檢驗是否已經 get√
題 1:
setTimeout(function () {
console.log(1)
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val)
})
console.log(4)
複製程式碼
Result:
2 → 4 → 3 → 1
複製程式碼
題 2:
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => {
// t2
console.log(2)
});
console.log(4)
}).then(t => {
// t1
console.log(t)
});
console.log(3);
複製程式碼
Result:
4 → 3 → 2 → 1
複製程式碼
解析:
- script任務執行,首先遇到Promise例項,執行建構函式,輸出4,此時微任務佇列中有t2和t1
- script任務繼續執行同步程式碼,輸出3後第一個巨集任務執行完成
- 執行所有的微任務,即輸出2和1
??? 為什麼t2會比t1先執行 ???
- 根據 Promises/A+ 規範
- Promise.resolve 方法允許呼叫時不帶引數,直接返回一個resolved 狀態的 Promise 物件
- 立即 resolved 的 Promise 物件,是在本輪“事件迴圈”(event loop)的結束時,而不是在下一輪“事件迴圈”的開始時
複製程式碼
48 - 非同步程式設計的六種方式
Javascript
是單執行緒工作,也就是隻有一個指令碼執行完之後才可以執行下一個指令碼,兩個指令碼不能同時執行,那麼如果指令碼耗時很長,後面的指令碼都必須排隊等待,會拖延整個程式的執行
非同步程式設計的六種方式
- 回撥函式 - 假如f1是一個需要一定時間的函式,所以可以將f2寫成f1的回撥函式,將同步操作變成操作,f1不會阻塞程式的執行,f2也不需等待
function f1(cb){
setTimeout(() => {
console.log('f1')
})
cb()
}
function f2(){
console.log('f2')
}
f1(f2) // f2 → f1
複製程式碼
function fn(a,b,cb){
var num = Math.ceil(Math.random() * (a - b) + b)
cb(num)
}
fn(10,20,function(num){
console.log("隨機數" + num);
}) // 10 ~ 20 的隨機數
複製程式碼
總結:
- 回撥函式易於實現,便於理解,但是多次回撥會導致程式碼高度耦合
- 回撥函式定義:函式A作為引數(函式引用)傳遞到另外一個函式B,並且這個函式B執行函式A,我們就叫函式A叫做回撥函式,如果沒有名稱(函式表示式),我們就叫它匿名回撥函式
- 回撥函式優點:簡單,容易理解
- 回撥函式缺點:不利於程式碼的閱讀和維護,各部分之間高度耦合,而且每一個任務只能指定一個回撥函式
複製程式碼
- 事件監聽 - 採用事件驅動模式,指令碼的執行不取代程式碼的順序,而取決於某一個事件是否發生
監聽函式有:on、bind、listen、addEventListener、observe
複製程式碼
優點:容易理解,可以繫結多個事件,每一個事件可以接收多個回撥函式,而且可以減少耦合,利於模組化
缺點:整個程式都要變成事件驅動型,執行流程會變得不清晰
複製程式碼
element.onclick = function(){
// toDo
}
Or:
element.onclick = handler1
element.onclick = handler2
element.onclick = handler3
缺點:
當同一個element元素繫結多個事件時,只有最後一個事件會被新增,上述只有handler3會被新增執行
複製程式碼
elment.attachEvent("onclick", handler1)
elment.attachEvent("onclick", handler2)
elment.attachEvent("onclick", handler3)
Result: 3 → 2 → 1
elment.addEventListener("click", handler1, false)
elment.addEventListener("click", handler2, false)
elment.addEventListener("click", handler3, false)
Result:1 → 2 → 3
(PS:該方法的第三個引數是泡沫獲取,是一個布林值:當為false時表示由裡向外,true表示由外向裡。)
複製程式碼
DOM - addEventListener()和removeListener()
addEventListenner()和removeListenner()表示用來分配和刪除事件的函式。這兩種方法都需要三種引數,分別為:
- 事件名稱(String)
- 觸發事件的回撥函式(function)
- 指定事件的處理函式的時期或階段(boolean)
複製程式碼
- 觀察者模式(Observe) - 也稱為釋出訂閱模式
定義了一種一對多的關係,讓多個觀察者同時監聽某一個主題物件,這一個主題物件一旦發生狀態變化,就會通知所有觀察者物件,使得它們能夠自動更新自己
優點:
- 支援簡單的廣播通訊,自動通知所有已經訂閱過的物件
- 頁面載入後,目標物件很容易與觀察者存在一種動態關聯,增加靈活性
- 目標物件與觀察者之間的抽象耦合關係能夠單獨擴充套件以及重用
複製程式碼
- Promise
- promise物件是commonJS工作組提出的一種規範,一種模式,目的是為了非同步程式設計提供統一介面
- promise是一種模式,promise可以幫忙管理非同步方式返回的程式碼。他將程式碼進行封裝並新增一個類似於事件處理的管理層。我們可以使用promise來註冊程式碼,這些程式碼會在在promise成功或者失敗後執行
- promise完成之後,對應的程式碼也會執行。我們可以註冊任意數量的函式再成功或者失敗後執行,也可以在任何時候註冊事件處理程式
- promise有兩種狀態:1、等待(pending);2、完成(settled)
- promise會一直處於等待狀態,直到它所包裝的非同步呼叫返回/超時/結束
- 這時候promise狀態變成完成。完成狀態分成兩類:1、解決(resolved);2、拒絕(rejected)
- promise解決(resolved):意味著順利結束。promise拒絕(rejected)意味著沒有順利結束
複製程式碼
- Generator - Generator函式是一個狀態機,封裝了多個內部狀態
- async
49 - 同源策略
Javascript
只能與同一個域中的頁面進行通訊
兩個指令碼被認為是同源的條件:
- 協議相同
- 埠相同
- 域名相同
50 - jsonP的優缺點
- 優點
- 它不像
XMLHttpRequest
物件實現的AJAX請求那樣受到同源策略的限制,jsonP可以實現跨越同源策略 - 它的相容性更好,在更加古老的瀏覽器中都可以執行,不需要
XMLHttpRequest
或ActiveX
的支援 - 在請求完畢後可以通過呼叫
callback
的方式回傳結果。將回撥方法的許可權給了呼叫方。
- 它不像
- 缺點
- 它只支援
GET
請求而不支援POST
等其他型別的HTTP請求 - 它只支援跨域HTTP請求這種情況,不能解決不同域的兩個頁面之間如何進行JavaScript呼叫問題
- JsonP在呼叫失敗的時候不會返回各種http狀態碼
- 缺點是安全性。萬一假如提供JsonP的服務存在頁面注入漏洞(即它所返回的javascript的內容被人控制),那麼結果是什麼?所有呼叫這個JsonP的網站都會存在漏洞,缺乏安全。
- 它只支援
★ 51 - AJAX
什麼是AJAX,為什麼使用AJAX?
AJAX
是一種建立互動式網頁應用的網頁開發技術AJAX
可以實現在不必重新整理整個頁面的情況下實現區域性更新,與伺服器進行非同步通訊的技術
XMLHttpRequest
物件
XMLHttpRequest
物件可以說是AJAX
的核心物件,是一種支援非同步請求的技術。即XMLHttpRequest
使你可以使用javascript
向伺服器提出請求並做出響應,又不會導致阻塞使用者。通過XMLHttpRequest
物件,可以實現在不重新整理整個頁面的情況下實現區域性更新
XMLHttpRequest
物件的常見屬性
onreadystatechange
- 一個Js函式物件,當readyState屬性改變時會呼叫它(請求狀態改變的事件觸發器)readyState
- Http請求的狀態,當一個XMLHttpRequest
初次建立時,這個屬性的值從0開始,直到接收到完整的Http響應,這個值增加到4- 0 - 初始化狀態。
XMLHttpRequest
物件已建立或已被abort()
方法重置。 - 1 -
open()
方法已呼叫,但是send()
方法未呼叫。請求還沒有被髮送 - 2 -
send()
方法已呼叫,HTTP 請求已傳送到 Web 伺服器,但未接收到響應 - 3 - 所有響應頭部都已經接收到,響應體開始接收但未完成
- 4 - Http響應已經完全接收
- 0 - 初始化狀態。
readyState 的值不會遞減,除非當一個請求在處理過程中的時候呼叫了 abort() 或 open() 方法
每次這個屬性的值增加的時候,都會觸發 onreadystatechange 事件控制程式碼。
複製程式碼
status
- 由伺服器返回的 HTTP 狀態程式碼,如 200 表示成功,而 404 表示 "Not Found" 錯誤。當 readyState 小於 3 的時候讀取這一屬性會導致一個異常。
關於Http狀態碼,常見如下:
1) 1XX 通知
2) 2XX 成功
3) 3XX 重定向
4) 4XX 客戶端錯誤
5) 5XX 服務端錯誤
最基本的響應狀態:
- 200('ok') : 伺服器已成功處理了請求
- 400('bad request'):伺服器無法解析該請求
- 500('Internal Server Error'):伺服器內部錯誤伺服器遇到錯誤,無法完成請求
- 301('Moved Permanently'):永久移動請求的網頁已永久移動到新位置,即永久重定向
- 404('Not Found'):未找到伺服器找不到請求的網頁
- 409('Conflict'):伺服器在完成請求時發生衝突
複製程式碼
XMLHttpRequest
物件的常見API
Open()
- 建立http請求- 第一個引數:定義請求的方式(get/post)
- 第二個引數:提交的地址
url
- 第三個引數:指定非同步/同步(true → 非同步,false → 同步)
- 第四第五個引數:http認證
在一個已經啟用的request下(已經呼叫open()或者openRequest()方法的請求)再次呼叫這個方法相當於呼叫了abort()方法。
複製程式碼
setRequestHeader()
- 向一個開啟但未傳送的請求設定或新增一個Http請求(設定請求頭)- 第一個引數:將要被賦值的請求頭名稱(header)
- 第二個引數:給指定的請求頭賦值(value)
send()
- 傳送http請求,使用傳遞給open()方法的引數,以及傳遞給該方法的可選請求體- 如果為get,引數為null / 如果為post,引數為提交的引數
abort()
- 取消當前響應getAllResponseHeaders()
- 把Http響應頭部作為未解析的字串返回getResponseHeader()
- 返回指定的 HTTP 響應頭部的值- 其引數是要返回的 HTTP 響應頭部的名稱。可以使用任何大小寫來制定這個頭部名字,和響應頭部的比較是不區分大小寫的
AJAX的流程是怎麼樣的?
- 建立
XMLHttpRequest
物件 - 定義
Http
物件 - 可以設定
Http
請求的請求頭 - 設定響應狀態改變的事件回撥函式
- 傳送請求
- 獲取非同步呼叫返回的資料
- 使用Js和DOM進行區域性解析
原生實現一個Ajax
var ajax = {}
// 相容性建立httpRequest
ajax.httpRequest = function(){
// 判斷是否支援XMLHttpRequest
if(window.XMLHttpRequest){
return new XMLHttpRequest()
}
// 相容 Ie
var versions = [
"MSXML2.XmlHttp.6.0",
"MSXML2.XmlHttp.5.0",
"MSXML2.XmlHttp.4.0",
"MSXML2.XmlHttp.3.0",
"MSXML2.XmlHttp.2.0",
"Microsoft.XmlHttp"
]
// 定義區域性xhr,儲存Ie瀏覽器的ActiveXObject物件
var xhr
for (var i = 0; i < versions.length; i++) {
try {
xhr = new ActiveXObject(versions[i]);
break;
} catch (e) {
}
}
return xhr
}
ajax.send = function(url, callback, method, data, async){
// 預設非同步
if(async === undefined){
async = true
}
var httpRequest = ajax.httpRequest()
// 建立Http請求(open)
httpRequest.open(method, url, async)
// 請求狀態改變的事件觸發器
httpRequest.onreadystatechange = function(){
// readyState變為4時,從伺服器拿到資料
if(httpRequest.readyState === 4){
callback(httpRequest.responseText)
}
}
// 設定http請求的請求頭(setRequestHeader)
if (method == 'POST') {
//給指定的HTTP請求頭賦值
httpRequest.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
}
// 傳送Http請求
httpRequest.send(data)
}
// 封裝GET/POST請求
ajax.get = function (url, data, callback, async) {
var query = [];
for (var key in data) {
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
}
ajax.send(url + (query.length ? '?' + query.join('&') : ''), callback, 'GET', null, async)
}
ajax.post = function (url, data, callback, async) {
var query = [];
for (var key in data) {
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
}
ajax.send(url, callback, 'POST', query.join('&'), async)
}
複製程式碼
52 - 如何解決跨域問題?
- CORS → 服務端設定請求頭(
Access-Control-Allow-Origin
) - jsonP → 動態載入
<script>
標籤(只能解決GET請求) - window.name → 利用瀏覽器視窗內,載入所有的域名都是共享一個window.name
- ducment.domain / window.postMessage()
53 - document.Ready()、onload、前寫JS有什麼區別?
document.Ready()
可以定義多個函式,按照繫結的順序進行執行,而onload
只能執行一次,定義多個onload
,後面的會覆蓋前面的onload()
是在頁面所有元素都載入完後才執行document.Ready()
是在DOM繪製完就執行,不必等到完全載入完再執行</body>
前寫Js則是執行到就開始執行,不管有沒有載入完成,所以有可能出現Js操作節點時獲取不到該節點
54 - Object.freeze()有什麼用?與const有什麼區別?
Object.freeze
適用於物件值,它能夠讓物件不可變(該物件屬性不能修改)
let foo = {
a:'A'
}
let bar = {
b:'B'
}
Object.freeze(foo)
foo.a = 'a'
console.log(foo) // {a: "A"}
複製程式碼
相比const
,兩者是不同的概念,const
作用是宣告變數(一個只讀變數),一旦宣告,這個變數就不能修改,而Object.freeze()
作用是讓一個物件不可變
55 - 細品new
new
- 配合建構函式建立物件
function Person(name, age, job){
this.name = name
this.age = age
this.job = job
}
var person = new Person('CHICAGO', 21, 'itMan')
複製程式碼
- 通過例子細品
new
在建立物件的過程中做了哪4件事
function Person(){
this.name = 'CHICAGO'
}
new Person()
- 建立一個空物件 → var obj = {}
- 將空物件賦給this → this = obj
- 將空物件的 __proto__ 屬性指向建構函式的 prototype → this.__proto__ = Person().prototype
- 返回這個物件(this)→ return this
複製程式碼
- 總結
- 建立空物件
{}
- 將空物件分配給
this
值 - 將空物件的
__proto__
指向建構函式的prototype
- 如果沒有使用顯式
return
語句,則返回this
- 建立空物件
56 - in 運算子和 Object.hasOwnProperty 方法有什麼區別?
hasOwnProperty()
- 返回一個布林值,判斷物件是否包含特定的自身(非繼承)屬性
判斷自身屬性是否存在
Object.prototype.c= 'C';
var obj = new Object()
obj.a = 'A'
function changeObj(){
obj.b = 'B'
delete obj.a
}
obj.hasOwnProperty('a') // true
obj.hasOwnProperty('c') // false
changeObj()
obj.hasOwnProperty('a') // false
obj.hasOwnProperty('b') // false
複製程式碼
如果在函式原型上定義一個變數,hasOwnProperty()方法會直接忽略掉
in
運算子
如果指定的屬性在指定的物件或其原型鏈中,則in
運算子返回true
Object.prototype.c= 'C';
var obj = new Object()
obj.a = 'A'
console.log('a' in obj) // true
console.log('c' in obj) // true
複製程式碼
in
運算子會檢查它或者其原型鏈是否包含具有指定名稱的屬性
57 - 如何建立一個沒有原型(prototype)的物件?
- 通過
Object.create()
可以實現建立沒有原型的物件
const objHavePrototype = {}
console.log(objHavePrototype.toString()) // [Object object]
const objHaveNoPrototype = Object.create(null)
console.log(objHaveNoPrototype.toString) // TypeError: objHaveNoPrototype.toString is not a function
typeof objHaveNoPrototype // object
複製程式碼
我們知道 typeof null === 'object'
,但 null 並沒有 prototype 屬性
58 - 如何判斷一個元素是否使用了event.preventDefault()
- 通過在事件物件中使用
event.defaultPrevented
屬性,該屬性返回一個布林值用於區分是否在特定元素中使用了event.preventDefault()
59 - 訪問不存在的屬性,為什麼有時返回undefined
,有時卻是報錯
var foo = {}
console.log(foo.a) // undefined
console.log(foo.a.A) // TypeError: Cannot read property 'A' of undefined
複製程式碼
觀察上面這個例子,有人會認為都是返回undefined
或者都是報錯,當我們訪問foo.a
的時候,由於foo物件並不存在a屬性,所以返回的是undefined,而當我們去訪問一個undefined的屬性時,就會報出TypeError: Cannot read property 'XXX' of undefined
的錯誤
60 - 為什麼 0.1 + 0.2 != 0.3 ? 如何解決這個問題 ?
由於計算機是通過二進位制來儲存東西,那麼0.1
在二進位制中會表示為
// (0011) 表示迴圈
0.1 = 2^-4 * 1.10011(0011)
複製程式碼
可以發現,0.1
在二進位制中是一個無限迴圈的數字,並不是精確的0.1
,其實很多十進位制小數用二進位制表示都會是無限迴圈的,因為Javascript
採用浮點數標準,導致會裁剪掉我們的數字,那麼這些迴圈的數字被裁剪之後,就會出現精度丟失的問題,也就造成0.1
不再是0.1
,而是變成0.100000000000000002
0.100000000000000002 === 0.1 // true
複製程式碼
自然,0.2
在二進位制中也是無限迴圈,所以
0.1 + 0.2 === 0.30000000000000004 // true
複製程式碼
解決 0.1 + 0.2 != 0.3
parseFloat(str)
- 解析一個字串,並返回一個浮點數- 該函式指定字串中的首個字元是否是數字。如果是,則對字串進行解析,直到到達數字的末端為止,然後以數字返回該數字,而不是作為字串
- str - 必需,要被解析的字串
toFixed(num)
- 把Number四捨五入為指定小數位數的數字- num - 必需,規定小數的位數(0 ~ 20)
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
複製程式碼
溫馨提示?
- 由於
Javascript
內容較多,本章列舉了較為重要的部分,個人會繼續總結知識,對該章持續更新,後續會總結Js
重點手寫題,建議對本文進行收藏 - 下一期 - 總結
ES6
核心知識點