[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. 執行上下文
- 全域性執行上下文
- 函式執行上下文
- 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。複製程式碼