最近我看了 You-Dont-Know-JS 的兩個小冊,在看書的過程中,為了方便以後索引與更深入的瞭解,也為了避免遺忘,我對每一冊的較為複雜的點做了總結,編輯如下。
本文地址: blog.xiange.tech/post/js-puz…
types & grammer
-
判斷以下結果
var s = 'abc'; s[1] = 'B'; console.log(s); var l = new String('abc'); l[1] = 'B'; console.log(l); 複製程式碼
string
及其包裝物件 (Boxed Object) 是不可變 (immutable) 型別,因此不能改變它本身(modify in place),所以String
的所有方法都是返回一個新的字串,而不會改變自身。 -
如何逆序一個字串?
s.split('').reverse().join('')
-
接上,為什麼不能直接使用
Array.prototype.reverse.call(s)
逆序字串?當一個陣列逆序時
l.reverse()
會改變 l 本身。正如第一題,string
不能改變自身。 -
判斷以下結果,為什麼會出現這樣的情況,如何做出正確的比較?
0.1 + 0.2 === 0.3; 0.8 - 0.6 === 0.2; 複製程式碼
浮點數根據 IEEE 754 標準儲存64 bit 雙精度,能夠表示 2^64 個數,而浮點數是無窮的,代表有些浮點數必會有精度的損失,0.1,0.2 表示為二進位制會有精度的損失。比較時引入一個很小的數值
Number.EPSILON
容忍誤差,其值為2^-52
。function equal (a, b) { return Math.abs(a - b) < Number.EPSILON } 複製程式碼
-
如何判斷一個數值為整數?
// ES6 Number.isInteger(num); // ES5 if (!Number.isInteger) { Number.isInteger = function(num) { return typeof num == "number" && num % 1 == 0; }; } 複製程式碼
-
如何判斷一個數值為 +0?
function isPosZero (n) { return n === 0 && 1 / n === Infinity } 複製程式碼
-
'abc'.toUpperCase()
中 'abc' 作為 primitive value,如何訪問toUpperCase
方法當
primitive value
訪問屬性或者方法時,會自動轉化為它的包裝物件。另外也可以使用Object.prototype.valueOf()
解包裝(Unboxing)。 -
判斷以下結果 (Boxing Wrappers)
function foo() { console.log(this) } foo.call(3); 複製程式碼
Number(3)。理由如上。
-
判斷以下結果
Array.isArray(Array.prototype) 複製程式碼
true 內建物件的 prototype 都不是純物件,比如
Date.prototype
是 Date,Set.prototype
是 Set。 -
判斷以下結果
Boolean(new Boolean(false)); Boolean(document.all); [] == ''; [3] == 3; [] == false; 42 == true; 複製程式碼
new Boolean() 返回 object,為 true document.all,歷史問題,參考這裡 Falsy value 指會被強制轉化為 false 的值,有以下五種。除此之外全部會轉化為 true
- undefined
- null
- false
- +0, -0, and NaN
- ""
-
找出以下程式碼問題 (TDZ)
var a = 3; let a; 複製程式碼
這是暫時性死域(Temporal Dead Zone)的問題,let a 宣告之前,不能使用 a。
-
找出以下程式碼問題 (TDZ)
var x = 3; function foo (x=x) { // .. } foo() 複製程式碼
同樣,在函式預設引數中,也有 TDZ。
scope & closures
-
var a = 2
中,Engine
,Scope
,Compiler
做了什麼工作 -
判斷以下結果 (Lexical Scope)
var scope = 'global scope'; function checkScope () { var scope = 'local scope'; function f() { return scope; } return f; } checkScope()(); 複製程式碼
'local scope'
由於 js 為詞法作用域(Lexical Scope),訪問某個變數時,先在當前作用域中查詢,如果查詢不到則在巢狀作用域中查詢,直到找到。如果找不到,則報
ReferenceError
。 -
判斷以下結果 (Hoisting)
console.log(a); var a = 3; 複製程式碼
undefined
以上程式碼會被編譯器理解為
var a; console.log(a); a = 3; 複製程式碼
-
判斷以下結果 (Function First)
var foo = 1; function foo () { } console.log(foo); 複製程式碼
1。函式也會有提升,所以會被賦值覆蓋。
-
判斷以下結果 (IIFE & Function First)
var foo = 1; (function () { foo = 2; function foo () {} console.log(foo); })() console.log(foo); 複製程式碼
2,1
以上程式碼會被編譯器理解為如下形式
var foo = 1; (function () { var foo; function foo () { } foo = 2; console.log(foo); })() console.log(foo); 複製程式碼
-
判斷以下結果,如何按序輸出 (Closure)
for (var i = 0; i < 10; i++) { setTimeout(function () { console.log(i); }, 1000) } 複製程式碼
大約 1s 之後連續輸出 10 個 10。因為沒有塊級作用域,可以把 var 改成 let,也可以給 setTimeout 包裝一層 IIFE。
this & object prototypes
注意:以下均為瀏覽器環境中
-
判斷以下結果 (Default Binding)
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); 複製程式碼
會報錯,在函式的嚴格模式下,預設繫結其中的 this 指向 undefined。
-
判斷以下結果
"use strict"; var a = 2; let b = 3; console.log(this.a, this.b); 複製程式碼
2, undefined
在瀏覽器環境中 this 指向 window,而 var 宣告的變數會被掛在 window 上。而 let 宣告的變數不會掛在 window 上。
-
判斷以下結果 (Strict Mode & Default Binding)
function foo() { console.log( this.a ); } var a = 2; (function(){ "use strict"; foo(); })(); 複製程式碼
2
只有存在 this 的函式中設定嚴格模式,this 為 undefined。因此會正常輸出。
-
判斷以下結果 (Hard Binding)
function foo () { console.log(this.a); } const o1 = { a: 3 }; const o2 = { a: 4 }; foo.bind(o1).bind(o2)(); 複製程式碼
3
bind 為硬繫結,第一次繫結後 this 無法再次繫結。
-
如何實現
Function.prototype.bind
與Function.prototype.softBind
bind 為硬繫結,softBind 可以多次繫結 this。大致實現程式碼如下。bind 第二個引數可以預設函式引數,所以,bind 也是一個偏函式。另外,bind 也需要考慮
new
的情況。但以下示例主要集中在硬繫結和軟繫結的差異之上。Function.prototype.fakeBind = function (obj) { var self = this; return function () { self.call(obj); } } Function.prototype.softBind = function(obj) { var self = this; return function () { self.call(this === window? obj : this); } }; 複製程式碼
-
new
的過程中發生了什麼,判斷以下結果 (new)function F () { this.a = 3; return { a: 4; } } const f = new F(); console.log(f.a); 複製程式碼
4
new
的過程大致為如下幾個步驟- 建立一個新的物件
- this 指向例項,並且執行函式
- 如果沒有顯式返回,則預設返回這個例項
因為函式最後顯式返回了一個物件,所以列印為 4
-
什麼是
data descriptor
和accessor descriptor
兩者均通過
Object.defineProperty()
定義,有兩個公有的鍵值- configurable 設定該鍵是否可以刪除
- enumerable 設定是否可被遍歷
資料描述符有以下鍵值
- writable 該鍵是否可以更改
- value
訪問器描述符有以下鍵值
- set
- get 另外,也可以通過字面量的形式表示訪問器描述符
const obj = { get a() {}, set a(val) {} } 複製程式碼
Vue中
computed
的內部原理便是get
,而watch
的內部原理是set
-
如何訪問一個物件的屬性? ([[Get]])
訪問物件的屬性會觸發 [[Get]] 操作,大致簡述如下
- 是否被 Proxy 攔截,如果攔截,檢視攔截器的返回值,如果沒攔截,繼續下一步
- 檢查自身屬性,如果沒找到則繼續下一步
- 如果沒被找到,則在原型鏈上查詢,如果沒找到,則返回 undefined
查詢過程與 Scope 查詢變數很相似,只不過,物件屬性找不到,返回 undefined,而變數找不到報 Reference Error。
-
如何對一個物件的屬性賦值 ([[Put]])
對一個物件的屬性賦值會觸發 [[Put]] 操作,大致簡述如下
- 檢查是否被 Proxy 攔截
- 如果該物件屬性為自身屬性 (obj.hasOwnProperty('a') === true)
- 如果屬性是訪問描述符,則呼叫 setter 函式
- 如果屬性是 data descriptor,則檢查 writable 是否可寫
- 普通屬性,直接賦值
- 如果該物件屬性存在於原型鏈上
- 如果屬性是訪問描述符,則呼叫 setter 函式
- 如果屬性是 data descriptor,則檢查 writable 是否可寫。如果可寫,被自身屬性覆蓋,否則在嚴格模式下將會報錯
- 普通屬性,被自身屬性覆蓋
- 如果該物件不存在與原型鏈上,直接給自身屬性賦值
-
如何遍歷一個物件 ($$iterator)
給物件設定 Symbol.iterator 屬性
-
如何實現一個繼承 (Object.create & call)
在 ES6 時代可以簡單的通過 class & extends 實現繼承,ES5 時代用如下方法
function A () {} function B () { A.call(this) } B.prototype = Object.create(A.prototype) // B.prototype = new A() 不推薦 複製程式碼
-
如何實現
Object.create
至於為什麼在繼承的時候不推薦
new
,原因在於你很難保證 A 是一個純函式,比如它會有自身屬性,有可能操作 DOM 等。以下是一個簡單版本的實現,省略了第二個引數。Object.create = function (o) { function F() {} F.prototype = o; return new F(); } 複製程式碼
關注公眾號山月行,記錄我的技術成長,歡迎交流