深入瞭解JavaScript底層原理
基本型別: null,undefined,boolean,number(浮點型別),string,symbol(es6)。 物件:Object。 複製程式碼
型別轉換
- typeof:
typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 沒有宣告,但是還會顯示 undefined typeof []// 'object' typeof {}// 'object typeof null// 'object' typeof console.log // 'function' 複製程式碼
-
valueOf
物件在轉換基本型別時,首先會呼叫 valueOf 然後呼叫 toString。並且這兩個方法你是可以重寫的。
let a = { valueOf() { return 0 toString() { return '1'; }, // Symbol.toPrimitive ,該方法在轉基本型別時呼叫優先順序最高。 [Symbol.toPrimitive]() { return 2; } } 1 + a // => 3 '1' + a // => '12' 複製程式碼
- 比較運算子
如果是物件,就通過 toPrimitive 轉換物件 如果是字串,就通過 unicode 字元索引來比較 複製程式碼
四則運算
只有當加法運算時,其中一方是字串型別,就會把另一個也轉為字串型別。 其他運算只要其中一方是數字,那麼另一方就轉為數字。 並且加法運算會觸發三種類型轉換:將值轉換為原始值,轉換為數字,轉換為字串。 複製程式碼
1 + '1' // '11' 2 * '2' // 4 [1, 2] + [2, 1] // '1,22,1' // [1, 2].toString() -> '1,2' // [2, 1].toString() -> '2,1' // '1,2' + '2,1' = '1,22,1' // 對於加號需要注意這個表示式 'a' + + 'b' 'a' + + 'b' // -> "aNaN" // 因為 + 'b' -> NaN 複製程式碼
冷知識
- NaN 屬於 number 型別,並且 NaN 不等於自身。
- undefined 不是保留字,能夠在低版本瀏覽器被賦值 let undefined = 1
2. 例項物件
new
- 在呼叫 new 的過程中會發生以上四件事情
// 新生成了一個物件 // 連結到原型 // 繫結 this // 返回新物件 function new() { // 建立一個空的物件 let obj = new Object() // 獲得建構函式 let Con = [].shift.call(arguments) // 連結到原型 obj.__proto__ = Con.prototype // 繫結 this,執行建構函式 let result = Con.apply(obj, arguments) // 確保 new 出來的是個物件 return typeof result === 'object' ? result : obj } 複製程式碼
- 執行優先順序
function Foo() { return this; } Foo.getName = function () { console.log('1'); }; Foo.prototype.getName = function () { console.log('2'); }; new Foo.getName();// -> 1 new Foo().getName(); // -> 2 // new Foo() 的優先順序大於 new Foo 複製程式碼
new (Foo.getName()); (new Foo()).getName(); // 對於第一個函式來說,先執行了 Foo.getName() ,所以結果為 1; // 對於後者來說,先執行 new Foo() 產生了一個例項, // 然後通過原型鏈找到了 Foo 上的 getName 函式,所以結果為 2。 複製程式碼
this
- 通用規則 new有最高優先順序,利用 call,apply,bind 改變 this,優先順序僅次於 new。
function foo() { console.log(this.a) } var a = 1 foo() var obj = { a: 2, foo: foo } obj.foo() // 以上兩者情況 `this` 只依賴於呼叫函式前的物件,優先順序是第二個情況大於第一個情況 // 以下情況是優先順序最高的,`this` 只會繫結在 `c` 上,不會被任何方式修改 `this` 指向 var c = new foo() c.a = 3 console.log(c.a) // 還有種就是利用 call,apply,bind 改變 this,這個優先順序僅次於 new 複製程式碼
- 箭頭函式其實是沒有 this 的,這個函式中的 this 只取決於他外面的第一個不是箭頭函式的函式的 this。在這個例子中,因為呼叫 a 符合前面程式碼中的第一個情況,所以 this 是 window。並且 this 一旦綁定了上下文,就不會被任何程式碼改變。
冷知識
- instanceof 可以正確的判斷物件的型別,因為內部機制是通過判斷物件的原型鏈中是不是能找到型別的 prototype。
3. 執行上下文
- 全域性執行上下文
- 函式執行上下文
- eval 執行上下文
屬性 VO & AO
變數物件 (縮寫為VO)就是與執行上下文相關的物件,它儲存下列內容:
- 變數 (var, VariableDeclaration);
- 函式宣告 (FunctionDeclaration, 縮寫為FD);
- 函式的形參
- 只有全域性上下文的變數物件允許通過VO的屬性名稱間接訪問(因為在全域性上下文裡,全域性物件自身就是一個VO(稍後會詳細介紹)。在其它上下文中是不可能直接訪問到VO的,因為變數物件完全是實現機制內部的事情。當我們宣告一個變數或一個函式的時候,同時還用變數的名稱和值,在VO裡建立了一個新的屬性。
啟用物件是函式上下文裡的啟用物件AO中的內部物件,它包括下列屬性:
- callee — 指向當前函式的引用;
- length —真正傳遞的引數的個數;
- properties-indexes(字串型別的整數)
- 屬性的值就是函式的引數值(按引數列表從左到右排列)。 properties-indexes內部元素的個數等於arguments.length. properties-indexes 的值和實際傳遞進來的引數之間是共享的。(譯者注:共享與不共享的區別可以對比理解為引用傳遞與值傳遞的區別)
屬性 this&作用域鏈
b() // call b console.log(a) // undefined var a = 'Hello world' function b() { console.log('call b') } 複製程式碼
-
以上眾所周知因為函式和變數提升的原因。通常提升的解釋是說將宣告的程式碼移動到了頂部。但是更準確的解釋應該是:在生成執行上下文時,會有兩個階段。第一個階段是建立的階段(具體步驟是建立 VO),JS直譯器會找出需要提升的變數和函式,並且給他們提前在記憶體中開闢好空間,函式的話會將整個函式存入記憶體中,變數只宣告並且賦值為 undefined,所以在第二個階段,也就是程式碼執行階段,我們可以直接提前使用。
-
在提升的過程中,相同的函式會覆蓋上一個函式,並且函式優先於變數提升
b() // call b second function b() { console.log('call b fist') } function b() { console.log('call b second') } var b = 'Hello world' 複製程式碼
- 對於非匿名的立即執行函式需要注意以下一點
var foo = 1 (function foo() { foo = 10 console.log(foo) }()) // -> ƒ foo() { foo = 10 ; console.log(foo) } // 內部獨立作用域,不會影響外部的值 複製程式碼
一個面試題
迴圈中使用閉包解決 var 定義函式的問題
for ( var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } // 因為 setTimeout 是個非同步函式,所有會先把迴圈全部執行完畢,這時候 i 就是 6 了,所以會輸出一堆 6。 複製程式碼
解決辦法
第一種使用閉包
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i); } 複製程式碼
第二種就是使用 setTimeout 的第三個引數
for ( var i=1; i<=5; i++) { setTimeout( function timer(j) { console.log( j ); }, i*1000, i); } // 第三個引數及以後的引數都可以作為func函式的引數,例: function a(x, y) { console.log(x, y) // 2 3 } setTimeout(a, 1000, 2, 3) 複製程式碼
第三種就是使用 let 定義 i 了
for ( let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } 複製程式碼
因為對於 let 來說,他會建立一個塊級作用域,相當於
{ // 形成塊級作用域 let i = 0 { let ii = i setTimeout( function timer() { console.log( i ); }, i*1000 ); } i++ { let ii = i } i++ { let ii = i } ... } 複製程式碼
4. 深淺拷貝
淺拷貝
- 通過 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 複製程式碼
- 弊端:淺拷貝只解決了第一層的問題。如果接下去的值中還有物件的話,那麼就又回到剛開始的話題了,兩者享有相同的引用。要解決這個問題,我們需要引入深拷貝。
深拷貝
- 通過 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 複製程式碼
該方法也是有侷限性的:會忽略 undefined,忽略函式,不能解決迴圈引用的物件
let obj = { a: 1, b: { c: 2, d: 3, }, } obj.c = obj.b obj.e = obj.a obj.b.c = obj.c obj.b.d = obj.b obj.b.e = obj.b.c let newObj = JSON.parse(JSON.stringify(obj)) // 會報錯 console.log(newObj) 複製程式碼
- 如果你的資料中含有以上三種情況下,通過lodash 的深拷貝函式,或者使用 MessageChannel
function structuralClone(obj) { return new Promise(resolve => { const {port1, port2} = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); }); } var obj = {a: 1, b: { c: b }} // 注意該方法是非同步的 // 可以處理 undefined 和迴圈引用物件 const clone = await structuralClone(obj); 複製程式碼
文章為學習筆記,整理自面譜InterviewMap。複製程式碼