javaScript ES5常考面試題總結


js的六種原始值

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol

坑1:
首先原始類型存儲的都是值,是沒有函數可以調用的,比如 undefined.toString() 會報錯
但是 '1'.toString()是可以調用的,因為已經轉換成了對應的對象類型了。

坑2:
number的類型 0.1 + 0.2 !== 0.3

坑3:
對於 null 來說,很多人會認為他是個對象類型,其實這是錯誤的。雖然 typeof null 會輸出 object,但是這只是 JS 存在的一個悠久 Bug。在 JS 的最初版本中使用的是 32 位系統,為了性能考慮使用低位存儲變量的類型信息,000 開頭代表是對象,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部類型判斷代碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。

對象(Object)類型

在 JS 中,除了原始類型那么其他的都是對象類型了。對象類型和原始類型不同的是,原始類型存儲的是值,對象類型存儲的是地址(指針)。當你創建了一個對象類型的時候,計算機會在內存中幫我們開辟一個空間來存放值,但是我們需要找到這個空間,這個空間會擁有一個地址(指針)。

const a = []; // 對於常量 a 來說,假設內存地址(指針)為 #001,那么在地址 #001 的位置存放了值 [],常量 a 存放了地址(指針) #001

const b = 1;

b.push(a); // 當我們將變量賦值給另外一個變量時,復制的是原本變量的地址(指針),
也就是說當前變量 b 存放的地址(指針)也是 #001,
當我們進行數據修改的時候,就會修改存放在地址(指針) #001 上的值,也就導致了兩個變量的值都發生了改變。

typeof vs instanceof

** 涉及面試題:typeof 是否能正確判斷類型?instanceof 能正確判斷對象的原理是什么?

typeof 對於原始類型來說,除了 null 都可以顯示正確的類型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof 對於對象來說,除了函數都會顯示 object,所以說 typeof 並不能准確判斷變量到底是什么類型

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function

如果我們想判斷一個對象的正確類型,這時候可以考慮使用 instanceof,因為內部機制是通過原型鏈來判斷的

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var str = 'hello world'
str instanceof String // false

var str1 = new String('hello world')
str1 instanceof String // true

todo: 類型判斷再去深入學習。

類型轉換

首先我們要知道,在 JS 中類型轉換只有三種情況,分別是:

轉換為布爾值
轉換為數字
轉換為字符串

【對象轉原始類型】
對象在轉換類型的時候,會調用內置的 [[ToPrimitive]] 函數,對於該函數來說,算法邏輯一般來說如下:

如果已經是原始類型了,那就不需要轉換了
調用 x.valueOf(),如果轉換為基礎類型,就返回轉換的值
調用 x.toString(),如果轉換為基礎類型,就返回轉換的值
如果都沒有返回原始類型,就會報錯

當然你也可以重寫 Symbol.toPrimitive ,該方法在轉原始類型時調用優先級最高。

let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  },
  [Symbol.toPrimitive]() {
    return 2
  }
}
1 + a // => 3

四則運算符

加法運算符不同於其他幾個運算符,它有以下幾個特點:

運算中其中一方為字符串,那么就會把另一方也轉換為字符串
如果一方不是字符串或者數字,那么會將它轉換為數字或者字符串

1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3" 數組 -> 字符串 的轉換

經典考題:

'a' + + 'b' // -> "aNaN"
因為 + 'b' 等於 NaN,所以結果為 "aNaN",你可能也會在一些代碼中看到過 + '1' 的形式來快速獲取 number 類型。

比較運算符

如果是對象,就通過 toPrimitive 轉換對象
如果是字符串,就通過 unicode 字符索引來比較

let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  }
}
a > -1 // true
// 在以上代碼中,因為 a 是對象,所以會通過 valueOf 轉換為原始類型再比較值。

this

1、誰調用this,this就是誰
2、箭頭函數沒有this,另外對箭頭函數使用 bind 這類函數是無效的

function foo() {
  console.log(this.a)
}
var a = 1
foo() // 這里調用相當於 window.foo() 所以 this則是window 那么window.a = 1

const obj = {
  a: 2,
  foo: foo
}
obj.foo() // this = obj 所以 obj.a = 2 所以這里輸出2

const c = new foo() // 對於 new 的方式來說,this 被永遠綁定在了 c 上面,不會被任何方式改變 this

bind

原則:對於這些函數來說,this 取決於第一個參數,如果第一個參數為空,那么就是 window。

let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => 根據規則 打印出來的就是window

== vs ===

== 會進行類型轉換
=== 不會進行類型轉換

平時使用還是強烈建議使用===進行嚴格的判斷

閉包

閉包的定義其實很簡單:函數 A 內部有一個函數 B,函數 B 可以訪問到函數 A 中的變量,那么函數 B 就是閉包。

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

在 JS 中,閉包存在的意義就是讓我們可以間接訪問函數內部的變量。

通過閉包解決for循環的訪問

for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)// 1 ,2 ,3 ,4 ,5
    }, j * 1000)
  })(i)
}

另外一個方案

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i) // 輸出 1,2,3,4,5
  }, i * 1000)
}

上面代碼中,變量i是let聲明的,當前的i只在本輪循環有效,所以每一次循環的i其實都是一個新的變量,所以最后輸出的是6。你可能會問,如果每一輪循環的變量i都是重新聲明的,那它怎么知道上一輪循環的值,從而計算出本輪循環的值?這是因為 JavaScript 引擎內部會記住上一輪循環的值,初始化本輪的變量i時,就在上一輪循環的基礎上進行計算。

深淺拷貝

之前,我們了解了對象類型在賦值的過程中其實是復制了地址,從而會導致改變了一方其他也都被改變的情況。通常在開發中我們不希望出現這樣的問題,我們可以使用淺拷貝來解決這個情況。

let a = {
  age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

【淺拷貝】
首先可以通過 Object.assign 來解決這個問題,很多人認為這個函數是用來深拷貝的。其實並不是,Object.assign 只會拷貝所有的屬性值到新的對象中,如果屬性值是對象的話,拷貝的是地址,所以並不是深拷貝。

let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

另外我們還可以通過展開運算符 ... 來實現淺拷貝

let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

通常淺拷貝就能解決大部分問題了,但是當我們遇到如下情況就可能需要使用到深拷貝了

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native

淺拷貝只解決了第一層的問題,如果接下去的值中還有對象的話,那么就又回到最開始的話題了,兩者享有相同的地址。要解決這個問題,我們就得使用深拷貝了。

【深拷貝】
這個問題通常可以通過 JSON.parse(JSON.stringify(object)) 來解決

但是該方法也是有局限性的:

會忽略 undefined
會忽略 symbol
不能序列化函數
不能解決循環引用的對象

let a = {
  age: undefined,
  sex: Symbol('male'),
  jobs: function() {},
  name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

看輸出結果忽略了undefined , symbol 和 函數

實現一個真正的深拷貝(簡易版)

function deepClone(obj) {
    // 判斷是不是對象
  function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
  }
    // 不是對象的話報錯
  if (!isObject(obj)) {
    throw new Error('非對象')
  }
    
  let isArray = Array.isArray(obj)
  let newObj = isArray ? [...obj] : { ...obj } // 是數組的話 用數組的解構來實現對象的復制,對象也同理
// 靜態方法 Reflect.ownKeys() 返回一個由目標對象自身的屬性鍵組成的數組
  Reflect.ownKeys(newObj).forEach(key => {
    newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] // 這里進行遞歸實現多層對象的復制
  })

  return newObj
}

原型

當我們創建一個對象時 let obj = { age: 25 },我們可以發現能使用很多種函數,但是我們明明沒有定義過它們,對於這種情況你是否有過疑惑?

其實每個 JS 對象都有 proto 屬性,這個屬性指向了原型。這個屬性在現在來說已經不推薦直接去使用它了,這只是瀏覽器在早期為了讓我們訪問到內部屬性 [[prototype]] 來實現的一個東西。

其實原型鏈就是多個對象通過 proto 的方式連接了起來。為什么 obj 可以訪問到 valueOf 函數,就是因為 obj 通過原型鏈找到了 valueOf 函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM