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 函數。