1. 程式人生 > >深入瞭解JavaScript底層原理

深入瞭解JavaScript底層原理

[TOC]

1. 七種內建型別

基本型別: 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. 執行上下文

  1. 全域性執行上下文
  2. 函式執行上下文
  3. eval 執行上下文

屬性 VO & AO

變數物件 (縮寫為VO)就是與執行上下文相關的物件,它儲存下列內容:

  1. 變數 (var, VariableDeclaration);
  2. 函式宣告 (FunctionDeclaration, 縮寫為FD);
  3. 函式的形參
  • 只有全域性上下文的變數物件允許通過VO的屬性名稱間接訪問(因為在全域性上下文裡,全域性物件自身就是一個VO(稍後會詳細介紹)。在其它上下文中是不可能直接訪問到VO的,因為變數物件完全是實現機制內部的事情。當我們宣告一個變數或一個函式的時候,同時還用變數的名稱和值,在VO裡建立了一個新的屬性。

啟用物件是函式上下文裡的啟用物件AO中的內部物件,它包括下列屬性:

  1. callee — 指向當前函式的引用;
  2. length —真正傳遞的引數的個數;
  3. 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。複製程式碼